代码之家  ›  专栏  ›  技术社区  ›  Alisson Reinaldo Silva

基于类属性的C#锁

  •  0
  • Alisson Reinaldo Silva  · 技术社区  · 6 年前

    我见过很多这样的例子 lock 用法,通常是这样的:

    private static readonly object obj = new object();
    
    lock (obj)
    {
        // code here
    }
    

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

    有可能吗?这有道理吗?

    这就是我的想法:

    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 上一个电话,所以我有重复的。

    我知道如果我想扩大规模,

    2 回复  |  直到 6 年前
        1
  •  9
  •   Eric Lippert    6 年前

    让我先确认一下我是否理解这个建议。给出的问题是,我们有一些资源共享给多个线程,称之为 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); }
    }
    

    已序列化。但是,如果我们有两个调用MyWrite和一个调用MyRead,并且在所有调用中context属性的值都是Z,那么这些调用 已序列化。

    可能的 ? 对。那可不是个好主意。如上所述,这是一个坏主意,你不应该这样做。

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

    首先,如果属性是值类型(如整数),则此操作将失败。你可能会想,我的上下文是一个ID号,这是一个整数,我想用ID号123序列化对数据库的所有访问,用ID号345序列化所有访问,但不序列化这些访问。 锁定仅适用于引用类型 ,和 装箱一个值类型总是给你一个新分配的框 从未 即使身份证相同也会受到质疑。它会完全破碎。

    参考 ,而不是 价值 有时 实习 你可能处于锁定“ABC”和 另一个“ABC”锁等待,有时它不!

    决不能锁定对象,除非该对象被专门设计为锁定对象,并且控制对锁定资源的访问的代码控制对锁定对象的访问 .

    这里的问题不是锁的“局部”问题,而是全局问题。假设你的财产是 Frob 哪里 是引用类型。 弗罗布 全球的 ,您可以确保没有其他人锁定您的某个对象,因此分析您的程序是否包含死锁变得更简单。

    注意,我说的是“简单”而不是“简单”。它把它减少到 几乎不可能得到正确的答案 ,来自 简直不可能得到正确的答案

    正确的方法是实现一个新的服务:一个 锁定对象提供程序 . LockProvider<T> 需要能够 搞砸 T s、 它提供的服务是:您告诉它您想要一个锁对象,用于 ,它将返回该对象的规范锁对象 T型

    显然,锁提供程序需要是线程安全的,并且需要非常低的争用,因为 它是一种旨在防止争用的机制 ,所以最好不要 原因 你需要一个C线程专家来设计和实现这个对象 . 这很容易出错。正如我在你文章的评论中提到的,你试图使用一个并发队列作为一种糟糕的锁提供者,这是一个大量的竞争条件错误。

    在所有.NET编程中,这是最难纠正的代码之一。我做了将近20年的.NET程序员 编译器的实现部分

        2
  •  0
  •   Alisson Reinaldo Silva    6 年前

    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 是private static readonly,只有 用户范围锁定器 可以锁定此对象。
    • _userId 是一个私有的只读字段,确保即使它自己的类也不能更改它的值。
    • lock 是在检查、添加和删除时完成的,因此两个线程不能在这些操作上竞争。

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