Аплет Rectangles
Исходные тексты
Описание текстов
Синхронизация потоков
Многопоточный режим работы открывает новые возможности для программистов, однако за эти возможности приходится расплачиваться усложнением процесса проектирования приложения и отладки. Основная трудность, с которой сталкиваются программисты, никогда не создававшие ранее многопоточные приложения, это синхронизация одновременно работающих потоков.
Для чего и когда она нужна?
Однопоточная программа, такая, например, как программа MS-DOS, при запуске получает в монопольное распоряжение все ресурсы компьютера. Так как в однопоточной системе существует только один процесс, он использует эти ресурсы в той последовательности, которая соответствует логике работы программы. Процессы и потоки, работающие одновременно в многопоточной системе, могут пытаться обращаться одновременно к одним и тем же ресурсам, что может привести к неправильной работе приложений.
Поясним это на простом примере.
Пусть мы создаем программу, выполняющую операции с банковским счетом. Операция снятия некоторой суммы денег со счета может происходить в следующей последовательности:
- на первом шаге проверяется общая сумма денег, которая хранится на счету;
- если общая сумма равна или превышает размер снимаемой суммы денег, общая сумма уменьшается на необходимую величину;
- значение остатка записывается на текущий счет.
Если операция уменьшения текущего счета выполняется в однопоточной системе, то никаких проблем не возникнет. Однако представим себе, что два процесса пытаются одновременно выполнить только что описанную операцию с одним и тем же счетом. Пусть при этом на счету находится 5 млн. долларов, а оба процесса пытаются снять с него по 3 млн. долларов.
Допустим, события разворачиваются следующим образом:
- первый процесс проверяет состояние текущего счета и убеждается, что на нем хранится 5 млн. долларов;
- второй процесс проверяет состояние текущего счета и также убеждается, что на нем хранится 5 млн. долларов;
- первый процесс уменьшает счет на 3 млн. долларов и записывает остаток (2 млн. долларов) на текущий счет;
- второй процесс выполняет ту же самую операцию, так как после проверки считает, что на счету по-прежнему хранится 5 млн. долларов.
В результате получилось, что со счета, на котором находилось 5 млн. долларов, было снято 6 млн. долларов, и при этом там осталось еще 2 млн. долларов! Итого - банку нанесен ущерб в 3 млн. долларов.
Как же составить программу уменьшения счета, чтобы она не позволяла вытворять подобное?
Очень просто - на время выполнения операций над счетом одним процессом необходимо запретить доступ к этому счету со стороны других процессов. В этом случае сценарий работы программы должен быть следующим:
- процесс блокирует счет для выполнения операций другими процессами, получая его в монопольное владение;
- процесс проводит процедуру уменьшения счета и записывает на текущий счет новое значение остатка;
- процесс разблокирует счет, разрешая другим процессам выполнение операций.
Когда первый процесс блокирует счет, он становится недоступен другим процессам. Если второй процесс также попытается заблокировать этот же счет, он будет переведен в состояние ожидания. Когда первый процесс уменьшит счет и на нем останется 2 млн. долларов, второй процесс будет разблокирован. Он проверит остаток, убедится, что сумма недостаточна и не будет проводить операцию.
Таким образом, в многопоточной среде необходима синхронизация потоков при обращении к критическим ресурсам. Если над такими ресурсами будут выполняться операции в неправильной последовательности, это приведет к возникновению трудно обнаруживаемых ошибок.
В языке программирования Java предусмотрено несколько средств для синхронизации потоков, которые мы сейчас рассмотрим.
Синхронизация методов
Возможность синхронизации как бы встроена в каждый объект, создаваемый приложением Java. Для этого объекты снабжаются защелками, которые могут быть использованы для блокировки потоков, обращающихся к этим объектам.
Чтобы воспользоваться защелками, вы можете объявить соответствующий метод как synchronized, сделав его синхронизированным:
public synchronized void decrement() { . . . }При вызове синхронизированного метода соответствующий ему объект (в котором он определен) блокируется для использования другими синхронизированными методами. В результате предотвращается одновременная запись двумя методами значений в область памяти, принадлежащую данному объекту.
Использование синхронизированных методов - достаточно простой способ синхронизации потоков, обращающихся к общим критическим ресурсам, наподобие описанного выше банковского счета.
Заметим, что не обязательно синхронизовать весь метод - можно выполнить синхронизацию только критичного фрагмента кода.
. . . synchronized(Account) { if(Account.check(3000000)) Account.decrement(3000000); } . . .Здесь синхронизация выполняется для объекта Account.
Блокировка потока
Синхронизированный поток, определенный как метод типа synchronized, может переходить в заблокированное состояние автоматически при попытке обращения к ресурсу, занятому другим синхронизированным методом, либо при выполнении некоторых операций ввода или вывода. Однако в ряде случаев полезно иметь более тонкие средства синхронизации, допускающие явное использование по запросу приложения.
Блокировка на заданный период времени
С помощью метода sleep можно заблокировать поток на заданный период времени:
try { Thread.sleep(500); } catch (InterruptedException ee) { . . . }В данном примере работа потока Thread приостанавливается на 500 миллисекунд. Заметим, что во время ожидания приостановленный поток не отнимает ресурсы процессора.
Так как метод sleep может создавать исключение InterruptedException, необходимо предусмотреть его обработку. Для этого мы использовали операторы try и catch.
Временная приостановка и возобновление работы
Методы suspend и resume позволяют, соответственно, временно приостанавливать и возобновлять работу потока.
В следующем фрагменте кода поток m_Rectangles приостанавливает свою работу, когда курсор мыши оказывается над окном аплета:
public boolean mouseEnter(Event evt, int x, int y) { if (m_Rectangles != null) { m_Rectangles.suspend(); } return true; }Работа потока возобновляется, когда курсор мыши покидает окно аплета:
public boolean mouseExit(Event evt, int x, int y) { if (m_Rectangles != null) { m_Rectangles.resume(); } return true; }Ожидание извещения
Если вам нужно организовать взаимодействие потоков таким образом, чтобы один поток управлял работой другого или других потоков, вы можете воспользоваться методами wait, notify и notifyAll, определенными в классе Object.
Метод wait может использоваться либо с параметром, либо без параметра. Этот метод переводит поток в состояние ожидания, в котором он будет находиться до тех пор, пока для потока не будет вызван извещающий метод notify, notifyAll, или пока не истечет период времени, указанный в параметре метода wait.
Как пользоваться методами wait, notify и notifyAll?
Метод, который будет переводиться в состояние ожидания, должен быть синхронизированным, то есть его следует описать как synchronized:
public synchronized void run() { while (true) { . . . try { this.wait(); } catch (InterruptedException e) { } } }В этом примере внутри метода run определен цикл, вызывающий метод wait без параметров. Каждый раз при очередном проходе цикла метод run переводится в состояние ожидания до тех пор, пока другой поток не выполнит извещение с помощью метода notify.
Ниже мы привели пример потока, вызывающией метод notify:
public void run() { while (true) { try { Thread.sleep(30); } catch (InterruptedException e) { } synchronized(STask) { STask.notify(); } } }Этот поток реализован в рамках отдельного класса, конструктору которого передается ссылка на поток, вызывающую метод wait. Эта ссылка хранится в поле STask.
Обратите внимание, что хотя сам метод run не синхронизированный, вызов метода notify выполняется в синхронизированном режиме. В качестве объекта синхронизации выступает поток, для которого вызывается метод notify.
Ожидание завершения потока
С помощью метода join вы можете выполнять ожидание завершения работы потока, для которой этот метод вызван.
Существует три определения метода join:
public final void join(); public final void join(long millis); public final void join(long millis, int nanos);Первый из них выполняет ожидание без ограничения во времени, для второго ожидание будет прервано принудительно через millis миллисекунд, а для третьего - через millis миллисекунд и nanos наносекунд. Учтите, что реально вы не сможете указывать время с точностью до наносекунд, так как дискретность системного таймера компьютера намного больше.
Потоки могут находиться в одном из нескольких состояний:
Ready (готов) – находящийся в пуле (pool) потоков, ожидающих выполнения;
Running (выполнение) - выполняющийся на процессоре;
Waiting (ожидание), также называется idle или suspended, приостановленный - в состоянии ожидания, которое завершается тем, что поток начинает выполняться (состояние Running) или переходит в состояние Ready ;
Terminated (завершение) - завершено выполнение всех команд потока. Впоследствии его можно удалить. Если поток не удален, система может вновь установить его в исходное состояние для последующего использования.
Синхронизация потоков
Выполняющимся потокам часто необходимо каким-то образом взаимодействовать. Например, если несколько потоков пытаются получить доступ к некоторым глобальным данным, то каждому потоку нужно предохранять данные от изменения другим потоком. Иногда одному потоку нужно получить информацию о том, когда другой поток завершит выполнение задачи. Такое взаимодействие обязательно между потоками как одного, так и разных процессов.
Синхронизация потоков (thread synchronization ) - это обобщенный термин, относящийся к процессу взаимодействия и взаимосвязи потоков. Учтите, что синхронизация потоков требует привлечения в качестве посредника самой операционной системы. Потоки не могут взаимодействовать друг с другом без ее участия.
В Win32 существует несколько методов синхронизации потоков. Бывает, что в конкретной ситуаций один метод более предпочтителен, чем другой. Давайте вкратце ознакомимся с этими методами.
Критические секции
Один из методов синхронизации потоков состоит в использовании критических секций (critical sections). Это единственный метод синхронизации потоков, который не требует привлечения ядра Windows. (Критическая секция не является объектом ядра). Однако этот метод может использоваться только для синхронизации потоков одного процесса.
Критическая секция - это некоторый участок кода, который в каждый момент времени может выполняться только одним из потоков. Если код, используемый для инициализации массива, поместить в критическую секцию, то другие потоки не смогут войти в этот участок кода до тех пор, пока первый поток не завершит его выполнение.
До использования критической секции необходимо инициализировать ее с помощью процедуры Win32 API InitializeCriticalSection(), которая определяется (в Delphi) следующим образом:
procedure InitializeCriticalSection(var IpCriticalSection: TRTLCriticalSection); stdcall;
Параметр IpCriticalSection представляет собой запись типа TRTLCriticalSection, которая передается по ссылке. Точное определение записи TRTLCriticalSection не имеет большого значения, поскольку вам вряд ли понадобится когда-либо заглядывать в ее содержимое. От вас требуется лишь передать неинициализированную запись в параметр IpCtitical Section, и эта запись будет тут же заполнена процедурой.
После заполнения записи в программе можно создать критическую секцию, поместив некоторый участок ее текста между вызовами функций EnterCriticalSection() и LeaveCriticalSection(). Эти процедуры определяются следующим образом:
procedure EnterCriticalSection(var IpCriticalSection: TRTLCriticalSection); stdcall;
procedure LeaveCriticalSection(var IpCriticalSection: TRTLCriticalSection); stdcall;
Параметр IpCriticalSection, который передается этим процедурам, является не чем иным, как записью, созданной процедурой InitializeCriticalSection().
Функция EnterCriticalSection проверяет, не выполняет ли уже какой-нибудь другой поток критическую секцию своей программы, связанную с данным объектом критической секции. Если нет, поток получает разрешение на выполнение своего критического кода, точнее, ему не запрещают это делать. Если да, то поток, обратившийся с запросом, переводится в состояние ожидания, а о запросе делается запись. Так как нужно создавать записи, объект «критическая секция» представляет собой структуру данных.
Когда функция LeaveCriticalSection вызывается потоком, который владеет в текущий момент разрешением на выполнение своей критической секции кода, связанной с данным объектом «критическая секция», система может проверить, нет ли в очереди другого потока, ожидающего освобождения этого объекта. Затем система может вынести ждущий поток из состояния ожидания, и он продолжит свою работу (в выделенные ему кванты времени).
По окончании работы с записью TRTLCriticalSection необходимо освободить ее, вызвав процедуру DeleteCriticalSection(), которая определяется следующим образом:
procedure DeleteCriticalSection(var IpCriticalSection: TRTLCriticalSection); stdcall;
Процессом (process) называется экземпляр программы, загруженной в память. Этот экземпляр может создавать нити (thread), которые представляют собой последовательность инструкций на выполнение. Важно понимать, что выполняются не процессы, а именно нити.
Причем любой процесс имеет хотя бы одну нить. Эта нить называется главной (основной) нитью приложения.
Так как практически всегда нитей гораздо больше, чем физических процессоров для их выполнения, то нити на самом деле выполняются не одновременно, а по очереди (распределение процессорного времени происходит именно между нитями). Но переключение между ними происходит так часто, что кажется, будто они выполняются параллельно.
В зависимости от ситуации нити могут находиться в трех состояниях. Во-первых, нить может выполняться, когда ей выделено процессорное время, т.е. она может находиться в состоянии активности. Во-вторых, она может быть неактивной и ожидать выделения процессора, т.е. быть в состоянии готовности. И есть еще третье, тоже очень важное состояние - состояние блокировки. Когда нить заблокирована, ей вообще не выделяется время. Обычно блокировка ставится на время ожидания какого-либо события. При возникновении этого события нить автоматически переводится из состояния блокировки в состояние готовности. Например, если одна нить выполняет вычисления, а другая должна ждать результатов, чтобы сохранить их на диск. Вторая могла бы использовать цикл типа "while(!isCalcFinished) continue;", но легко убедиться на практике, что во время выполнения этого цикла процессор занят на 100 % (это называется активным ожиданием). Таких вот циклов следует по возможности избегать, в чем оказывает неоценимую помощь механизм блокировки. Вторая нить может заблокировать себя до тех пор, пока первая не установит событие, сигнализирующее о том, что чтение окончено.
Синхронизация нитей в ОС Windows
В Windows реализована вытесняющая многозадачность - это значит, что в любой момент система может прервать выполнение одной нити и передать управление другой. Ранее, в Windows 3.1, использовался способ организации, называемый кооперативной многозадачностью: система ждала, пока нить сама не передаст ей управление и именно поэтому в случае зависания одного приложения приходилось перезагружать компьютер.
Все нити, принадлежащие одному процессу, разделяют некоторые общие ресурсы - такие, как адресное пространство оперативной памяти или открытые файлы. Эти ресурсы принадлежат всему процессу, а значит, и каждой его нити. Следовательно, каждая нить может работать с этими ресурсами без каких-либо ограничений. Но... Если одна нить еще не закончила работать с каким-либо общим ресурсом, а система переключилась на другую нить, использующую этот же ресурс, то результат работы этих нитей может чрезвычайно сильно отличаться от задуманного. Такие конфликты могут возникнуть и между нитями, принадлежащими различным процессам. Всегда, когда две или более нитей используют какой-либо общий ресурс, возникает эта проблема.
Пример. Несинхронизированная работа нитей: если временно приостановить выполнение нити вывода на экран (пауза), фоновая нить заполнения массива будет продолжать работать.
#include
Именно поэтому необходим механизм, позволяющий потокам согласовывать свою работу с общими ресурсами. Этот механизм получил название механизма синхронизации нитей (thread synchronization).
Этот механизм представляет собой набор объектов операционной системы, которые создаются и управляются программно, являются общими для всех нитей в системе (некоторые - для нитей, принадлежащих одному процессу) и используются для координирования доступа к ресурсам. В качестве ресурсов может выступать все, что может быть общим для двух и более нитей - файл на диске, порт, запись в базе данных, объект GDI, и даже глобальная переменная программы (которая может быть доступна из нитей, принадлежащих одному процессу).
Объектов синхронизации существует несколько, самые важные из них - это взаимоисключение (mutex), критическая секция (critical section), событие (event) и семафор (semaphore). Каждый из этих объектов реализует свой способ синхронизации. Также в качестве объектов синхронизации могут использоваться сами процессы и нити (когда одна нить ждет завершения другой нити или процесса); а также файлы, коммуникационные устройства, консольный ввод и уведомления об изменении.
Любой объект синхронизации может находиться в так называемом сигнальном состоянии. Для каждого типа объектов это состояние имеет различный смысл. Нити могут проверять текущее состояние объекта и/или ждать изменения этого состояния и таким образом согласовывать свои действия. При этом гарантируется, что когда нить работает с объектами синхронизации (создает их, изменяет состояние) система не прервет ее выполнения, пока она не завершит это действие. Таким образом, все конечные операции с объектами синхронизации являются атомарными (неделимыми.
Работа с объектами синхронизации
Чтобы создать тот или иной объект синхронизации, производится вызов специальной функции WinAPI типа Create... (напр. CreateMutex). Этот вызов возвращает дескриптор объекта (HANDLE), который может использоваться всеми нитями, принадлежащими данному процессу. Есть возможность получить доступ к объекту синхронизации из другого процесса - либо унаследовав дескриптор этого объекта, либо, что предпочтительнее, воспользовавшись вызовом функции открытия объекта (Open...). После этого вызова процесс получит дескриптор, который в дальнейшем можно использовать для работы с объектом. Объекту, если только он не предназначен для использования внутри одного процесса, обязательно присваивается имя. Имена всех объектов должны быть различны (даже если они разного типа). Нельзя, например, создать событие и семафор с одним и тем же именем.
По имеющемуся дескриптору объекта можно определить его текущее состояние. Это делается с помощью т.н. ожидающих функций. Чаще всего используется функция WaitForSingleObject. Эта функция принимает два параметра, первый из которых - дескриптор объекта, второй - время ожидания в мсек. Функция возвращает WAIT_OBJECT_0, если объект находится в сигнальном состоянии, WAIT_TIMEOUT - если истекло время ожидания, и WAIT_ABANDONED, если объект-взаимоисключение не был освобожден до того, как владеющая им нить завершилась. Если время ожидания указано равным нулю, функция возвращает результат немедленно, в противном случае она ждет в течение указанного промежутка времени. В случае, если состояние объекта станет сигнальным до истечения этого времени, функция вернет WAIT_OBJECT_0, в противном случае функция вернет WAIT_TIMEOUT. Если в качестве времени указана символическая константа INFINITE, то функция будет ждать неограниченно долго, пока состояние объекта не станет сигнальным.
Очень важен тот факт, что обращение к ожидающей функции блокирует текущую нить, т.е. пока нить находится в состоянии ожидания, ей не выделяется процессорного времени.
Критические секции
Объект-критическая секция помогает программисту выделить участок кода, где нить получает доступ к разделяемому ресурсу, и предотвратить одновременное использование ресурса. Перед использованием ресурса нить входит в критическую секцию (вызывает функцию EnterCriticalSection). Если после этого какая-либо другая нить попытается войти в ту же самую критическую секцию, ее выполнение приостановится, пока первая нить не покинет секцию с помощью вызова LeaveCriticalSection. Используется только для нитей одного процесса. Порядок входа в критическую секцию не определен.
Существует также функция TryEnterCriticalSection, которая проверяет, занята ли критическая секция в данный момент. С ее помощью нить в процессе ожидания доступа к ресурсу может не блокироваться, а выполнять какие-то полезные действия.
Пример. Синхронизация нитей с помощью критических секций.
#include
Взаимоисключения
Объекты-взаимоисключения (мьютексы, mutex - от MUTual EXclusion) позволяют координировать взаимное исключение доступа к разделяемому ресурсу. Сигнальное состояние объекта (т.е. состояние "установлен") соответствует моменту времени, когда объект не принадлежит ни одной нити и его можно "захватить". И наоборот, состояние "сброшен" (не сигнальное) соответствует моменту, когда какая-либо нить уже владеет этим объектом. Доступ к объекту разрешается, когда нить, владеющая объектом, освободит его.
Две (или более) нити могут создать мьютекс с одним и тем же именем, вызвав функцию CreateMutex. Первая нить действительно создает мьютекс, а следующие - получают дескриптор уже существующего объекта. Это дает возможность нескольким нитям получить дескриптор одного и того же мьютекса, освобождая программиста от необходимости заботиться о том, кто в действительности создает мьютекс. Если используется такой подход, желательно установить флаг bInitialOwner в FALSE, иначе возникнут определенные трудности при определении действительного создателя мьютекса.
Несколько нитей могут получить дескриптор одного и того же мьютекса, что делает возможным взаимодействие между процессами. Можно использовать следующие механизмы такого подхода:
- Дочерний процесс, созданный при помощи функции CreateProcess может наследовать дескриптор мьютекса в случае, если при создании мьютекса функцией CreateMutex был указан параметр lpMutexAttributes.
- Нить может получить дубликат существующего мьютекса с помощью функции DuplicateHandle.
- Нить может указать имя существующего мьютекса при вызове функций OpenMutex или CreateMutex.
Для того чтобы объявить взаимоисключение принадлежащим текущей нити, надо вызвать одну из ожидающих функций. Нить, которой принадлежит объект, может его "захватывать" повторно сколько угодно раз (это не приведет к самоблокировке), но столько же раз она должна будет его освобождать с помощью функции ReleaseMutex.
Для синхронизации нитей одного процесса более эффективно использование критических секций.
Пример. Синхронизация нитей с помощью мьютексов.
#include
События
Объекты-события используются для уведомления ожидающих нитей о наступлении какого-либо события. Различают два вида событий - с ручным и автоматическим сбросом. Ручной сброс осуществляется функцией ResetEvent. События с ручным сбросом используются для уведомления сразу нескольких нитей. При использовании события с автосбросом уведомление получит и продолжит свое выполнение только одна ожидающая нить, остальные будут ожидать дальше.
Функция CreateEvent создает объект-событие, SetEvent - устанавливает событие в сигнальное состояние, ResetEvent - сбрасывает событие. Функция PulseEvent устанавливает событие, а после возобновления ожидающих это событие нитей (всех при ручном сбросе и только одной при автоматическом), сбрасывает его. Если ожидающих нитей нет, PulseEvent просто сбрасывает событие.
Пример. Синхронизация нитей с помощью событий.
#include
Семафоры
Объект-семафор - это фактически объект-взаимоисключение со счетчиком. Данный объект позволяет "захватить" себя определенному количеству нитей. После этого "захват" будет невозможен, пока одна из ранее "захвативших" семафор нитей не освободит его. Семафоры применяются для ограничения количества нитей, одновременно работающих с ресурсом. Объекту при инициализации передается максимальное число нитей, после каждого "захвата" счетчик семафора уменьшается. Сигнальному состоянию соответствует значение счетчика больше нуля. Когда счетчик равен нулю, семафор считается не установленным (сброшенным).
Функция CreateSemaphore создает объект-семафор с указанием и максимально возможного начального его значения, OpenSemaphore – возвращает дескриптор существующего семафора, захват семафора производится с помощью ожидающих функций, при этом значение семафора уменьшается на единицу, ReleaseSemaphore - освобождение семафора с увеличением значения семафора на указанное в параметре число.
Пример. Синхронизация нитей с помощью семафоров.
#include
Защищенный доступ к переменным
Существует ряд функций, позволяющих работать с глобальными переменными из всех нитей, не заботясь о синхронизации, т.к. эти функции сами за ней следят – их выполнение атомарно. Это функции InterlockedIncrement, InterlockedDecrement, InterlockedExchange, InterlockedExchangeAdd и InterlockedCompareExchange. Например, функция InterlockedIncrement атомарно увеличивает значение 32-битной переменной на единицу, что удобно использовать для различных счетчиков.
Для получения полной информации о назначении, использовании и синтаксисе всех функций WIN32 API необходимо воспользоваться системой помощи MS SDK, входящей в состав сред программирования Borland Delphi или CBuilder, а также MSDN, поставляемым в составе системы программирования Visual C.
Иногда при работе с несколькими потоками или процессами появляется необходимость синхронизировать выполнение двух или более из них. Причина этого чаще всего заключается в том, что два или более потоков могут требовать доступ к разделяемому ресурсу, которыйреально не может быть предоставлен сразу нескольким потокам. Разделяемым называется ресурс, доступ к которому могут одновременно получать несколько выполняющихся задач.
Механизм, обеспечивающий процесс синхронизации, называется ограничением доступа. Необходимость в нем возникает также в тех случаях, когда один поток ожидает события, генерируемого другим потоком. Естественно, должен существовать какой-то способ, с помощью которого первой поток будет приостановлен до совершения события. После этого поток должен продолжить свое выполнение.
Имеется два общих состояния, в которых может находиться задача. Во-первых, задача может выполняться (или быть готовой к выполнению, как только получит доступ к ресурсам процессора). Во-вторых, задача может бытьблокирована. В этом случае ее выполнение приостановлено до тех пор, пока не освободится нужный ей ресурс или не произойдет определенное событие.
В Windows имеется специальные сервисы, которые позволяют определенным образом ограничить доступ к разделяемым ресурсам, ведь без помощи операционной системы отдельный процесс или поток не может сам определить, имеет ли он единоличный доступ к ресурсу. Операционная система Windows содержит процедуру, которая в течении одной непрерывной операции проверяет и, если это возможно, устанавливает флаг доступа к ресурсу. На языке разработчиков операционной системы такая операция называется операцией проверки и установки . Флаги, используемые для обеспечения синхронизации и управления доступом к ресурсам, называютсясемафорами (semaphore) . Интерфейс Win32 API обеспечивает поддержку семафоров и других объектов синхронизации. Библиотека MFC также включает поддержку данных объектов.
Объекты синхронизации и классы mfc
Интерфейс Win32 поддерживает четыре типа объектов синхронизации - все они так или иначе основаны на понятии семафора.
Первым типом объектов является собственно семафор, или классический (стандартный) семафор . Он позволяет ограниченному числу процессов и потоков обращаться к одному ресурсу. При этом доступ к ресурсу либо полностью ограничен (один и только один поток или процесс может обратиться к ресурсу в определенный период времени), либо одновременный доступ получает лишь малое количество потоков и процессов. Семафоры реализуются с помощью счетчика, значение которого уменьшается, когда задаче выделяется семафор, то увеличивается, когда задача освобождает семафор.
Вторым типом объектов синхронизации является исключающий (mutex) семафор . Он предназначен для полного ограничения доступа к ресурсу, чтобы в любой момент времени к ресурсу мог обратиться только один процесс или поток. Фактически, это особая разновидность семафора.
Третьим типом объектов синхронизации является событие , илиобъект события (event object). Он используется для блокирования доступа к ресурсу до тех пор, пока какой-нибудь другой процесс или поток не заявит о том, что данный ресурс может быть использован. Таким образом, данный объект сигнализирует о выполнении требуемого события.
При помощи объекта синхронизации четвертого типа можно запрещать выполнения определенных участков кода программы несколькими потоками одновременно. Для этого данные участки должны быть объявлены как критический раздел (critical section) . Когда в этот раздел входит один поток, другим потокам запрещается делать тоже самое до тех пор, пока первый поток не выйдет из данного раздела.
Критические разделы, в отличие от других типов объектов синхронизации, применяются только для синхронизации потоков внутри одного процесса. Другие же типы объектов могут быть использованы для синхронизации потоков внутри процесса или для синхронизации процессов.
В MFC механизм синхронизации, обеспечиваемый интерфейсом Win32 , поддерживается с помощью следующих классов, порожденных от класса CSyncObject:
CCriticalSection - реализует критический раздел.
CEvent - реализует объект события
CMutex - реализует исключающий семафор.
CSemaphore - реализует классический семафор.
Кроме этих классов в MFC определены также два вспомогательных класса синхронизации: CSingleLock иCMultiLock . Они контролируют доступ к объекту синхронизации и содержат методы, используемы для предоставления и освобождения таких объектов. КлассCSingleLock управляет доступом к одному объекту синхронизации, а классCMultiLock - к нескольким объектам. Далее будем рассматривать только классCSingleLock .
Когда какой-либо объект синхронизации создан, доступ к нему можно контролировать с помощью класса CSingleLock . Для этого необходимо сначала создать объект типаCSingleLock с помощью конструктора:
CSingleLock (CSyncObject* pObject, BOOL bInitialLock = FALSE);
Через первый параметр передается указатель на объект синхронизации, например семафор. Значение второго параметра определяет, должен ли конструктор попытаться получить доступ к данному объекту. Если этот параметр не равен нулю, то доступ будет получен, в противном случае попыток получить доступ не будет. Если доступ получен, то поток, создавший объект класса CSingleLock , будет остановлен до освобождения соответствующего объекта синхронизации методомUnlock классаCSingleLock .
Когда объект типа CSingleLock создан, доступ к объекту, на который указывал параметр pObject , может контролироваться с помощью двух функций: Lock иUnlock классаCSingleLock .
Метод Lock предназначен для получения доступа к объекту к объекту синхронизации. Вызвавший его поток приостанавливается до завершения данного метода, то есть до тех пор, пока не будет получен доступ к ресурсу. Значение параметра определяет, как долго функция будет ожидать получения доступа к требуемому объекту. Каждый раз при успешном завершении метода значение счетчика, связанного с объектом синхронизации, уменьшается на единицу.
Метод Unlock освобождает объект синхронизации, давая возможность другим потокам использовать ресурс. В первом варианте метода значение счетчика, связанного с данным объектом, увеличивается на единицу. Во втором варианте первый параметр определяет, на сколько это значение должно быть увеличено. Второй параметр указывает на переменную, в которую будет записано предыдущее значение счетчика.
При работе с классом CSingleLock общая процедура управления доступом к ресурсу такова:
создать объект типа CSyncObj (например, семафор), который будет использоваться для управления доступом к ресурсу;
с помощью созданного объекта синхронизации создать объект типа CSingleLock;
для получения доступа к ресурсу вызвать метод Lock;
выполнить обращение к ресурсу;
вызвать метод Unlock , чтобы освободить ресурс.
Далее описывается, как создавать и использовать семафоры и объекты событий. Разобравшись с этими понятиями, можно достаточно просто изучить и использовать два других типа объектов снхронизации: критические секции и мьютексы.
В операционной системе Windows поддерживаются четыре типа объекта синхронизации.
- Первый тип представляет собой классический семафор и может быть использован для управления доступом к определенным ресурсам ограниченного количества потоков. Разделяемый ресурс в этом случае может быть использован одним, и только одним потоком, либо некоторым числом потоков из множества претендующих на этот ресурс. Семафоры реализуются как простые счетчики, значения которых увеличиваются, когда поток освобождает семафор, и уменьшаются, когда поток занимает семафор.
- Второй тип объектов синхронизации называется исключающим семафором (mutex). Исключающие семафоры применяются для разделения ресурсов таким образом, что в любой момент времени их может использовать один и только один поток. Очевидно, что исключающий семафор представляет собой специальный тип обычного семафора.
- Третий тип объектов синхронизации - это событие (event). События могут служить для блокировки доступа к ресурсу до тех пор, пока другой поток не сигнализирует об его освобождении.
- Четвертый тип объектов синхронизации представляет собой критическую секцию (critical section). При вхождении потока в критическую секцию никакой другой поток не может начать ее выполнение до того, как работающий с ней поток не выйдет из нее.
Следует отметить, что синхронизация потоков с помощью критических секций может осуществляться только в рамках одного процесса, так как невозможно передать адрес критической секции из одного процесса в другой, так как критическая секция располагается в области глобальных переменных запущенного процесса.
Семафор создается с помощью функции CreateSemaphore. Количество задач, одновременно имеющих доступ к некоторому ресурсу, определяется одним из параметров функции. Если значение этой функции равно 1, то семафор работает как исключающий. При успешном создании семафора возвращается его дескриптор, в противном случае - null. Функция WaitForSingleObject обеспечивает режим ожидания семафора. Один из параметров указывает время его ожидания в миллисекундах. Если значение этого параметра равно INFINITE, то время ожидания неопределено. При успешном завершении функции значение счетчика, связанного с семафором, уменьшается не единицу. Функция ReleaseSemaphore() освобождает семафор, позволяя использовать его другому потоку.
Исключающий семафор mutex создается с помощью функции CreateMutex(), которая возвращает идентификатор созданного объекта или null в случае ошибки. В случае необходимости объект освобождается с помощью универсальной функции CloseHandle(). Зная имя объекта mutex, его можно открыть с помощью функции OpenMutex(). С помощью этой функции несколько потоков могут открыть один и тот же объект, а затем одновременно выполнить его ожидание. После того, как имя объекта стало известно потоку, он может им завладеть, используя функции WaitForSingleObject или WaitForMultipleObjects. Освобождение объекта mutex осуществляется с помощью функции ReleaseMutex().
Событие создается с помощью функции CreateEvent. Она возвращает дескриптор созданного события или null в случае неуспешного завершения. После создании события поток просто ожидает его наступления используя функцию WaitForSingleObject, задавая в качестве первого параметра для нее дескриптор этого события. Тем самым выполнение потока приостанавливается до наступления соответствующего события. После вызова функции SetEvent процесс, ожидающий данного события с помощью функции WaitForSingleObject, продолжит свое выполнение. Событие может быть сброшено с помощью функции ResetEvent.