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

本文关键字:一次 方法 调用 安全 线程 一个 如何 | 更新日期: 2023-09-27 18:26:10

我正在尝试编写一个线程安全方法,该方法只能调用一次(每个对象实例)。如果以前调用过异常,则应抛出该异常。

我提出了两个解决方案。他们都对吗?如果没有,他们怎么了?

  1. 使用lock:

    public void Foo()
    {
        lock (fooLock)
        {
            if (fooCalled) throw new InvalidOperationException();
            fooCalled = true;
        }
        …
    }
    private object fooLock = new object();
    private bool fooCalled;
    
  2. 使用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
    }