понедельник, 12 сентября 2011 г.

The Interlocked Anything Pattern

Сегодня, хочу разобрать описанный Джефри Рихтером паттерн - The Interlocked Anything Pattern. Данный паттерн показан к применению для синхронизации потоков с малой конкуренцией, в этом случае он отрабатывает быстрее lock, но с увеличением конкуренции производительность резко падает. Используем мы его очень часто, даже не задумываясь об этом, события это не что иное, как обертка для использования делегатов, с применением The Interlocked Anything Pattern.

Рассмотрим поближе, при объявлении в типе события:

  1. public sealed class SomeType  
  2. {  
  3.     public event DoSomthing SomeEvent;  
  4. }  
  5. public delegate void DoSomthing();  


Компилятор C# создаст следующий код:

  1. public sealed class SomeType  
  2. {  
  3.     private DoSomthing _someEvent;  
  4.   
  5.     public event DoSomthing SomeEvent  
  6.     {  
  7.         add {  
  8.             DoSomthing doSomthing = this._someEvent;  
  9.             DoSomthing comparand;  
  10.             do {  
  11.                 comparand = doSomthing;  
  12.                 doSomthing = Interlocked.CompareExchange<dosomthing>(ref this._someEvent, comparand + value, comparand);  
  13.             } while (doSomthing != comparand);  
  14.         }  
  15.         remove {  
  16.             DoSomthing doSomthing = this._someEvent;  
  17.             DoSomthing comparand;  
  18.             do{  
  19.                 comparand = doSomthing;  
  20.                 doSomthing = Interlocked.CompareExchange<dosomthing>(ref this._someEvent, comparand - value, comparand); }  
  21.             while (doSomthing != comparand);  
  22.         }  
  23.     }  
  24. }  
  25. </dosomthing></dosomthing>  


Где паттерн Interlocked Anything Pattern будет использован дважды: в методах add и remove.

Метод add, без изменений, можно преобразовать в следующий код:

  1. add  
  2. {   DoSomthing doSomthing = this._someEvent;  
  3.     DoSomthing comparand;  
  4.     do {  
  5.         comparand = doSomthing;  
  6.         DoSomthing result = (DoSomthing)Delegate.Combine(comparand, value);  
  7.         doSomthing = Interlocked.CompareExchange<dosomthing>(ref this._someEvent, result, comparand);  }  
  8.     while (doSomthing != comparand);  
  9. }  
  10. </dosomthing>  


Проще всего понять, как работает данный код в многопоточной среде – на диаграммах.

Предположим, в куче есть объект типа SomeType, поле _someEvent, в данный момент, уже указывает на некий экземпляр делегата DoSomthing, а так же присутствуют два потока, которые начинают одновременно добавлять делегаты в очередь события, через метод add. В стеке потока каждого из них создается указатель doSomthing типа DoSomthing указывающий на тот же экземпляр делегата что и поле _someEvent, далее создается еще один указатель comparand типа DoSomthing который указывает на null.



В данный момент каждый из потоков, как бы бросил «якорь» с помощью указателей doSomthing. Третий поток, теперь может изменять значение указателя в SomeType, и благодаря этому «якорю» изменения будут замечены и корректно обработаны.

Далее, так как третий поток уже мог изменить значение указаетля _someEvent, в цикле do, указателю comparand присваивается значение, на которое указывает «якорь»(doSomthing). Каждый из потоков с помощью метода Delegate.Combine создает свой собственный, результирующий экземпляр DoSomthig (вместо метода Delegate.Combine можно использовать любой другой код, хоть в тысячу строк, результатом которого должен стать новый результирующий объект). Теперь в игру вступает главный игрок – метод Interlocked.CompareExchange, при помощи которого происходит атомарная замена значения указателя _someEvent на результирующий объект, в метод передается три аргумента : указатель, по ссылке, значение которого будет изменено(ref this._someEvent), объект (result)на который будет указывать указатель(ref this._someEvent), после успешного выполнения операции, а так же объект(comparand) для сравнения. В случае если объект, на который указывает указатель(ref this._someEvent) равен объекту (comparand), то метод перепишет указатель (ref this._someEvent) на объект result. В случае если объекты не равны то замещения не произойдет.

Предположим второй поток выполняет метод Interlocked.CompareExchange раньше, и до этого объект SomeType не изменялся, с момента, когда был брошен «якорь». Указатель comparand указывает на тот же объект что и _someEvent, сравнивание объектов проходит успешно и происходит замещение, так же метод возвращает изначальное значение _someEvent и переписывает значение указателя doSomthing, после чего производит сравнивание объектов на которые указывают указатели doSomthing и comparand. Так как это один и тот же объект, сравнивание проходит успешно, значит метод Interlocked.CompareExchange удачно отработал и выполнение метода можно завершать.



Вернемся к первому потоку, теперь он начинает выполнять метод Interlocked.CompareExchange, в результате которого doSomthing будет перезаписана на тот объект, что содержался в this._someEvent, на начало выполнения метода Interlocked.CompareExchange, а это уже объект, созданный вторым потоком. При сравнении doSomthing с comparand - результат будет отрицательным, говорящий потоку о том, что другой поток внес изменения, с момента когда первый поток поставил «якорь», метод Interlocked.CompareExchange не выполнил замещения _someEvent на result, что приводит к повторения цикла do и повторной попытке внести изменения, с учетом изменений сделанных вторым потоком.


Александр Кобелев, aka Megano