Много читателей и один писатель

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

Ниже приведена моя реализация “читателя” и “писателя”. В этой реализации есть допущение о том, что нам известно максимальное количество читателей MAX_READERS.


LONG gCounter = 0;
//алгоритм читателя
for (;;) //бесконечный цикл ожидания освобождения ресурса
{
LONG n = InterlockedIncrement(&gCounter);
//в n - значение gCounter после инкремента
if (n <= MAX_READERS) break; //писатель ничего не пишет - можно читать
InterlockedDecrement(&gCounter);
}
// здесь читаем данные
...
//
InterlockedDecrement(&gCounter); //освобождаем блокировку читателем
// алгоритм писателя
for (;;) //бесконечный цикл освобождения ресурса читателями/писателями
{
LONG n = InterlockedCompareExchange(&gCounter, (MAX_READERS+1), 0);
//в n - предыдущее значение gCounter, которое было ДО попытки заменить его на MAX_READERS+1 в InterlockedCompareExchange;
//если там был 0, то никаких читателей/писателей не было, новое значение в gCounter будет MAX_READERS+1;
//если в gCounter был не 0, то это значение НЕ будет заменено на MAX_READERS+1, а останется прежним
if (n == 0) break;
}
// здесь пишем данные
...
//
InterlockedExchangeAdd(&gCounter, -(MAX_READERS+1)); //освобождаем блокировку писателем

Обращаю внимание на использование Interlocked-функций. Это такие функции, которые обеспечивают атомарность.
Например, InterlockedIncrement(&gCounter) - это атомарное увеличение на 1 (инкремент) значения gCounter.
Вообще, операция инкремента gCounter не атомарна, она состоит из следующих 3-х операций:

1. прочитать значение из памяти по адресу &gCounter
2. увеличить значение на 1
3. записать результат в память по адресу &gCounter

Если выполняется параллельно 2 потока, то возможна такая ситуация :

[поток 1] читает gCounter, прочитан 0
[поток 2] читает gCounter, прочитан 0
[поток 1] увеличивает значение на 1, получается 1
[поток 2] увеличивает значение на 1, получается 1
[поток 1] записывает 1 в gCounter
[поток 2] записывает 1 в gCounter

В итоге в gCounter будет 1, а не 2, как можно было бы предположить.

UPD. LockLib - классы C++ для блокировки разделяемых ресурсов: http://blog.coolsoftware.ru/2013/12/locklib.html

===
Перепечатка материалов блога разрешается с обязательной ссылкой на blog.coolsoftware.ru