Поскольку имеется четыре тесно связанных между собой переменных, вызовы для выполнения их считывания и обновления следует поместить внутрь критического раздела или флага синхронизации. Мы будем использовать критический раздел, поскольку эти компоненты эффективнее. Итак, это будет первым объектом синхронизации. Первым шагом выполнения каждого из четырех описанных методов будет запрос критического раздела, последним - его освобождение. Однако вспомните, что методы, которые позволяют запустить поток считывания, могут блокироваться внутри подпрограммы. Если бы этот программный блок оказался между процедурами вызова и освобождения управляющего критического раздела, возникла бы тупиковая ситуация. Поэтому необходимо обеспечить, чтобы блокировка выполнялась снаружи, после того, как критический раздел освобожден.
Поскольку одновременно только один поток записи может быть активным, может показаться целесообразным поместить объект синхронизации, который ставит потоки записи в очередь, также в критический раздел, поскольку этот раздел может принадлежать только одному потоку. Однако на практике проще воспользоваться семафором. Причина этого проста: в действительности не требуется вызов объекта синхронизации, поскольку не существует подходящего места для его освобождения. Действительно, вы убедитесь, что придется дожидаться семафора в одном потоке и освобождать его в другом. Такой подход невозможен при использовании критического раздела: поток, обращающийся к критическому разделу, владеет им.
А каким должен быть объект синхронизации для потоков считывания? Больше всего подошли бы семафор или событие сброса вручную. Как и в предыдущем случае, лучше использовать семафор, поскольку применение объекта события привело бы возникновению проблем (при получении сигнала будут освобождаться только ожидающие его прихода потоки;
в данной реализации поток может находиться в состоянии, в котором он еще не вызвал подпрограмму WaitFor).
Код интерфейса создаваемого нами класса синхронизации TtdReadWriteSync приведен в листинге 12.1. Он содержит ряд приватных полей, которые будут использоваться в четырех основных методах.
Листинг 12.1. Интерфейс класса TtdReadWriteSync
type
TtdReadWriteSync = class private
FActiveReaders : integer;
FActiveWriter : boolean;
FBlockedReaders : THandle;
{семафор}
FBlockedWriters : THandle;
{семафор}
FController : TRTLCriticalSection;
FWaitingReaders : integer;
FWaitingWriters : integer;
protected
public
constructor Create;
destructor Destroy; override;
procedure StartReading;
procedure StartWriting;
procedure StopReading;
procedure StopWriting;
end;
Приватное поле FBlockedReaders семафора предназначено для ожидающих потоков считывания, а поле FBlockedWriters - для ожидающих потоков записи. Поле FController - основной компонент, обеспечивающий последовательный доступ к объектам (к сожалению, применение подобного механизма последовательной обработки необходимо для обеспечения того, чтобы каждый поток получал целостное и неискаженное изображение всего класса).
Код метода StartReading приведен в листинге 12.2.
Листинг 12.2. Метод StartReading
procedure TtdReadWriteSync.StartReading;
var
HaveToWait : boolean;
begin
{перехватить управление критическим разделом}
EnterCriticalSection(FController);
{если существует выполняющийся поток записи или хотя бы один ожидающий своей очереди поток записи, метод добавляет себя в качестве ожидающего метода записи, обеспечивая переход в состояние ожидания}
if FActiveWriter or (FWaitingWriters <> 0) then begin
inc(FWaitingReaders);
HaveToWait :=true;
end
{в противном случае он добавляет себя в качестве еще одного выполняющегося потока считывания и обеспечивает отсутствие состояния ожидания}
else begin
inc(FActiveReaders);
HaveToWait := false;
end;
{освободить управление критическим разделом}
LeaveCriticalSection(FController);
{при необходимости ожидания нужно выполнить следующее}
if HaveToWait then
WaitForSingleObject(FBlockedReaders, INFINITE);
end;
Прежде всего, мы перехватываем управление критическим разделом. После этого можно осуществлять управление значениями внутренних полей. При наличии выполняющегося в текущий момент или хотя бы одного ожидающего потока записи метод увеличивает число ожидающих потоков считывания, освобождает управление критическим разделом, а затем переходит в состояние ожидания семафора "заблокированные потоки считывания". При отсутствии ожидающих или выполняющихся потоков записи метод увеличивает число выполняющихся потоков считывания и освобождает критический раздел. По выходу из этого метода программа либо освобождается от необходимости ожидать прихода семафора, либо сразу пропускает состояние ожидания. Обратите внимание, что во втором случае метод увеличил число выполняющихся потоков считывания, а в первом нет. Это может показаться программной ошибкой, но вскоре мы покажем, как можно решить возникающую при этом проблему.
Читать дальше