基于类属性的C#锁
我见过很多使用基于类属性的C#锁,c#,locking,C#,Locking,我见过很多使用lock的例子,通常是这样的: private static readonly object obj = new object(); lock (obj) { // code here } [HttpPost] public IActionResult ProcessPushNotification(PushRequest push) { using(var scope = UserScopeLocker.Acquire(push.UserId)) {
lock
的例子,通常是这样的:
private static readonly object obj = new object();
lock (obj)
{
// code here
}
[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
}
}
是否可以基于类的属性进行锁定?我不想用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);
}
}
}
公共类Gmail控制器:控制器
{
私有静态只读ConcurrentQueue队列=新ConcurrentQueue();
[HttpPost]
公共IActionResult ProcessPushNotification(PushRequest push)
{
var existingPush=queue.FirstOrDefault(q=>q.Matches(push));
if(existingPush==null)
{
排队,排队(推);
现有推力=推力;
}
尝试
{
//如果系统中存在推送,则锁定
//与请求的队列匹配的队列
锁定(现有推送)
{
//处理推送通知
}
}
最后
{
queue.TryDequeue(out existingPush);
}
}
}
背景:我有一个API,当我们的用户发送/接收电子邮件时,我从Gmail的API接收推送通知。但是,如果有人同时向两个用户发送消息,我会收到两个推送通知。我的第一个想法是在插入之前查询数据库(基于主题、发件人等)。在一些罕见的情况下,第二次调用的查询是在前一次调用的
SaveChanges
之前进行的,因此我最终得到了重复的查询
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);
}
}
}
我知道如果我想扩展,
lock
将变得毫无用处。我也知道我可以创建一个作业来检查最近的条目并消除重复项,但我尝试了一些不同的方法。欢迎提出任何建议。首先让我确定我理解了这个建议。给出的问题是,我们有一些资源共享给多个线程,称之为数据库
,它允许两个操作:读取(上下文)
和写入(上下文)
。建议基于上下文的属性设置锁粒度。即:
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的调用,并且在所有调用中上下文属性的值都是Z,那么这些调用都是序列化的
这可能吗?对这不是个好主意。如上所述,这是一个坏主意,您不应该这样做
了解为什么这是个坏主意是很有启发性的
首先,如果属性是值类型(如整数),则此操作将失败。您可能会认为,我的上下文是一个ID号,这是一个整数,我希望使用ID号123序列化对数据库的所有访问,并使用ID号345序列化所有访问,但不序列化这些访问锁只对引用类型起作用,将值类型装箱总是会给您一个新分配的框,因此即使ID相同,锁也不会受到质疑。它将被完全破坏
第二,如果属性是字符串,则会严重失败。锁在逻辑上是通过引用而不是通过值进行“比较”的。对于装箱整数,总是会得到不同的引用。对于字符串,您有时会得到不同的引用!(因为实习申请不一致。)你可能正处于锁定“ABC”的状态,有时“ABC”上的另一个锁会等待,有时不会
但被打破的基本规则是:除非对象被专门设计为锁定对象,否则决不能锁定该对象,并且控制对锁定资源的访问的代码控制对锁定对象的访问
这里的问题不是锁的“局部”问题,而是全局问题。假设您的属性是Frob
,其中Frob
是引用类型您不知道流程中是否有任何其他代码也锁定在同一Frob
上,因此您不知道防止死锁所需的锁顺序约束。程序是否死锁是程序的全局属性。就像你可以用实心砖建造一座空心房子一样,你也可以用一组单独正确的锁来建造一个死锁程序。通过确保每个锁只在您控制的私有对象上被取出,您可以确保没有其他人在锁定您的一个对象,因此对您的程序是否包含死锁的分析变得更简单
请注意,我说的是“更简单”而不是“简单”。它把它从字面上的不可能得到正确,减少到几乎不可能得到正确
那么,如果你下定决心要做这件事,那正确的方法是什么呢
正确的方法是实现一个新的服务:锁对象提供程序LockProvider
需要能够散列并比较等式2T
s。它提供的服务是:你告诉它你想要一个锁对象来表示一个特定的T
,它会给你这个T
的规范锁对象。当你完成时,你说你完成了。提供程序保留一个引用计数,记录它发出锁对象的次数和取回锁对象的次数,并在计数为零时将其从字典中删除,这样就不会出现内存泄漏
显然,锁提供程序需要是线程安全的,并且需要具有极低的争用,因为它是一种设计用于防止争用的机制,所以它具有