Поскольку речь идет о Windows, то одним из основных источников есть MSDN, на который я постоянно буду давать ссылки на писание функций, ну и книга Джеффри Рихтера «Windows для профессионалов». В статье рассказаны основы основ синхронизации многопоточных приложений с коротким описаний функций WinAPI.
Способы синхронизация разделяются на две группы: синхронизация в пользовательском режиме и с использованием объектов ядра. Синхронизировать в пользовательском режиме можно двумя способами: использую семейство Interlocked-функций либо критические секции. Их преимуществом есть быстрота(один переходит из пользовательского режмимо в режим ядра занимает около 1000 тактов процессора), но именно из-за этого они способны решать очень ограниченный круг задач. Универсальным советом есть использовать синхронизацию объектами ядра только в том случае, если поставленную задачу нельзя решить с помощью Interlocked-функций или критических секций.
В статье пойдет разговор именно про Interlocked-функции, которые доступны разработчикам под ОС Windows.
Используются в случае, если разным потокам необходимо изменять одну и ту же переменную. Например, увеличивать счетчик числа операций.
Код вида var++; компилятором генерируется в что-то вроде
Соответственно, если будут два потока с таким кодом, то нельзя гарантировать, что выполняться строки 1,2,3 первого потока, а потом 1,2,3 второго, первая строчка второго потока может выполнится до того, как первый запишет новое значение в переменную, и тогда второй поток изменит старое значение и перетрет изменения первого. Использование Interlocked-функций гарантирует, что операция будет выполнена атомарно, и описанная выше проблема отпадает. При использование процессоров х86 они выдают по шине аппаратный сигнал, не давая другим процессорам процессорам обратится по тому же адресу памяти на время работы функции.
В примере с инкрементом желанного результат можно достичь двумя функциями – InterlockedExchangeAdd(&var, 1), которая возвратит начальное значение var, либо InterlockedIncrement(&var), которая возвращает уже инкрементированое значение переменной. Также существуют их 64-разрядные версии — InterlockedIncrement64 и InterlockedExchangeAdd64.
Существуют ещё функции, которые реализуют:
Interlocked-функции не переводят процесс в режим ожидания, работают в пользовательском режиме, и поэтому обладают довольно большим быстродействием, так что если необходима синхронизация для доступа к одной переменной, следует использовать именно их.
Способы синхронизация разделяются на две группы: синхронизация в пользовательском режиме и с использованием объектов ядра. Синхронизировать в пользовательском режиме можно двумя способами: использую семейство Interlocked-функций либо критические секции. Их преимуществом есть быстрота(один переходит из пользовательского режмимо в режим ядра занимает около 1000 тактов процессора), но именно из-за этого они способны решать очень ограниченный круг задач. Универсальным советом есть использовать синхронизацию объектами ядра только в том случае, если поставленную задачу нельзя решить с помощью Interlocked-функций или критических секций.
В статье пойдет разговор именно про Interlocked-функции, которые доступны разработчикам под ОС Windows.
Семейство Interlocked-функций
Используются в случае, если разным потокам необходимо изменять одну и ту же переменную. Например, увеличивать счетчик числа операций.
Код вида var++; компилятором генерируется в что-то вроде
MOV EAX, [var]; 1
INC EAX; 2
MOV [var], EAX; 3
Соответственно, если будут два потока с таким кодом, то нельзя гарантировать, что выполняться строки 1,2,3 первого потока, а потом 1,2,3 второго, первая строчка второго потока может выполнится до того, как первый запишет новое значение в переменную, и тогда второй поток изменит старое значение и перетрет изменения первого. Использование Interlocked-функций гарантирует, что операция будет выполнена атомарно, и описанная выше проблема отпадает. При использование процессоров х86 они выдают по шине аппаратный сигнал, не давая другим процессорам процессорам обратится по тому же адресу памяти на время работы функции.
В примере с инкрементом желанного результат можно достичь двумя функциями – InterlockedExchangeAdd(&var, 1), которая возвратит начальное значение var, либо InterlockedIncrement(&var), которая возвращает уже инкрементированое значение переменной. Также существуют их 64-разрядные версии — InterlockedIncrement64 и InterlockedExchangeAdd64.
Существуют ещё функции, которые реализуют:
- декремент: LONG InterlockedDecrement(LONG volatile *Addend), возвращает значение декрементированной переменной и 64-битная версия — InterlockedDecrement64
- монопольную замены текущего значения: LONG InterlockedExchange(LONG volatile *Target, LONG Value), которая всегда оперирует 32-разрядными значениямии возвращает начальное значение параметра Target, и PVOID InterlockedExchangePointer с такой же сигнатурой, которая в случае 64-разрядной программе будет оперировать с 64-разрядными значениями, возвращает указатель на Target
- логические операции: логическое «И» LONG InterlockedAnd(LONG volatile *Destination, LONG Value), логическое «ИЛИ» InterlockedOr и сумму по модулю два InterlockedXor. У них одинаковая сигнатура, все они результат выполнения записывают в первый параметр и возвращают исходное значение первого параметра
- выполнение сравнения и присвоения как атомарной операции: LONG InterlockedCompareExchange(LONG volatile *Destination, LONG Exchange, LONG Comparand) сравнивает значение первых двух параметров, и если они совпадают, записывает в первый параметр третий. Возвращает исходное значение первого параметра и работает всегдда з 32-разрядными значениями. PVOID InterlockedCompareExchangePointer(PVOID volatile *Destination, PVOID Exchange, PVOID Comparand) делает тоже самое, отличие в том, что в 64-разрядном приложение оперирует с 64-разрядными значениями. Также существует InterlockedCompare64Exchange128, которая тоже работает так же, сравнивает два 64-битных значения, а третий параметр у неё имеет разрядность 128 бит
Interlocked-функции не переводят процесс в режим ожидания, работают в пользовательском режиме, и поэтому обладают довольно большим быстродействием, так что если необходима синхронизация для доступа к одной переменной, следует использовать именно их.