如何:编写一个只能调用一次的线程安全方法
本文关键字:一次 方法 调用 安全 线程 一个 如何 | 更新日期: 2023-09-27 18:26:10
我正在尝试编写一个线程安全方法,该方法只能调用一次(每个对象实例)。如果以前调用过异常,则应抛出该异常。
我提出了两个解决方案。他们都对吗?如果没有,他们怎么了?
-
使用
lock
:public void Foo() { lock (fooLock) { if (fooCalled) throw new InvalidOperationException(); fooCalled = true; } … } private object fooLock = new object(); private bool fooCalled;
-
使用
Interlocked.CompareExchange
:public void Foo() { if (Interlocked.CompareExchange(ref fooCalled, 1, 0) == 1) throw new InvalidOperationException(); … } private int fooCalled;
如果我没有错的话,这个解决方案的优点是无锁(在我的情况下,这似乎无关紧要),并且它需要更少的私有字段。
我也愿意接受合理的意见,哪种解决方案应该是首选的,如果有更好的方法,我也愿意进一步提出建议。
您的Interlocked.CompareExchange
解决方案看起来最好,而且(正如您所说)是无锁的。它也比其他解决方案复杂得多。锁相当重,而CompareExchange
可以编译为单个CAS cpu指令。我说用那个吧。
双重检查锁定模式就是您想要的:
这就是你想要的:
class Foo
{
private object someLock = new object();
private object someFlag = false;
void SomeMethod()
{
// to prevent locking on subsequent calls
if(someFlag)
throw new Exception();
// to make sure only one thread can change the contents of someFlag
lock(someLock)
{
if(someFlag)
throw new Exception();
someFlag = true;
}
//execute your code
}
}
一般来说,当遇到这样的问题时,试着遵循上面这样的众所周知的模式
这使得它易于识别,也不容易出错,因为在遵循模式时,尤其是在线程处理时,您不太可能错过一些东西
在您的情况下,第一个if没有多大意义,但通常您希望执行实际的逻辑,然后设置标志。第二个线程将在您执行(可能相当昂贵)代码时被阻塞。
关于第二个示例:
是的,这是正确的,但不要让它变得更复杂。你应该有充分的理由不使用简单的锁定,在这种情况下,它会让代码变得更复杂(因为Interlocked.CompareExchange()
不太为人所知),但却没有实现任何目标(正如你所指出的,在这种情况下,对设置布尔标志的锁定进行无锁定并不是一个真正的好处)。
Task task = new Task((Action)(() => { Console.WriteLine("Called!"); }));
public void Foo()
{
task.Start();
}
public void Bar()
{
Foo();
Foo();//this line will throws different exceptions depends on
//whether task in progress or task has already been completed
}