Синхронизация в сценарии "один писатель/группа читателей"
Во многих приложениях возникает одна и та же проблемя синхронизации, о которой часто говорят как о сценарии "один писатель/группа читателей" (single-wrirer/ multiple-readers). В чем ее суть? Представьте: произвольное число потоков пытается получить доступ к некоему разделяемому ресурсу. Каким-то потокам ("писателям") нужно модифицировать данные, а каким-то ("читателям") — лишь прочесть эти данные. Синхронизация такого процесса необходима хотя бы потому, что Вы должны соблюдать следующие правила:
Посмотрим на проблему в контексте базы данных. Допустим, с ней работают пять конечных пользователей: двое вводят в нее записи, трое — считывают.
В этом сценарии правило 1 необходимо потому, что мы, конечно же, не можем позволить одновременно обновлять одну и ту же запись. Иначе информация в записи будет повреждена.
Правило 2 запрещает доступ к записи, обновляемой в данный момент другим пользователем. Будь то иначе, один пользователь считывал бы запись, когда другой пользователь изменял бы ее содержимое. Что увидел бы на мониторе своего компьютера первый пользователь, предсказать не берусь. Правило 3 служит тем же целям, что и правило 2. И действительно, какая разница, кто первый получит доступ к данным: тот, кто записывает, или тот, кто считывает, — все равно одновременно этого делать нельзя.
И, наконец, последнее правило. Оно введено для большей эффективности работы баз данных. Если никто не модифицирует записи в базе данных, все пользователи могут свободно читать любые записи. Также предполагается, что количество "читателей" превышает число "писателей".
О'кэй, суть проблемы Вы ухватили.
А теперь вопрос: как ее решить?
NOTE:
Я представлю здесь совершенно новый код. Решения этой проблемы, которые я публиковал в прежних изданиях, часто критиковались по двум причинам. Во первых, предыдущие реализации работали слишком медленно, так как я писал их в расчете на самые разные сценарии. Например, я шире использовал объекты ядра, стремясь синхронизировать доступ к базе данных потоков из разных процессов. Конечно, эти реализации работали и в сценарии для одного процесса, но интенсивное использование объектов ядра приводило в этом случае к существенным издержкам. Похоже, сценарий для одного процесса более распространен, чем я думал.
Во-вторых, в моей реализации был потенциальный риск блокировки noтоков-"писателей". Из правил, о которых я рассказал в начале этого раздела, вытекает, что потоки-"писатели" — при обращении к базе данных очень большого количества потоков-"читателей" — могут вообще не получить доступ к этому ресурсу.
Все эти недостатки я теперь устранил. В новой реализации объекты ядра применяются лишь в тсх случаях, когда без них не обойтись, и потоки синхронизируются в основном за счет использования критической секции.
Плоды своих трудов я инкапсулировал в С++-класс CSWMRG (я произношу его название как swimerge); это аббревиатура от "single writer/multiple reader guard". Он содержится в фцйлах SWMRG.h и SWMRG.cpp (см. листинг на рис. 10-3).
Использовать CSWMRG проще простого. Вы создаете объект С++-класса CSWMRG и вызываете нужные в Вашей программе функции-члены. В этом классе всего три метода (не считая конструктора и деструктора):
VOID CSWMRG:;WaitToRead(); // доступ к разделяемому ресурсу для чтения
VOID CSWMRG::WaitToWrite(); // монопольный доступ к разделяемому ресурсу для записи
VOID CSWMRG::Done(); // вызывается по окончании работы с ресурсом
Первый метод (WaitToRead) вызывается перед выполнением кода, что-либо считывающего из разделяемого ресурса, а второй (WaitToWrite) - перед выполнением кода, который считывает и записывает данные в разделяемом ресурсе.
К последнему методу (Done) программа обращается, закончив работу с этим ресурсом. Куда уж проще, а?
Объект CSWMRG содержит набор переменных-членов (см. таблицу ниже), отражающих то, как потоки работают с разделяемым ресурсом на данный момент. Остальные подробности Вы узнаете из исходного кода.
Переменная | Описание |
m_cs | Охраняет доступ к остальным членам класса, обеспечивая опера ции с ними на атомарном уровне |
т_nActive | Отражает текущее состояние разделяемого ресурса. Если она равна 0, ни один поток к ресурсу не обращается. Ее значение, большее 0, со общает текущее число потоков, считывающих данные из ресурса. Отрицательное значение (-1) свидетельствует о том, что какой-то поток записывает данные в ресурс. |
m_nWaitingReaders | Сообщает количество потоков "читателей", которым нужен доступ к ресурсу. Значение этой переменной инициализируется 0 и увели чивается на 1 всякий раз, когда поток вызывает WaitToRead в то вре мя, как т nActive равна — 1. |
т_nWaitingWriters | Сообщает количество потоков-"писателей", которым нужен доступ к ресурсу. Значение этой переменной инициализируется 0 и увели чивается на 1 всякий раз, когда поток вызывает WaitToWrite в то вре мя, как т nActive больше 0, |
т_hsemWriters | Когда потоки-"писатели"> вызывают WaitToWrtie, но получают отказ в доступе, так как m_nActive больше 0, они переходят в состояние ожидания этого семафора. Пока ждет хотя бы один поток-"писатель", новые потоки-"читатели" получают отказ в доступе к ресурсу. Тем самым я не даю потокам-"читателям" монополизировать доступ к этому ресурсу. Когда последний поток-"читатель", работавший с ресурсом, вызывает Done, семафор освобождается со счетчиком, равным 1, и система пробуждает один ждущий поток-"писатель". |
m_hsemReaders | Когда потоки-"читатели" вызывают WaitToRead, но получают отказ в доступе, так как m_nActive равна - 1, они переходят в состояние ожидания этого семафора. Когда последний из ждущих потоков-"писателей" вызывает Done, семафор освобождается со счетчиком, равным m_nWaitingReaders, и система пробуждает все ждущие потоки-"читатели". |