如何:编写一个只能调用一次的线程安全方法?
Posted
技术标签:
【中文标题】如何:编写一个只能调用一次的线程安全方法?【英文标题】:How to: Write a thread-safe method that may only be called once? 【发布时间】:2012-03-01 10:21:20 【问题描述】:我正在尝试编写一个线程安全的方法,该方法只能被调用一次(每个对象实例)。如果之前调用过,应该抛出异常。
我想出了两个解决方案。他们都正确吗?如果不是,他们有什么问题?
与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;
如果我没记错的话,这个解决方案的优点是无锁(这在我的情况下似乎无关紧要),并且它需要更少的私有字段。
我也愿意接受合理的意见,应该首选哪种解决方案,如果有更好的方法,我也愿意提供进一步的建议。
【问题讨论】:
【参考方案1】:您的Interlocked.CompareExchange
解决方案看起来最好,并且(如您所说)是无锁的。它也比其他解决方案简单得多。锁是相当重量级的,而CompareExchange
可以编译为单个 CAS cpu 指令。我说用那个。
【讨论】:
出于好奇:当您说它“不那么复杂”时,您似乎指的是在百叶窗后面发生的一切;对于普通程序员,您如何判断Interlocked.ExchangeCompare
构造的可读性/易理解性?
@stakx:这就是 cmets 的用途。当程序员遇到他们不理解的东西时,他们应该查找它以便他们理解它。这就是他们如何成为更好的程序员的方式。
@thecoop 我不同意,是的,这是最正确的解决方案,但它并不简单,您需要了解原子操作等。这在某种程度上是一个初始化过程,我建议遵循初始化模式被广泛使用(例如双重检查锁)。此外,这些模式还可以防止您错过线程处理时很容易发生的事情。
这种事情是CompareExchange
设计的目的。它使用一个变量,编译成一条 CPU 指令,并且是一行代码。只要有评论,并且没有其他线程问题使其复杂化,那么就没有理由不使用它。如果您的任何程序员不了解它的作用,那么他们应该查找它。你不应该降低到他们的水平,他们应该上升到你的水平。如果他们不了解原子操作,那么他们应该学习。这就是他们变得更好的方式,这是每个人都应该渴望做的事情。
与普通的旧 Exchange 相比,使用 CompareExchange 有什么优势吗?比较似乎没有必要。【参考方案2】:
双重检查锁定模式是你所追求的:
这就是你所追求的:
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()
鲜为人知)而没有实现任何目标(正如您指出的那样,对锁定设置布尔标志的锁定较少是在这种情况下并不是真正的好处)。
【讨论】:
1. 这似乎使读/写someFlag
非原子。你确定这是正确的吗? 2. 基于Interlocked.CompareExchange
的解决方案怎么样?
sry 忘了最重要的一行
Hmm...如果你要抛出异常,会锁定这样的问题吗?
@stakx 注意需要把锁+标志static
,否则另一个对象实例可以再次执行代码
@stakx 我只是遵循了双重检查锁定模式,例外是您的要求。这个想法是遵循一种模式,以便其他人能够快速理解你所做的事情。在优化 可维护性之间通常需要权衡取舍,我的建议与模式一致。【参考方案3】:
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
【讨论】:
很抱歉,这并不能回答问题。 (顺便说一句,我知道任务并行库,但它不能在任何地方使用,例如,有问题的方法实现了接口方法。)以上是关于如何:编写一个只能调用一次的线程安全方法?的主要内容,如果未能解决你的问题,请参考以下文章