Как: написать потокобезопасный метод, который можно вызывать только один раз? - PullRequest
8 голосов
/ 01 марта 2012

Я пытаюсь написать потокобезопасный метод, который можно вызывать только один раз (для каждого экземпляра объекта). Должно быть выброшено исключение, если оно было вызвано ранее.

Я предложил два решения. Они оба правы? Если нет, что с ними не так?

  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;
    

    Если я не ошибаюсь, это решение имеет то преимущество, что оно не блокируется (что в моем случае кажется неуместным) и требует меньше приватных полей.

Я также открыт для обоснованных мнений о том, какое решение следует предпочесть, и для дальнейших предложений, если есть лучший способ.

Ответы [ 3 ]

6 голосов
/ 01 марта 2012

Ваше решение Interlocked.CompareExchange выглядит лучше всего и (как вы сказали) не блокируется. Это также значительно менее сложно, чем другие решения. Блокировки довольно тяжелые, тогда как CompareExchange может быть скомпилирован в одну инструкцию CAS CPU. Я говорю, иди с этим.

0 голосов
/ 01 марта 2012
    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
    }    
0 голосов
/ 01 марта 2012

Паттер блокировки с двойной проверкой - это то, что вам нужно:

Это то, что вам нужно:

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
  }
}

В общем, когда вы сталкиваетесь с такими проблемами, пытайтесь следовать хорошо известным шаблонам.как приведенный выше.
Это делает его распознаваемым и менее подверженным ошибкам, так как вы с меньшей вероятностью что-то пропустите при следовании шаблону, особенно когда дело доходит до многопоточности.
В вашем случае первое, если не многосмысла, но часто вы захотите выполнить реальную логику, а затем установить флаг.Второй поток будет заблокирован, пока вы выполняете свой (возможно, довольно дорогостоящий) код.

О втором примере:
Да, это правильно, но не усложняйте его, чем это.У вас должны быть очень веские причины не использовать простую блокировку, и в этой ситуации это делает код более сложным (потому что Interlocked.CompareExchange() менее известен), ничего не добиваясь (как вы указали, что блокировка меньше против блокировки для установки логического флагав данном случае это не совсем выгодно).

...