III
Поговорим теперь о многопотоковой программе. В принципе, если не предполагается, что потоки как-то взаимодействуют друг с другом, технически не имеет значения, запущен один или несколько потоков. Сложности возникают, когда работа одного потока зависит от деятельности другого потока. Здесь возможны самые разные ситуации. Давайте рассмотрим все по порядку.
Нам уже пришлось столкнуться с ситуацией, когда два параллельных процесса взаимодействовали друг с другом посредством глобальных переменных. Точнее, один процесс готовил данные для другого процесса. Здесь не было никакой сложности: просто один процесс с некоторой периодичностью менял содержимое переменной, а второй процесс, с другой периодичностью, читал из этой переменной. Если период обновления данных меньше периода взятия данных, то мы почти с достоверностью (почти!) получаем, что второй процесс будет получать всегда свежие данные. Иногда это, т.е. соотношение между периодом обновления и периодом получения данных, как в нашем случае, вообще не имеет никакого значения.
Часто случается, что данные невозможно получать периодически. Они могут,
например, зависеть от деятельности третьего процесса. Как же второй процесс
узнает, что данные уже готовы для передачи? На первый взгляд проблема решается
введением дополнительной переменной, назовем ее FLAG
. Примем, что
при FLAG=0
данные не готовы, а при FLAG=1
данные
готовы. Далее действует весьма простая схема.
NO_DAT: CMP FLAG,1 JNE NO_DAT ... ;передача данных ... MOV FLAG,0 ...
Это фрагмент, как Вы понимаете, для второго потока. Первый же поток также
должен проверять переменную FLAG
и, если FLAG=0
,
поместить новые данные и установить значение переменной FLAG
,
равное единице. Данная схема совсем не плоха, например, когда один процесс ждет
окончания работы другого процесса. Другими словами, данные являются результатом
всей работы этого процесса. Например, запущен компилятор, а другой поток ждет
окончания его работы, дабы вывести результаты этой работы на некое устройство.
Эта ситуация весьма распространена, но все же случай этот частный.
А что будет, если процесс передачи данных должен производится многократно?
Легко видеть, что данная схема будет работать и в более сложном случае. Важно,
однако, чтобы второй процесс менял содержимое FLAG
только после
того, как он возьмет данные, а первый процесс - после того, как положит данные.
Если нарушить это правило, то может возникнуть коллизия, когда, например, второй
процесс еще не взял данные, а они уже изменились.
Такой подход можно осуществить и в более общем случае, когда два потока (или
- два процесса) должны поочередно получать доступ к одному ресурсу. Как легко
видеть, данный подход предполагает, что ресурс открыт либо для одного, либо для
другого процесса. Если бы поставить задачу несколько иным образом - процесс либо
открыт, либо закрыт для доступа, то возникло бы некоторое затруднение.
Действительно, вероятна такая ситуация, когда оба потока ожидают открытия
ресурса. Другими словами, они непрерывно осуществляют проверку переменной
FLAG
(CMP FLAG,1
). Может статься, что они оба почти
одновременно обратятся к ресурсу. Совершенно ясно, что здесь возникает
необходимость в "третьей силе", которая бы занималась распределением доступа к
ресурсу. Например, посылала бы сообщение вначале одному потоку и, если он
ожидает доступа, давала доступ именно ему, а затем подобный процесс повторяется
со вторым потоком.
Схема, описанная выше, весьма хороша, но при условии поочередного обращения к ресурсу. Если это в общем случае не выполняется, то данный подход может дать серьезный сбой. Все приведенные здесь рассуждения имеют одну цель - показать, что проблема взаимодействия потоков и процессов, их синхронизации, является и сложной, и актуальной для многозадачных операционных систем. Отсюда следует, что такие операционные системы должны иметь свои собственные средства синхронизации.41
Оставшаяся часть данного раздела будет всецело посвящена средствам синхронизации операционной системы Windows.
Семафоры. Семафор представляет собой глобальный
объект, позволяющий синхронизировать работу двух или нескольких процессов или
потоков. Для программиста семафор - это просто счетчик. Если счетчик равен
N
, это означает, что к ресурсу имеют доступ N
процессов. Рассмотрим функции для работы с семафорами.
CreateSemaphore
- создает глобальный объект-семафор. Возвращает
дескриптор семафора. Параметры функции:
- 1-й параметр. Указатель на структуру, определяющую атрибуты доступа. Может
иметь значение для Windows NT. Обычно данный параметр полагается равным
NULL
. - 2-й параметр. Начальное значение счетчика семафора. Определяет, сколько задач имеют доступ к ресурсу вначале.
- 3-й параметр. Количество задач, которые имеют одновременный доступ к ресурсу.
- 4-й параметр. Указатель на строку, содержащую имя семафора.
OpenSemaphore
- открыть уже созданный семафор. Возвращает
дескриптор семафора. Данную функцию используют не так часто. Обычно создают
семафор и присваивают его дескриптор глобальной переменной, а потом используют
этот дескриптор в порождаемых потоках. Параметры функции:
- 1-й параметр. Определяет желаемый уровень доступа к семафору. Возможные
значения:
SEMAPHORE_MODIFY_STATE = 2Н
, разрешить использование функцииReleaseSemaphore
,SYNCHRONIZE = 100000Н
, разрешить использование любой функции ожидания, только для Windows NT,SEMAPHORE_ALL_ACCESS = 0F0000h + SYNCHRONIZE + 3H
, специфицирует все возможные флаги доступа к семафору. - (в книге пропущен) 2-й параметр. Указывает, может ли наследоваться
дескриптор семафора, возвращаемый данной функцией, процессом, созаваемым
функцией CreateProcess;
0
- не может. - (также в книге пропущен) 3-й параметр. Указатель на ASCIIZ-строку, содержащую имя семафора.
WaitForSingleObject
- ожидать открытие семафора. При успешном
завершении, т.е. открытии доступа к объекту, функция возвращает 0
.
Значение 102h
будет означать, что закончился заданный период
ожидания. Параметры функции:
- 1-й параметр. Дескриптор семафора.
- 2-й параметр. Время ожидания в миллисекундах. Если параметр равен
INFINITE = 0FFFFFFFFh
, то время ожидания не ограничено.
ReleaseSemaphore
- освободить семафор и позволить получить
доступ к ресурсу другим процессам. Параметры функции:
- 1-й параметр. Дескриптор семафора.
- 2-й параметр. Определяет, какое значение должно быть добавлено к счетчику семафора. Чаще всего этот параметр равен единице.
- 3-й параметр. Указатель на переменную, куда должно быть помещено предыдущее значение счетчика.
Рассмотрим алгоритм работы с семафором. Сначала при помощи функции
CreateSemaphore
создается семафор и его дескриптор присваивается
глобальной переменной. Перед попыткой обращения к ресурсам, доступ к которым
необходимо ограничить, поток должен вызвать функцию
WaitForSingleObject
. При открытии доступа функция возвращает
0
. По окончании работы с ресурсом следует вызвать функцию
ReleaseSemaphore
. Тем самым увеличивается счетчик доступа на
1
. С помощью семафора можно регулировать количество потоков,
которые одновременно могут иметь доступ к ресурсу. Максимальное значение
счетчика как раз и определяет, сколько потоков могут получить доступ к ресурсу
одновременно. Но обычно, как я уже говорил, максимальное значение полагают
равным 1
.
События. Событие является объектом, очень похожим на семафор, но в несколько видоизмененном виде. Рассмотрим функции для работы с событиями.
CreateEvent
- создает объект-событие. Параметры функции.
1-й
параметр. Имеет тот же смысл, что и первый параметр функции
CreateSemaphore
. Обычно полагается равным
NULL
.
2-параметр. Если параметр не равен нулю, то событие может
быть сброшено при помощи функции ResetEvent
. Иначе событие
сбрасывается при доступе к нему какого либо процесса.
3-й параметр. Если
параметр равен 0
, то событие инициализируется как сброшенное, в
противном случае сразу же подается сигнал о наступлении соответствующей
ситуации.
4-й параметр. Указатель на строку, которая содержит имя
события.
Ожидание события осуществляется, как и в случае с семафором, функцией
WaitForSingleObject
.
Функция OpenEvent
аналогична функции OpenSemaphore
,
и на ней мы останавливаться не будем.
SetEvent
- подать сигнал о наступлении события. Параметры
функции.
1-й параметр. Дескриптор события.
Критические секции. Понятие критической секции позволяет уберечь определенные области программы так, чтобы в этой области программы в данный момент времени исполнялся бы только один поток. Рассмотрим функции для работы с критической секцией.
InitializeCriticalSection
- данная функция создает объект под
названием критическая секция. Параметры функции.
- 1-й параметр. Указатель на структуру, указанную ниже. Поля данной структуры
используются только внутренними процедурами, и смысл их безразличен.
CRITICAL_SECTION STRUCT DebugInfo DWORD ? LockCount LONG ? RecursionCount LONG ? OwningThread HANDLE ? LockSemaphore HANDLE ? SpinCount DWORD ? CRITICAL_SECTION ENDS
EnterCriticalSection
- войти в критическую секцию. После
выполнения этой функции данный поток становится владельцем данной секции.
Следующий поток, вызвав данную функцию, будет находиться в состоянии ожидания.
Параметр функции такой же, что и в предыдущей функции.
LeaveCriticalSection
- покинуть критическую секцию. После этого
второй поток, который был остановлен функцией EnterCriticalSection
,
станет владельцем критической секции. Параметр функции
LeaveCriticalSection
такой же, как и у предыдущих функций.
DeleteCriticalSection
- удалить объект "критическая секция".
Параметр аналогичен предыдущим.
Программно можно определить несколько объектов критической секции, с которыми будут работать несколько потоков. Мы не зря, говоря о критических секциях, упоминаем только потоки. Разные процессы не могут использовать данный объект синхронизации.
Теперь рассмотрим пример использования критической секции. Примеры использования семафоров и событий Вы сможете найти в книге [4]. Изложим вкратце идею, положенную в основу примера на Рис. 3.2.3.
Два потока обращаются время от времени к процедуре, выводящей очередной символ из строки в окно. В результате такой конкурентной деятельности должна быть напечатана строка.
Часть процедуры, выводящей очередной символ, сделана критической, поэтому доступ к выводу в окно в данный момент времени имеет только один поток.
; файл thread2.inc ; константы ; сообщение приходит при закрытии окна WM_DESTROY equ 2 ; сообщение приходит при создании окна WM_CREATE equ 1 ; сообщение при щелчке левой кнопкой мыши в области окна WM_LBUTTONDOWN equ 201h ; сообщение при щелчке правой кнопкой мыши в области окна WM_RBUTTONDOWN equ 204h ; свойства окна CS_VREDRAW equ 1h CS_HREDRAW equ 2h CS_GLOBALCLASS equ 4000h WS_OVERLAPPEDWINDOW equ 000CF0000H stylcl equ CS_HREDRAW + CS_VREDRAW + CS_GLOBALCLASS DX0 equ 300 DY0 equ 200 ; компоненты цветов RED equ 50 GREEN equ 50 BLUE equ 255 RGBW equ (RED or (GREEN shl 8)) or (BLUE shl 16) RGBT equ 255 ; красный ; идентификатор стандартной иконки IDI_APPLICATION equ 32512 ; идентификатор курсора IDC_CROSS equ 32515 ; режим показа окна - нормальный SW_SHOWNORMAL equ 1 ; прототипы внешних процедур IFDEF MASM EXTERN Sleep@4:NEAR EXTERN CreateThread@24:NEAR EXTERN InitializeCriticalSection@4:NEAR EXTERN EnterCriticalSection@4:NEAR EXTERN LeaveCriticalSection@4:NEAR EXTERN DeleteCriticalSection@4:NEAR EXTERN GetTextExtentPoint32A@16:NEAR EXTERN CreateWindowExA@48:NEAR EXTERN DefWindowProcA@16:NEAR EXTERN DispatchMessageA@4:NEAR EXTERN ExitProcess@4:NEAR EXTERN GetMessageA@16:NEAR EXTERN GetModuleHandleA@4:NEAR EXTERN LoadCursorA@8:NEAR EXTERN LoadIconA@8:NEAR EXTERN PostQuitMessage@4:NEAR EXTERN RegisterClassA@4:NEAR EXTERN ShowWindow@8:NEAR EXTERN TranslateMessage@4:NEAR EXTERN UpdateWindow@4:NEAR EXTERN TextOutA@20:NEAR EXTERN CreateSolidBrush@4:NEAR EXTERN SetBkColor@8:NEAR EXTERN SetTextColor@8:NEAR EXTERN GetDC@4:NEAR EXTERN DeleteDC@4:NEAR ELSE EXTERN Sleep:NEAR EXTERN CreateThread:NEAR EXTERN InitializeCriticalSection:NEAR EXTERN EnterCriticalSection:NEAR EXTERN LeaveCriticalSection:NEAR EXTERN DeleteCriticaiSection:NEAR EXTERN GetTextExtentPoint32A:NEAR EXTERN CreateWindowExA:NEAR EXTERN DefWindowProcA:NEAR EXTERN DispatchMessageA:NEAR EXTERN ExitProcess:NEAR EXTERN GetMessageA:NEAR EXTERN GetModuleHandleA:NEAR EXTERN LoadCursorA:NEAR EXTERN LoadIconA:NEAR EXTERN PostQuitMessage:NEAR EXTERN RegisterClassA:NEAR EXTERN ShowWindow:NEAR EXTERN TranslateMessage:NEAR EXTERN UpdateWindow:NEAR EXTERN TextOutA:NEAR EXTERN CreateSolidBrush:NEAR EXTERN SetBkColor:NEAR EXTERN SetTextColor:NEAR EXTERN GetDC:NEAR EXTERN DeleteDC:NEAR Sleep@4 = Sleep CreateThread@24 = CreateThread InitializeCriticalSection@4 = InitializeCriticalSection EnterCriticalSection@4 = EnterCriticalSection LeaveCriticalSection@4 = LeaveCriticalSection DeleteCriticalSection@4 = DeleteCriticaiSection GetTextExtentPoint32A@16 = GetTextExtentPoint32A CreateWindowExA@48 = CreateWindowExA DefWindowProcA@16 = DefWindowProcA DispatchMessageA@4 = DispatchMessageA ExitProcess@4 = ExitProcess GetMessageA@16 = GetMessageA GetModuleHandleA@4 = GetModuleHandleA LoadCursorA@8 = LoadCursorA LoadIconA@8 = LoadIconA PostQuitMessage@4 = PostQuitMessage RegisterClassA@4 = RegisterClassA ShowWindow@8 = ShowWindow TranslateMessage@4 = TranslateMessage UpdateWindow@4 = UpdateWindow TextOutA@20 = TextOutA CreateSolidBrush@4 = CreateSolidBrush SetBkColor@8 = SetBkColor SetTextColor@8 = SetTextColor GetDC@4 = GetDC DeleteDC@4 = DeleteDC ENDIF ; структуры ; структура сообщения MSGSTRUCT STRUC MSHWND DD ? MSMESSAGE DD ? MSWPARAM DD ? MSLPARAM DD ? MSTIME DD ? MSPT DD ? MSGSTRUCT ENDS ;------------- WNDCLASS STRUC CLSSTYLE DD ? CLSLPFNWNDPROC DD ? CLSCBCLSEXTRA DD ? CLSCBWNDEXTRA DD ? CLSHINSTANCE DD ? CLSHICON DD ? CLSHCURSOR DD ? CLSHBRBACKGROUND DD ? MENNAME DD ? CLSNAME DD ? WNDCLASS ENDS ; структура для работы с критической секцией CRIT STRUC DD ? DD ? DD ? DD ? DD ? DD ? CRIT ENDS ; структура для определения длины текста SIZET STRUC X1 DWORD ? Y1 DWORD ? SIZET ENDS ; файл thread2.asm .386P ; плоская модель .MODEL FLAT, stdcall ;------------------------------------------------ include thread2.inc ; подключения библиотек IFDEF MASM includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib includelib c:\masm32\lib\gdi32.lib ELSE includelib c:\tasm32\lib\import32.lib ENDIF ;------------------------------------------------ ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' NEWHWND DD 0 MSG MSGSTRUCT <?> WC WNDCLASS <?> SZT SIZET <?> HINST DD 0 TITLENAME DB 'Вывод в окно двумя потоками',0 NAM DB 'CLASS32',0 XT DWORD 30 YT DWORD 30 HW DD ? DC DD ? TEXT DB 'Текст в окне красный',0 SPA DB ' ' DB ' ',0 IND DD 0 SK CRIT <?> THR1 DD ? THR2 DD ? FLAG1 DD 0 FLAG2 DD 0 _DATA ENDS ; сегмент кода _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' START: ; получить дескриптор приложения PUSH 0 CALL GetModuleHandleA@4 MOV [HINST], EAX REG_CLASS: ; заполнить структуру окна ; стиль MOV [WC.CLSSTYLE],stylcl ; процедура обработки сообщений MOV [WC.CLSLPFNWNDPROC],OFFSET WNDPROC MOV [WC.CLSCBCLSEXTRA],0 MOV [WC.CLSCBWNDEXTRA],0 MOV EAX,[HINST] MOV [WC.CLSHINSTANCE],EAX ;----------иконка окна PUSH IDI_APPLICATION PUSH 0 CALL LoadIconA@8 MOV [WC.CLSHICON], EAX ;----------курсор окна PUSH IDC_CROSS PUSH 0 CALL LoadCursorA@8 MOV [WC.CLSHCURSOR], EAX ;---------- PUSH RGBW ; цвет кисти CALL CreateSolidBrush@4 ; создать кисть MOV [WC.CLSHBRBACKGROUND],EAX MOV DWORD PTR [WC.MENNAME],0 MOV DWORD PTR [WC.CLSNAME],OFFSET NAM PUSH OFFSET WC CALL RegisterClassA@4 ; создать окно зарегистрированного класса PUSH 0 PUSH [HINST] PUSH 0 PUSH 0 PUSH DY0 ; DY0 - высота окна PUSH DX0 ; DX0 - ширина окна PUSH 100 ; координата Y PUSH 100 ; координата X PUSH WS_OVERLAPPEDWINDOW PUSH OFFSET TITLENAME ; имя окна PUSH OFFSET NAM ; имя класса PUSH 0 CALL CreateWindowExA@48 ; проверка на ошибку CMP EAX,0 JZ _ERR MOV [NEWHWND], EAX ; дескриптор окна ;------------------------------------ PUSH SW_SHOWNORMAL PUSH [NEWHWND] CALL ShowWindow@8 ; показать созданное окно ;------------------------------------ PUSH [NEWHWND] CALL UpdateWindow@4 ;перерисовать видимую часть окна ; петля обработки сообщений MSG_LOOP: PUSH 0 PUSH 0 PUSH 0 PUSH OFFSET MSG CALL GetMessageA@16 CMP AX, 0 JE END_LOOP PUSH OFFSET MSG CALL TranslateMessage@4 PUSH OFFSET MSG CALL DispatchMessageA@4 JMP MSG_LOOP END_LOOP: ; выход из программы (закрыть процесс) PUSH [MSG.MSWPARAM] CALL ExitProcess@4 _ERR: JMP END_LOOP ;------------------------------------- ; процедура окна ; расположение параметров в стеке ; [EBP+014Н] ; LPARAM ; [EBP+10Н] ; WAPARAM ; [EBP+0CH] ; MES ; [EBP+8] ; HWND WNDPROC PROC PUSH EBP MOV EBP,ESP PUSH EBX PUSH ESI PUSH EDI CMP DWORD PTR [EBP+0CH],WM_DESTROY JE WMDESTROY CMP DWORD PTR [EBP+0CH],WM_CREATE JE WMCREATE CMP DWORD PTR [EBP+0CH],WM_LBUTTONDOWN JNE CONTIN ; проверить флаг запуска CMP FLAG1,0 JNE DEFWNDPROC MOV FLAG1,1 ; инициализировать указатели LEA EAX,TEXT MOV IND,EAX MOV XT,30 ; запуск первого потока PUSH OFFSET THR1 PUSH 0 PUSH EAX PUSH OFFSET THREAD1 PUSH 0 PUSH 0 CALL CreateThread@24 ; запуск второго потока PUSH OFFSET THR2 PUSH 0 PUSH EAX PUSH OFFSET THREAD2 PUSH 0 PUSH 0 CALL CreateThread@24 JMP DEFWNDPROC CONTIN: CMP DWORD PTR [EBP+0CH],WM_RBUTTONDOWN JNE DEFWNDPROC ; проверить флаг запуска CMP FLAG2,0 JNE DEFWNDPROC MOV FLAG2,1 ; инициализировать указатели LEA EAX,SPA MOV IND,EAX MOV XT,30 ; запуск первого потока PUSH OFFSET THR1 PUSH 0 PUSH EAX PUSH OFFSET THREAD1 PUSH 0 PUSH 0 CALL CreateThread@24 ; запуск второго потока PUSH OFFSET THR2 PUSH 0 PUSH EAX PUSH OFFSET THREAD2 PUSH 0 PUSH 0 CALL CreateThread@24 JMP DEFWNDPROC WMCREATE: MOV EAX,DWORD PTR [EBP+08H] ; запомнить дескриптор окна в глобальной переменной MOV HW,EAX ; инициализировать критическую секцию PUSH OFFSET SK CALL InitializeCriticalSection@4 MOV EAX, 0 JMP FINISH DEFWNDPROC: PUSH DWORD PTR [EBP+14H] PUSH DWORD PTR [EBP+10H] PUSH DWORD PTR [EBP+0CH] PUSH DWORD PTR [EBP+08H] CALL DefWindowProcA@16 JMP FINISH WMDESTROY: ; удалить критическую секцию PUSH OFFSET SK CALL DeleteCriticalSection@4 PUSH 0 CALL PostQuitMessage@4 ; WM_QUIT MOV EAX, 0 FINISH: POP EDI POP ESI POP EBX POP EBP RET 16 WNDPROC ENDP ; вывод OUTSTR PROC ; проверяем, не закончился ли текст MOV EBX,IND CMP BYTE PTR [EBX],0 JNE NO_0 RET NO_0: ; вход в критическую секцию PUSH OFFSET SK CALL EnterCriticalSection@4 ;----------------- PUSH HW CALL GetDC@4 MOV DC,EAX ;---------------- цвет фона = цвет окна PUSH RGBW PUSH EAX CALL SetBkColor@8 ;---------------- цвет текста (красный) PUSH RGBT PUSH DC CALL SetTextColor@8 ;---------------- вывести текст PUSH 1 PUSH IND PUSH YT PUSH XT PUSH DC CALL TextOutA@20 ;- вычислить длину текста в пикселях текста PUSH OFFSET SZT PUSH 1 PUSH IND PUSH DC CALL GetTextExtentPoint32A@16 ; увеличить указатели MOV EAX,SZT.X1 ADD XT,EAX INC IND ;---------------- закрыть контекст PUSH DC CALL DeleteDC@4 ; выход из критической секции PUSH OFFSET SK CALL LeaveCriticalSection@4 RET OUTSTR ENDP ; первый поток THREAD1 PROC L01: ; проверить, не конец ли текста MOV EBX,IND CMP BYTE PTR [EBX],0 JE _END1 ; вывод очередного символа CALL OUTSTR ; задержка PUSH 1000 CALL Sleep@4 JMP L01 _END1: RET 4 THREAD1 ENDP ; второй поток THREAD2 PROC L02: ; проверить, не конец ли текста MOV EBX,IND CMP BYTE PTR [EBX],0 JE _END2 ; вывод очередного символа CALL OUTSTR ; задержка PUSH 1000 CALL Sleep@4 JMP L02 _END2: RET 4 THREAD2 ENDP _TEXT ENDS END STARTРис. 3.2.3. Пример синхронизации двух потоков посредством критической секции.