C#手动锁定/解锁

我在C#中有一个函数,可以从多个线程多次调用,我希望它只能完成一次,所以我想到了这个:

class MyClass { bool done = false; public void DoSomething() { lock(this) if(!done) { done = true; _DoSomething(); } } } 

问题是_DoSomething需要很长时间,我不希望许multithreading等待它,只要他们可以看到done是真的。
这样的事情可以解决方法:

 class MyClass { bool done = false; public void DoSomething() { bool doIt = false; lock(this) if(!done) doIt = done = true; if(doIt) _DoSomething(); } } 

但只是手动锁定和解锁会更好。
如何像lock(object)一样手动锁定和解锁? 我需要它使用相同的接口作为lock以便这种手动方式和lock将相互阻塞(对于更复杂的情况)。

lock关键字只是Monitor.Enter和Monitor.Exit的语法糖:

 Monitor.Enter(o); try { //put your code here } finally { Monitor.Exit(o); } 

是相同的

 lock(o) { //put your code here } 

托马斯建议双重检查锁定他的答案。 这是有问题的。 首先, 你不应该使用低锁技术,除非你已经certificate你有一个真正的性能问题,这是通过低锁技术解决的 。 低锁技术很难做到正确。

其次,这是有问题的,因为我们不知道“_DoSomething”会做什么,或者我们将依赖它的行为的后果。

第三,正如我在上面的评论中指出的那样,当另一个线程实际上仍处于执行过程中时,返回_DoSomething已“完成”似乎很疯狂。 我不明白为什么你有这个要求,我会认为这是一个错误。 即使在“_DoSomething”之后设置“完成”,这种模式的问题仍然存在。

考虑以下:

 class MyClass { readonly object locker = new object(); bool done = false; public void DoSomething() { if (!done) { lock(locker) { if(!done) { ReallyDoSomething(); done = true; } } } } int x; void ReallyDoSomething() { x = 123; } void DoIt() { DoSomething(); int y = x; Debug.Assert(y == 123); // Can this fire? } 

这是C#的所有可能实现中的线程安全吗? 我认为不是。 请记住, 处理器缓存可能会及时移动非易失性读取 。 C#语言保证易失性读取与锁等关键执行点一致排序,并且它保证非易失性读取在单个执行线程中是一致的,但它不保证非易失性读取在任何执行中都是一致的。跨越执行线程的方式。

我们来看一个例子。

假设有两个线程,Alpha和Bravo。 两者都在MyClass的新实例上调用DoIt。 怎么了?

在线程Bravo上,处理器高速缓存恰好对x的内存位置进行(非易失性!)获取,其中包含零。 “完成”恰好位于不同的内存页面上,而目前尚未将其提取到缓存中。

在线程Alpha在“同一时间”在不同的处理器上DoIt调用DoSomething。 线程Alpha现在运行在那里的一切。 当线程Alpha完成其工作时,完成为真,并且Alpha在Alpha处理器上为123。 Thread Alpha的处理器将这些事实刷回主存储器。

Thread bravo现在运行DoSomething。 它将包含“done”的主内存页面读入处理器缓存,并发现它是真的。

所以现在“完成”是正确的,但在线程Bravo的处理器缓存中“x”仍为零。 线程Bravo不需要使包含“x”为零的高速缓存部分无效,因为在线程Bravo上既没有读取“完成”也没有读取“x”都是易失性读取。

建议的双重检查锁定版本实际上并不是双重检查锁定。 当您更改双重检查的锁定模式时,您需要从头开始重新开始并重新分析所有内容

使这个版本的模式正确的方法是至少将“完成”的第一次读取变为易失性读取。 然后,不允许读取“x”将易失性读取“提前”移动到“完成”。

您可以在锁定之前之后检查已done的值:

  if (!done) { lock(this) { if(!done) { done = true; _DoSomething(); } } } 

这样,如果done则不会进入锁定状态。 如果两个线程同时进入第一个线程,则锁内的第二个检查是应对竞争条件。

顺便说一下 , 你不应该锁定this ,因为它可能导致死锁。 锁定私有字段(如private readonly object _syncLock = new object()

lock关键字只是Monitor类的语法糖。 你也可以调用Monitor.Enter()Monitor.Exit()

但Monitor类本身也有TryEnter()Wait()函数,它们可以帮助你的情况。

我知道这个答案会迟到几年,但目前的答案似乎都没有解决你的实际情况,只有在你的评论之后才能明白:

其他线程不需要使用ReallyDoSomething生成的任何信息。

如果其他线程不需要等待操作完成,则问题中的第二个代码段可以正常工作。 您可以通过完全取消锁定并使用primefaces操作来进一步优化它:

 private int done = 0; public void DoSomething() { if (Interlocked.Exchange(ref done, 1) == 0) // only evaluates to true ONCE _DoSomething(); } 

此外,如果您的_DoSomething()是一个即发即_DoSomething()操作,那么您甚至可能不需要第一个线程等待它,允许它在线程池上的任务中异步运行:

 int done = 0; public void DoSomething() { if (Interlocked.Exchange(ref done, 1) == 0) Task.Factory.StartNew(_DoSomething); }