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

The Interlocked Anything Pattern

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

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


public sealed class SomeType
{
public event DoSomthing SomeEvent;
}
public delegate void DoSomthing();


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


public sealed class SomeType
{
private DoSomthing _someEvent;

public event DoSomthing SomeEvent
{
add {
DoSomthing doSomthing = this._someEvent;
DoSomthing comparand;
do {
comparand = doSomthing;
doSomthing = Interlocked.CompareExchange(ref this._someEvent, comparand + value, comparand);
} while (doSomthing != comparand);
}
remove {
DoSomthing doSomthing = this._someEvent;
DoSomthing comparand;
do{
comparand = doSomthing;
doSomthing = Interlocked.CompareExchange(ref this._someEvent, comparand - value, comparand); }
while (doSomthing != comparand);
}
}
}


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

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


add
{ DoSomthing doSomthing = this._someEvent;
DoSomthing comparand;
do {
comparand = doSomthing;
DoSomthing result = (DoSomthing)Delegate.Combine(comparand, value);
doSomthing = Interlocked.CompareExchange(ref this._someEvent, result, comparand); }
while (doSomthing != comparand);
}


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

Предположим, в куче есть объект типа 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