基于类属性的C#锁定

时间:2018-09-06 21:45:15

标签: c# locking

我已经看到了lock用法的许多示例,通常是这样的:

private static readonly object obj = new object();

lock (obj)
{
    // code here
}

是否可以基于类的属性进行锁定?我不想为lock语句对方法的任何调用全局锁定,我只想在作为参数传递的对象与另一个对象在之前处理过的对象具有相同的属性值时才锁定那个。

有可能吗?真的有道理吗?

这就是我的想法:

public class GmailController : Controller
{

    private static readonly ConcurrentQueue<PushRequest> queue = new ConcurrentQueue<PushRequest>();

    [HttpPost]
    public IActionResult ProcessPushNotification(PushRequest push)
    {
        var existingPush = queue.FirstOrDefault(q => q.Matches(push));
        if (existingPush == null)
        {
            queue.Enqueue(push);
            existingPush = push;
        }
        try
        {
            // lock if there is an existing push in the
            // queue that matches the requested one
            lock (existingPush)
            {
                // process the push notification
            }
        }
        finally
        {
            queue.TryDequeue(out existingPush);
        }
    }
}

背景:我有一个API,当我们的用户发送/接收电子邮件时,我会从Gmail的API接收推送通知。但是,如果某人同时向两个用户发送消息,则会收到两个推送通知。我的第一个想法是在插入之前查询数据库(基于主题,发件人等)。在极少数情况下,第二个呼叫的查询是在前一个呼叫的SaveChanges之前进行的,因此我最终会重复。

我知道,如果我想向外扩展,lock将变得毫无用处。我也知道我可以创建一个工作来检查最近的条目并消除重复项,但是我正在尝试不同的方法。欢迎任何建议。

2 个答案:

答案 0 :(得分:8)

首先让我确保我了解该建议。给定的问题是我们有一些资源共享给多个线程,称为database,它接受​​两个操作:Read(Context)Write(Context)。提议是基于上下文的属性具有锁定粒度。那就是:

void MyRead(Context c) 
{
  lock(c.P) { database.Read(c); }
}
void MyWrite(Context c)
{
  lock(c.P) { database.Write(c); }
}

所以现在,如果我们有一个调用MyRead的上下文属性值为X,有一个MyWrite调用的上下文属性值为Y,并且这两个调用在两个不同的线程上进行竞争,则它们不是 序列化。但是,例如,如果有两个对MyWrite的调用和对MyRead的调用,并且所有这些上下文属性的值均为Z,则这些调用 被序列化。

这是可能吗?是。那并不是一个好主意。如上所述,这是个坏主意,您不应该这样做。

了解为什么这是一个坏主意很有启发性。

首先,如果属性是一个值类型(如整数),则此操作只会失败。您可能会想,我的上下文是一个ID号,它是一个整数,我想使用ID号123序列化对数据库的所有访问,并使用ID号345序列化对所有访问的访问,但不对每个访问进行序列化其他。 锁仅适用于引用类型,并且装箱值类型始终会为您提供一个新分配的框,因此即使绝不会使用该锁id是一样的。它会被完全破坏。

第二,如果该属性是一个字符串,它将严重失败。锁在逻辑上是通过 reference 而不是 value 进行“比较”的。使用装箱的整数,您总是会获得不同的引用。使用字符串,您有时会得到不同的引用! (由于 interning 的应用不一致。)您可能处在锁定“ ABC”的状态,而有时有时锁定“ ABC”的另一种情况是,有时不!

但是被打破的基本规则是:除非该对象专门设计为锁定对象,否则您绝不能锁定该对象,并且控制对锁定资源的访问的相同代码控制对对象的访问。锁定对象

这里的问题不是锁的“本地”问题,而是全局的问题。假设您的媒体资源是Frob,其中Frob是引用类型。 您不知道您的进程中是否还有其他代码也锁定在相同的Frob 上,因此,您不知道为防止死锁需要什么锁定顺序约束。程序是否死锁是程序的 global 属性。就像您可以用实心砖建造空心房屋一样,您也可以根据一组正确的锁来建造死锁程序。通过确保仅在您控制的私有对象上取得所有锁定,您可以确保没有其他人锁定您的对象之一,因此可以分析程序是否包含死锁。变得更简单。

请注意,我说的是“简单”而不是“简单”。它将它从从根本上不可能得到正确的减少到几乎不可能得到正确的

因此,如果您不愿意这样做,什么是正确的方法?

正确的方法是实施一项新服务:锁对象提供程序LockProvider<T>必须能够哈希比较相等性两个T。它提供的服务是:告诉它您想要一个特定值T的锁定对象,并为您返回该T的规范锁定对象。完成后,您说您已完成。提供程序保留一个引用计数,该计数提供了多少次派发锁对象以及将它退回多少次,并在计数变为零时将其从其字典中删除,这样我们就不会发生内存泄漏。

很显然,锁提供者需要是线程安全的,并且必须具有极低的争用,因为这是一种旨在防止争用的机制,因此最好不要 cause !如果这是您要走的路,您需要聘请C#线程专家来设计和实现此对象。这很容易弄错。正如我在您的帖子评论中所指出的那样,您正在尝试将并发队列用作一种不良的锁提供程序,并且这是大量竞争条件错误。

这是所有.NET编程中最难纠正的一些代码。我从事.NET程序员已有近20年的时间,并且是编译器的已实现部分,但我认为自己没有能力正确解决这些问题。寻求实际专家的帮助。

答案 1 :(得分:0)

尽管我发现埃里克·利珀特(Eric Lippert)的答案很棒,并将其标记为正确的答案(而且我不会改变它),但他的想法让我开始思考,我想与大家分享我针对此问题找到的另一种解决方案(我想感谢我的反馈),即使我最终将我的代码使用Azure函数(这样就没有意义)了,也不会使用它,而且还有一项cron作业来检测并消除可能的重复项。

public class UserScopeLocker : IDisposable
{
    private static readonly object _obj = new object();

    private static ICollection<string> UserQueue = new HashSet<string>();

    private readonly string _userId;

    protected UserScopeLocker(string userId)
    {
        this._userId = userId;
    }

    public static UserScopeLocker Acquire(string userId)
    {
        while (true)
        {
            lock (_obj)
            {
                if (UserQueue.Contains(userId))
                {
                    continue;
                }
                UserQueue.Add(userId);
                return new UserScopeLocker(userId);
            }
        }
    }

    public void Dispose()
    {
        lock (_obj)
        {
            UserQueue.Remove(this._userId);
        }
    }
}

...然后,您将像这样使用它:

[HttpPost]
public IActionResult ProcessPushNotification(PushRequest push)
{
    using(var scope = UserScopeLocker.Acquire(push.UserId))
    {
        // process the push notification
        // two threads can't enter here for the same UserId
        // the second one will be blocked until the first disposes
    }
}

想法是:

  • UserScopeLocker具有受保护的构造函数,确保您调用Acquire
  • _obj是私有静态只读,只有UserScopeLocker可以锁定此对象。
  • _userId是一个私有只读字段,可确保即使是其自己的类也无法更改其值。
  • lock是在检查,添加和删除时完成的,因此两个线程无法在这些操作上竞争。

我检测到的可能的缺陷:

  • 由于UserScopeLocker依赖IDisposable来发布某些UserId,因此我不能保证调用方将正确使用using语句(或手动处置范围对象)。
  • 我不能保证范围不会在递归函数中使用(因此可能导致死锁)。
  • 我不能保证using语句中的代码不会调用另一个试图向用户获取作用域的函数(这也会导致死锁)。