Для синхронизации действий, выполняемых разными потоками существует несколько различных способов. Условно их можно разделить на следующие:
· синхронизация с помощью глобальных переменных
· синхронизация с помощью обмена событиями
· синхронизация с помощью специальных объектов (событий, семафоров, критических секций, объектов исключительного владения и других)
Первый метод синхронизации следует признать самым неудачным, так как он требует постоянного опроса состояния глобальной переменной. У этого метода есть и более серьезные недостатки — так, например, возможна полная блокировка потоков, если поток, ожидающий изменения глобальной переменной имеет более высокий приоритет, чем поток, изменяющий эту переменную. Правда, его можно несколько улучшить — вводя дополнительные временные задержки между последовательными опросами.
Второй метод потребует создания объектов, способных получать сообщения или извещения о выполнении некоторых действий. Это могут быть окна, файлы (например, при использовании асинхронных операций или операций с уведомлением), каталоги и т.д. Во многих случаях может быть удобно создать невидимое окно, участвующее в обмене сообщениями или ожидающее получения сообщений для выполнения тех или иных действий.
Третий метод — синхронизация с помощью специальных объектов — потребует рассмотрения разных объектов и разных методов синхронизации с использованием объектов. В Win32 API существует большое число объектов, которые могут быть применены в качестве синхронизирующих, причем во многих случаях вместо одного объекта может применяться объект другого типа. Поэтому будет интересно рассмотреть несколько основных методов синхронизации с использованием объектов.
При рассмотрении способов синхронизации удобно выделить несколько основных способов, применяемых разными операционными системами. В числе таких способов можно выделить четыре метода:
· критические секции
· объекты исключительного владения
· события
· синхронизация группой объектов
Последовательно рассмотрим эти основные методы, используя в качестве примера функции по работе с глобальной кучей. Попробуем абстрактно разобраться со свойствами этих функций, что бы понять, какие методы синхронизации и в каких случаях удобно применять.
Во–первых, глобальную кучу в целом можно рассматривать как некий сложный объект, над которым могут выполняться некоторые операции (пока мы рассматриваем только операции над кучей в целом, не вдаваясь в нюансы работы с отдельными блоками — при необходимости доступ к отдельным блокам кучи должен быть синхронизирован, но это уже не функции кучи, а проблема того, кто пользуется кучей).
Часть операций, например получение указателя на какой–либо блок в куче, не нуждается в изменении самой кучи. То есть функции, осуществляющие подобные операции могут выполняться одновременно разными потоками не конфликтуя друг с другом. Такие функции удобно назвать “читателями” — они не изменяют кучу как единый объект.
Другие операции, например выделение нового блока в куче, требуют внесения изменений в кучу — изменения в цепочке выделяемых блоков. Обычно такие изменения осуществляются не в одном месте, а требуется согласованное внесение изменений в нескольких структурах данных. В процессе такой операции структура кучи какое–то время оказывается нарушенной, вследствие чего одновременное выполнение таких действий должно быть исключено. Такие функции удобно назвать “писателями” — они изменяют кучу как единый объект.
Применительно к куче возможен одновременный доступ нескольких читателей и исключительный — писателей. Более того, если к куче получает доступ писатель, то читатели также должны быть лишены доступа (в других случаях это может быть не так — читатели могут работать одновременно с писателем).
Можно сформулировать несколько правил для работы с таким объектом:
· если к объекту имеет доступ писатель, то ни читатели, ни другие писатели доступа не имеют;
· если к объекту имеет доступ читатель, то возможен одновременный доступ других читателей и запрещен доступ писателям;
· если объект свободен, то первый пришедший писатель или читатель имеет право доступа.
Рассмотренный пример очень удобен, так как подобная ситуация (много читателей, единственный писатель) встречается сплошь и рядом — а стандартного объекта для синхронизации доступа к такому объекту нет. Благодаря этому такой пример становится благодатной почвой для рассмотрения разных способов синхронизации.
Номер | Поток, уже имеющий доступ | Поток, требующий доступа | Действие |
1 | читатель | читатель | разрешить доступ |
2 | читатель | писатель | ожидать освобождения[1] |
3 | писатель | читатель | ожидать освобождения[1] |
4 | писатель | писатель | ожидать освобождения |
5 | — | читатель | разрешить доступ |
6 | — | писатель | разрешить доступ |
Даже при простейшем анализе такой таблицы можно заметить, что писатели находятся в неравном положении — при достаточно большом числе читателей и писателей возможна полная блокировка последних, так как читатели могут получать право одновременного доступа к объекту, так что возможна ситуация, когда объект вообще не будет освобожден для писателей — хоть один читатель да найдется. Для выхода из такой ситуации следует слегка модифицировать реакцию объекта во 1м случае — если имеется ожидающий поток–писатель, то надо разрешить доступ ему, а не читателю. Для полной “симметрии” следует в 4м случае проверять наличие читателей и разрешать им доступ, даже если имеется ожидающий поток–писатель. Такой механизм, хотя и достаточно сложный в реализации, обеспечит более–менее равные права для выполнения потоков разного типа.
Этот синхронизирующий объект может использоваться только локально внутри процесса, создавшего его. Остальные объекты могут быть использованы для синхронизации потоков разных процессов. Название объекта “критическая секция” связано с некоторым абстрактным выделением части программного кода (секции), выполняющего некоторые операции, порядок которых не может быть нарушен. То есть попытка двумя разными потоками одновременно выполнять код этой секции приведет к ошибке.
Например, такой секцией может быть удобно защитить функции–писатели, так как одновременный доступ нескольких писателей должен быть исключен.
Для критической секции вводят две операции:
· войти в секцию;
Пока какой–либо поток находится в критической секции, все остальные потоки при попытке войти в нее будут автоматически останавливаться в ожидании. Поток, уже вошедший в эту секцию, может входить в нее многократно, не ожидая ее освобождения.
· покинуть секцию;
При покидании потоком секции уменьшается счетчик числа вхождений этого потока в секцию, так что секция будет освобождена для других потоков только если поток выйдет из секции столько раз, сколько раз в нее входил. При освобождении критической секции будет пробужден только один поток, ожидающий разрешения на вход в эту секцию.
Вообще говоря, в других API, отличных от Win32 (например, OS/2), критическая секция рассматривается не как синхронизирующий объект, а как фрагмент кода программы, который может исполняться только одним потоком приложения. То есть вход в критическую секцию рассматривается как временное выключение механизма переключения потоков до выхода из этой секции. В Win32 API критические секции рассматриваются как объекты, что приводит к определенной путанице — они очень близки по своим свойствам к неименованным объектам исключительного владения (mutex, см. ниже).
При использовании критических секций надо следить, что бы в секции не выделялись чересчур большие фрагменты кода, так как это может привести к существенным задержкам в выполнении других потоков.
Например, применительно к уже рассмотренным кучам — не имеет смысла все функции по работе с кучей защищать критической секцией, так как функции–читатели могут выполняться параллельно. Более того, применение критической секции даже для синхронизации писателей на самом деле представляется малоудобным — так как для синхронизации писателя с читателями последним все–равно придется входить в эту секцию, что практически приводит к защите всех функций единой секцией.
Можно выделить несколько случаев эффективного применения критических секций:
· читатели не конфликтуют с писателями (защищать надо только писателей);
· все потоки имеют примерно равные права доступа (скажем, нельзя выделить чистых писателей и читателей);
· при построении составных синхронизирующих объектов, состоящих из нескольких стандартных, для защиты последовательных операций над составным объектом.
Объекты исключительного владения — mutex (mutualexclusion) — могут принадлежать только одному потоку одновременно. Соответственно определяются операции над этими объектами:
· затребовать владение объектом;
При запросе владения система проверяет, владеет–ли какой–либо другой поток этим объектом или нет. Если имеется другой поток–владелец, то данный поток останавливается до тех пор, пока объект не освободиться. Как только объект становится свободным, система отдает его во владение новому потоку. Поток, уже владеющий объектом, может многократно вступать во владение им.
· освободить объект;
При освобождении объекта система просматривает, имеются–ли другие потоки, ожидающие освобождения этого объекта. Если имеются, то возобновит работу только один поток, а все остальные продолжат ожидание — объект mutex может быть во владении только у одного потока. Освобождать объект может только тот поток, который им в данный момент владеет, другие потоки этого сделать не могут. Для полного освобождения объекта поток должен столько раз освободить его, сколько раз он затребовал владение с того момента, как ему дали этот объект во владение.