我在C#中有一个函数,可以从多个线程多次调用,我希望它只能完成一次,所以我想到了这个:
class MyClass
{
bool done = false;
public void DoSomething()
{
lock(this)
if(!done)
{
done = true;
_DoSomething();
}
}
}
问题是_DoSomething
需要很长时间,当他们看到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
将相互阻止(对于更复杂的情况)。
答案 0 :(得分:29)
lock
关键字只是Monitor.Enter和Monitor.Exit的语法糖:
Monitor.Enter(o);
try
{
//put your code here
}
finally
{
Monitor.Exit(o);
}
与
相同lock(o)
{
//put your code here
}
答案 1 :(得分:26)
其次,这是有问题的,因为我们不知道“_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”的主内存页面读入处理器缓存,并看到它是真的。
所以现在“完成”是真的,但是“x”在线程Bravo的处理器缓存中仍为零。 线程Bravo不需要使包含“x”的缓存部分无效,因为在线程Bravo上,“完成”和“x”的读取都不是易失性读取。
建议的双重检查锁定版本实际上并不是双重检查锁定。当您更改双重检查的锁定模式时,您需要从头开始重新开始并重新分析所有内容。
使这个版本的模式正确的方法是至少将第一次“完成”读入易失性读取。然后,不允许读取“x”将易失性读取“提前”移动到“完成”。
答案 2 :(得分:4)
您可以在锁定后检查之前的done
和的值:
if (!done)
{
lock(this)
{
if(!done)
{
done = true;
_DoSomething();
}
}
}
这样,如果done
为真,您就不会进入锁定状态。如果两个线程同时进入第一个if
,则锁内的第二个检查是应对竞争条件。
BTW,you shouldn't lock on this
,因为它可能导致死锁。锁定私人字段(如private readonly object _syncLock = new object()
)
答案 3 :(得分:2)
lock
关键字只是Monitor
类的语法糖。您也可以拨打Monitor.Enter()
,Monitor.Exit()
。
但是Monitor类本身也有函数TryEnter()
和Wait()
,它们可以帮助您处理。
答案 4 :(得分:1)
我知道这个答案会迟到几年,但目前的答案似乎都没有解决你的实际情况,只有在你comment之后才明白:
其他主题不需要使用ReallyDoSomething生成的任何信息。
如果其他线程不需要等待操作完成,则问题中的第二个代码段可以正常工作。您可以通过完全取消锁定并使用原子操作来进一步优化它:
private int done = 0;
public void DoSomething()
{
if (Interlocked.Exchange(ref done, 1) == 0) // only evaluates to true ONCE
_DoSomething();
}
此外,如果您的_DoSomething()
是一个即发即弃操作,那么您甚至可能不需要第一个线程等待它,允许它在线程池上的任务中异步运行:
int done = 0;
public void DoSomething()
{
if (Interlocked.Exchange(ref done, 1) == 0)
Task.Factory.StartNew(_DoSomething);
}