C#锁定基于类属性
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C#锁定基于类属性相关的知识,希望对你有一定的参考价值。
我已经看过许多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
将变得无用。我也知道我可以创建一个工作来检查最近的条目并消除重复,但我尝试了不同的东西。欢迎任何建议。
我首先要确保我理解该提案。给出的问题是我们有一些资源共享给多个线程,称之为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>
需要能够哈希并比较两个T
s的平等。它提供的服务是:你告诉它你想要一个特定值的T
的锁对象,它会为你提供T
的规范锁对象。当你完成后,你说你已经完成了。提供程序保留一个引用计数,指出它发出锁定对象的次数以及它返回的次数,并在计数变为零时从字典中删除它,这样我们就不会有内存泄漏。
显然,锁提供程序需要是线程安全的,并且需要极低的争用,因为它是一种旨在防止争用的机制,所以最好不要引起争用!如果这是你打算走下去的道路,你需要有一个C#线程专家来设计和实现这个对象。这很容易弄错。正如我在您的帖子的评论中所指出的那样,您正在尝试将并发队列用作一种糟糕的锁定提供程序,并且它是大量的竞争条件错误。
这是在所有.NET编程中纠正的一些最难的代码。我已经成为一名.NET程序员已有近20年的时间并实现了编译器的部分功能,我认为自己不能胜任这些东西。寻求实际专家的帮助。
虽然我发现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
是一个私人的readonly字段,确保即使它自己的类也不能改变它的价值。lock
在检查,添加和删除时完成,因此两个线程无法在这些操作上竞争。
我发现可能的缺陷:
- 由于
UserScopeLocker
依赖IDisposable
释放一些UserId
,我无法保证调用者将正确使用using
语句(或手动处理范围对象)。 - 我不能保证范围不会在递归函数中使用(因此可能导致死锁)。
- 我不能保证
using
语句中的代码不会调用另一个也试图获取用户范围的函数(这也会导致死锁)。
以上是关于C#锁定基于类属性的主要内容,如果未能解决你的问题,请参考以下文章
csharp C#代码片段 - 使类成为Singleton模式。 (C#4.0+)https://heiswayi.github.io/2016/simple-singleton-pattern-us