среда, 9 апреля 2008 г.

InterlockedOr/And/ExchangeAdd в .Net

Программистам C++ через WinAPI доступно много различных Interlocked функций на все случаи жизни. Выбор этих функций в .Net невелик: Increment, Decrement, Add, Exchange, CompareExchange и почти бесполезный Read. В большей части случаев этих методов вполне достаточно, да и более 80% программистов вообще не знают об атомарных функциях и не используют их. Не так давно я столкнулся с задачей, в которой нужны были атомарные бинарные операции InterlockedOr и InterlockedAnd. О самой этой задаче я расскажу в другой статье, а в этой рассмотрим, как можно получить в C# доступ к этим функциям, а также к экзотической функции InterlockedExchangeAdd


Логично предположить, что нужные нам функции скрываются в модуле kernel32.dll, как и другие Interlocked*. Как культурные люди мы не лезем в этот файл через Far/Total Commander (хотя и делаем так зачастую :)), а изучим его через Dependency Walker из комплекта Visual Studio (Common7\Tools\Bin\Depends.Exe).

Результат выглядит приблизительно так:



Возможно, на других системах вид будет немного другой, но на моей Windows 2008 RC 2 в списке наблюдаются только основные Interlocked функции. Забегу немного вперед и скажу, что в kernel32.dll для платформы x64 они вообще отсутствуют. Если же мы посмотрим в kernel32.lib от Microsoft Platform SDK (в этот раз обычным текстовым или бинарным редактором), то увидим, что InterlockedOr нет и там.

Ответ можно найти в файле WinBase.h:

#if !defined (InterlockedOr)

#define InterlockedOr InterlockedOr_Inline

LONG
FORCEINLINE
InterlockedOr_Inline (
__inout LONG volatile *Target,
__in LONG Set
)
{
LONG i;
LONG j;

j = *Target;
do {
i = j;
j = InterlockedCompareExchange(Target,
i | Set,
i);

} while (i != j);

return j;
}

#endif


Как видим, битовые атомарные операции реализованы в виде цикла с InterlockedCompareExchange и ничего не мешает нам сделать то же самое в C#. Вот пример реализации InterlockedOr с сохранением оригинального синтаксиса:


public static int InterlockedOr(ref int Target, int Set)
{
int i;
int j = Target;
do
{
i = j;
j = Interlocked.CompareExchange(ref Target, i | Set, i);
}
while (i != j);

return j;
}


Точно так же реализуется и InterlockedAnd с заменой i | Set на i & Set. Как именно работает этот код я тут объяснять не буду, отослав вопрошающих к первоисточникам, литературе, или к дзен-поиску (http://dzen.yandex.ru/).

А мы же вернемся к экзотической функции InterlockedExchangeAdd. Эта функция добавляет указанное значение к переменной и возвращает ее старое значение. Отличие от InterlockedAdd минимально, а если взглянуть на .Net Framework через призму декомпилятора, то мы увидим, что сам Interlocked.Add реализован именно через ExchangeAdd добавлением слагаемого:

public static int Add(ref int location1, int value)
{
return (ExchangeAdd(ref location1, value) + value);
}


Это утверждение верно и в обратную сторону: мы можем вычесть из результата Interlocked.Add слагаемое и получить такой же результат, как при вызове InterlockedExchangeAdd. Этот метод универсален и для повседневного использования я рекомендую именно его (например, при портировании кода с С++ с сохранением синтаксиса и имен используемых методов).
Тут бы и время закончить статью, но в ходе подготовки материалов для второй части статьи "Многопоточность и свойства" (первая часть: http://blog.runserver.net/2008/03/blog-post_28.html) я столкнулся с задачей, в которой нужно атомарной функцией добавить некое значение к переменной, передающейся по указателю int * (или же IntPtr), а не по ссылке ref int. Нормального метода преобразования указателя в ссылку в C# не существует, потому пришлось обратиться к P/Invoke. Сайт http://pinvoke.net предлагает нам следующее описание:

[DllImport("kernel32.dll")]
static extern int
InterlockedExchangeAdd(ref int Addend, int Value);


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

[DllImport("kernel32.dll")]
static extern int
InterlockedExchangeAdd(int* Addend, int Value);


Метод пригоден к использованию, все с ним хорошо, кроме одной "мелочи": в x64 режиме мы получаем System.EntryPointNotFoundException, сообщающий, что точка входа с таким именем не найдена в kernel32.dll. Ранее я уже упоминал, что Interlocked методы в x64 версии kernel32.dll попросту отсутствуют. Снова обратившись к WinBase.h мы видим, что в этот раз методы не прячутся под inline реализации, а переадресованы на intrinsic функции (http://msdn2.microsoft.com/en-us/library/191ca0sk.aspx). Где именно находится их реализация, я не нашел. Это ни .lib файлы от Platform SDK, ни системные библиотеки. Будем считать, что их реализация вшита в компилятор, или куда-либо еще. Единственный вариант, который я нашел, это создание собственной DLL, которая экспортирует функцию-враппер:

extern "C" __declspec(dllexport) LONG
interlockedExchangeAdd(LONG volatile * target, LONG value)
{
return InterlockedExchangeAdd(target, value);
}


Сложность еще заключается в том, что необходимо будет держать две разные библиотеки: для x86 и x64 режима. Я обошел это ограничение таким образом:

[DllImport("kernel32.dll",
EntryPoint = "InterlockedExchangeAdd")]
private static extern int
NativeInterlockedExchangeAdd(int * target, int value);

[DllImport("InterlockedWrapper_x64.dll",
EntryPoint = "interlockedExchangeAdd")]
private static extern int
CustomInterlockedExchangeAdd(int * target, int value);

public static int
InterlockedExchangeAdd(int* target, int value)
{
if (IntPtr.Size == 4) // x86
return NativeInterlockedExchangeAdd(target, value);
else
return CustomInterlockedExchangeAdd(target, value);
}


Таким образом, в x86 режиме используется kernel32.dll, а в x64 - наша собственная библиотека-враппер.

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