如何:编写一个只能调用一次的线程安全方法?

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
        

【讨论】:

很抱歉,这并不能回答问题。 (顺便说一句,我知道任务并行库,但它不能在任何地方使用,例如,有问题的方法实现了接口方法。)

以上是关于如何:编写一个只能调用一次的线程安全方法?的主要内容,如果未能解决你的问题,请参考以下文章

线程安全与可重入编写方法

关于使用 D-Bus 异步方法调用的 Python 中的线程安全

ARRAYLIST如何保证线程安全

ArrayList如何保证线程安全

如何保证ArrayList线程安全

accept() 线程安全吗?