21 Потоковая многозадачность (1061100), страница 2
Текст из файла (страница 2)
Имеется два общих состояния, в которых может находиться задача. Во-первых, задача может выполняться (или быть готовой к выполнению, как только получит доступ к ресурсам процессора). Во-вторых, задача может быть блокирована. В этом случае ее выполнение приостановлено до тех пор, пока не освободится нужный ей ресурс или не произойдет определенное событие.
В 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, чтобы освободить ресурс.
Далее описывается, как создавать и использовать семафоры и объекты событий. Разобравшись с этими понятиями, можно достаточно просто изучить и использовать два других типа объектов снхронизации: критические секции и мьютексы.
Работа с семафорами
Рассмотрим, как обеспечить синхронизацию потоков на основе семафоров. Прежде всего необходимо создать семафор путем объявления объекта типа CSemaphore. Конструктор этого класса имеет следующий вид:
CSemaphore( LONG lInitialCount = 1, LONG lMaxCount = 1, LPCTSTR pstrName = NULL, LPSECURITY_ATTRIBUTES lpsaAttributes = NULL );
Семафор позволяет одному или нескольким потокам получать доступ к объекту. Допустимое число потоков, которым будет разрешен одновременный доступ, указывается во втором параметре. Если это значение равно единице, то семафор будет исключающим, т.е. только один поток или процесс сможет одновременно обращаться к ресурсу.
Семафоры имеют счетчик, указывающий количество задач, указывающий количество задач, которым в настоящее время предоставлен доступ к ресурсу. Если значение счетчика равно нулю, то последующий доступ к ресурсу запрещается до тех пор, пока одна из задач не освободит семафор. Начальное значение счетчика семафора указывается в первом параметре конструктора (это значение не должно быть отрицательным и не должно превышать значение второго параметра конструктора). Если этот параметр равен нулю, то все потоки, ожидающие семафор, будут приостановлены, пока семафор не освободится где-нибудь в программе. Обычно начальное значение задается равным единице, чтобы хотя бы один поток мог получить семафор.
Третий параметр конструктора указывает на строку, содержащую имя объекта семафора. Поименованные семафоры становятся системными объектами и могут использоваться другими процессами. Когда два процесса вызывают семафоры с одинаковыми именами, обоим процессам будет предоставлен один и тот же семафор - это позволяет синхронизировать процессы. Вместо имени строки можно указать NULL - в этом случае семафор будет локализован внутри одного процесса.
Последний параметр конструктора является указателем на набор атрибутов прав доступа, связанный с семафором. Если этот параметр равен NULL, то семафор наследует данный набор у вызвавшего его потока.
Внесем некоторые изменения в пример, рассматриваемый в предыдущем параграфе данной главы. Добавим в класс CExampleView объект-семафор:
class CExampleView : public CView
{
protected:
// только один поток сможет одновременно обращаться к ресурсу
CSemaphore sem;
// другие описания класса
.....
};
Далее при обработке сообщения от меню создадим два потока. В каждой из функций этих потоков объект-семафор используется для разграничения доступа потоков к ресурсам:
UINT MyThread1(LPVOID pParam);
UINT MyThread2(LPVOID pParam);
void CExampleView::OnStart()
{
AfxBeginThread(MyThread1,this);
AfxBeginThread(MyThread2,this;
}
UINT MyThread1(LPVOID pParam)
{
CExampleView *ptrView=(CExampleView *)pParam;
CSingleLock syncObj(&(ptrView->sem));
.......
syncObj.Lock(); // получение семафора
действия, связанные с доступом к ресурсу
syncObj.Unlock(); // освобождение семафора
.......
return 0;
}
UINT MyThread2(LPVOID pParam)
{
CExampleView *ptrView=(CExampleView *)pParam;
CSingleLock syncObj(&(ptrView->sem));
.......
syncObj.Lock(); // получение семафора
действия, связанные с доступом к ресурсу
syncObj.Unlock(); // освобождение семафора
.......
return 0;
}
В каждой из функций потока создается объект типа CsingleLock на базе семафора, а затем вызывается метод Lock. Когда первый поток получает доступ к семафору, другой поток приостанавливается до тех пор, пока первый поток не освободит семафор с помощью функции Unlock. Таким образом, семафор в данном примере предоставляется только одному потоку. Это напоминает работу с исключающим семафором.
Работа с объектами событий
Объект событие используется для оповещения процесса или потока о том, сто произошло некоторое событие. Для работы с такими объектами предназначен класс CEvent. Конструктор класса имеет следующий прототип:
CEvent( BOOL bInitiallyOwn = FALSE, BOOL bManualReset = FALSE,
LPCTSTR lpszName = NULL,
LPSECURITY_ATTRIBUTES lpsaAttribute = NULL );
Значение первого параметра определяет начальное состояние объекта. Если оно равно TRUE, то объект событие установлен (событие произошло), а если FALSE, то объект не установлен или сброшен (событие не произошло).
Второй параметр указывает, каким образом состояние объекта будет изменяться при выполнении события. Если значение параметра равно TRUE (не ноль), то объект может быть сброшен только путем вызова метода ResetEvent класса CEvent. В противном случае объект автоматически сбрасывается после предоставления блокированному потоку доступа к ресурсу.
Третий параметр конструктора указывает на строку, содержащую имя объекта события. Поименованные объекты события становятся системными объектами и могут использоваться другими процессами. Когда два процесса вызывают объекты события с одинаковыми именами, обоим процессам будет предоставлен один и тот же объект - это позволяет синхронизировать процессы. Вместо имени строки можно указать NULL - в этом случае объект события будет локализован внутри одного процесса.
Последний параметр конструктора является указателем на набор атрибутов прав доступа, связанный с объектом события. Если этот параметр равен NULL, то объект событие наследует данный набор у вызвавшего его потока.
Когда объект событие создан, то поток, ожидающий данное событие, должен с помощью этого объекта создать объект типа CSingleLock, для которого затем следует вызвать метод Lock. При этом выполнение данного потока останавливается до тех пор, пока не произойдет ожидаемое событие.
Для сигнализации о том, что событие произошло, предназначена функция SetEvent класса CEvent.
При вызове данной функции первый поток, ожидающий событие, выйдет из остановленного состояния (вызванный им метод Lock завершится и продолжит свое выполнение.
При работе с программным объектом события необходимо вызывать метод ResetEvent класса CEvent всякий раз, когда поступил сигнал о выполнении события. После его вызова соответствующий объект будет сброшен.
Чтобы продемонстрировать вышесказанное, заменим в предыдущем примере семафор на объект событие. В этой версии функция MyThread1 будет блокирована до тех пор, пока функция MyThread2 не завершится и не сигнализирует о своем завершении. Таким образом, завершение функции MyThread2 является событием, которое ожидает MyThread1: