我已经看过很多lock
用法示例,它通常是这样的:
private static readonly object obj = new object();
lock (obj)
{
// code here
}
是否可以根据类的属性进行锁定?我不想在全球锁与方法调用的任何lock
声明,我想锁定功能只有在作为参数传递的对象具有相同的属性值,将其被在此之前,处理的另一对象.
那可能吗?这有道理吗?
这就是我的想法:
public class GmailController : Controller
{
private static readonly ConcurrentQueue queue = new ConcurrentQueue();
[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
就会变得毫无用处.我也知道我可以创建一个工作来检查最近的条目并消除重复,但我尝试了不同的东西.欢迎任何建议.
1> Eric Lippert..:
我首先要确保我理解该提案.给出的问题是我们有一些资源共享给多个线程,调用它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,其中context属性具有值X,并且调用MyWrite,其中context属性具有值Y,并且两个调用在两个不同的线程上竞争,则它们不是序列化的.但是,如果我们有两次调用MyWrite和调用MyRead,并且在所有这些调用中,context属性的值为Z,则这些调用将被序列化.
这可能吗?是.这不是一个好主意.如上所述,这是一个坏主意,你不应该这样做.
了解为什么这是一个坏主意是有益的.
首先,如果属性是值类型(如整数),则会失败.您可能会认为,我的上下文是一个ID号,它是一个整数,我想使用ID号123序列化对数据库的所有访问,并使用ID号345序列化所有访问,但不会对每个访问序列化这些访问其他.锁仅适用于引用类型,而装箱值类型总是会为您提供一个新分配的框,因此即使ID相同,也不会对锁进行争用.它将完全被打破.
其次,如果属性是字符串,则会严重失败.锁在逻辑上通过引用 "比较" ,而不是通过值.使用盒装整数,您总能得到不同的引用.使用字符串,您有时会得到不同的引用!(因为实习不一致.)你可能处于锁定"ABC"的情况,有时 "ABC"上的另一个锁等待,有时它不会!
但是,这是破的基本规则是:你必须永远在对象上锁定,除非该对象已被专门设计为一个锁定对象,并控制访问锁定的资源控制访问锁定对象相同的代码.
这里的问题不是锁定的"本地",而是全局的.假设你的财产是一个Frob
地方Frob
是引用类型. 您不知道进程中的任何其他代码是否也锁定了相同的代码Frob
,因此您不知道需要哪些锁定顺序约束来防止死锁.程序是否死锁是程序的全局属性.就像你可以用实心砖建造一个空心房子一样,你可以用一系列单独正确的锁来构建一个死锁程序.通过确保每个锁仅在您控制的私有对象上取出,您可以确保没有其他人锁定您的某个对象,因此分析您的程序是否包含死锁变得更加简单.
请注意,我说"更简单"而不是"简单".它将它简化为几乎不可能正确,从字面上看不可能得到正确.
所以,如果你一直在努力做到这一点,那么这样做的正确方法是什么?
正确的方法是实现一个新服务:一个锁对象提供者. LockProvider
需要能够哈希并比较相等的两个T
s.它提供的服务是:你告诉它你想要一个特定值的锁对象T
,它会为你提供规范的锁对象T
.当你完成后,你说你已经完成了.提供程序保留一个引用计数,指出它发出锁定对象的次数以及它返回的次数,并在计数变为零时从字典中删除它,这样我们就不会有内存泄漏.
显然,锁提供程序需要是线程安全的,并且需要极低的争用,因为它是一种旨在防止争用的机制,所以最好不要引起争用!如果这是你打算走下去的道路,你需要有一个C#线程专家来设计和实现这个对象.这很容易弄错.正如我在您的帖子的评论中所指出的那样,您正在尝试将并发队列用作一种糟糕的锁定提供程序,并且它是大量的竞争条件错误.
这是在所有.NET编程中纠正的一些最难的代码.我已经成为一名.NET程序员已有近20年的时间并实现了部分编译器,我认为自己无法胜任这些东西.寻求实际专家的帮助.
@Alisson:这个注释太短,无法解释如何在.NET中实现锁对象,但短版本是:**.NET中每个引用类型的对象都有一个在其内存映像中保留的同步块**,同步块用于存储在该对象上取出锁定时所需的信息.所以底层机制不需要比较两个锁是否相等; 它只是在同步块中查看是否有任何东西.这就是为什么锁始终绑定到*特定对象实例*; 信息实际上在对象中.