C#锁定基于类属性

Ali*_*son 0 c# locking

我已经看过很多lock用法示例,它通常是这样的:

private static readonly object obj = new object();

lock (obj)
{
    // code here
}
Run Code Online (Sandbox Code Playgroud)

是否可以根据类的属性进行锁定?我不想在全球锁与方法调用的任何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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

我知道,如果我想扩大规模,lock就会变得毫无用处.我也知道我可以创建一个工作来检查最近的条目并消除重复,但我尝试了不同的东西.欢迎任何建议.

Eri*_*ert 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); }
}
Run Code Online (Sandbox Code Playgroud)

所以现在如果我们调用MyRead,其中context属性具有值X,并且调用MyWrite,其中context属性具有值Y,并且两个调用在两个不同的线程上竞争,则它们不是序列化的.但是,如果我们有两次调用MyWrite和调用MyRead,并且在所有这些调用中,context属性的值为Z,则这些调用将被序列化.

这可能吗?是.这不是一个好主意.如上所述,这是一个坏主意,你不应该这样做.

了解为什么这是一个坏主意是有益的.

首先,如果属性是值类型(如整数),则会失败.您可能会认为,我的上下文是一个ID号,它是一个整数,我想使用ID号123序列化对数据库的所有访问,并使用ID号345序列化所有访问,但不会对每个访问序列化这些访问其他.锁仅适用于引用类型,而装箱值类型总是会为您提供一个新分配的框,因此即使ID相同,也不会对锁进行争用.它将完全被打破.

其次,如果属性是字符串,则会严重失败.锁在逻辑上通过引用 "比较" ,而不是通过值.使用盒装整数,您总能得到不同的引用.使用字符串,您有时会得到不同的引用!(因为实习不一致.)你可能处于锁定"ABC"的情况,有时 "ABC"上的另一个锁等待,有时它不会!

但是,这是破的基本规则是:你必须永远在对象上锁定,除非该对象已被专门设计为一个锁定对象,并控制访问锁定的资源控制访问锁定对象相同的代码.

这里的问题不是锁定的"本地",而是全局的.假设你的财产是一个Frob地方Frob是引用类型. 您不知道进程中的任何其他代码是否也锁定了相同的代码Frob,因此您不知道需要哪些锁定顺序约束来防止死锁.程序是否死锁是程序的全局属性.就像你可以用实心砖建造一个空心房子一样,你可以用一系列单独正确的锁来构建一个死锁程序.通过确保每个锁仅在您控制的私有对象上取出,您可以确保没有其他人锁定您的某个对象,因此分析您的程序是否包含死锁变得更加简单.

请注意,我说"更简单"而不是"简单".它将它简化为几乎不可能正确,从字面上看不可能得到正确.

所以,如果你一直在努力做到这一点,那么这样做的正确方法是什么?

正确的方法是实现一个新服务:一个锁对象提供者. LockProvider<T>需要能够哈希并比较相等的两个Ts.它提供的服务是:你告诉它你想要一个特定值的锁对象T,它会为你提供规范的锁对象T.当你完成后,你说你已经完成了.提供程序保留一个引用计数,指出它发出锁定对象的次数以及它返回的次数,并在计数变为零时从字典中删除它,这样我们就不会有内存泄漏.

显然,锁提供程序需要是线程安全的,并且需要极低的争用,因为它是一种旨在防止争用的机制,所以最好不要引起争用!如果这是你打算走下去的道路,你需要有一个C#线程专家来设计和实现这个对象.这很容易弄错.正如我在您的帖子的评论中所指出的那样,您正在尝试将并发队列用作一种糟糕的锁定提供程序,并且它是大量的竞争条件错误.

这是在所有.NET编程中纠正的一些最难的代码.我已经成为一名.NET程序员已有近20年的时间并实现了部分编译器,我认为自己无法胜任这些东西.寻求实际专家的帮助.

  • @Alisson:这个注释太短,无法解释如何在.NET中实现锁对象,但短版本是:**.NET中每个引用类型的对象都有一个在其内存映像中保留的同步块**,同步块用于存储在该对象上取出锁定时所需的信息.所以底层机制不需要比较两个锁是否相等; 它只是在同步块中查看是否有任何东西.这就是为什么锁始终绑定到*特定对象实例*; 信息实际上在对象中. (2认同)