Assembler для Windows

           

Глава 2. Многозадачное программирование

В предыдущей главе нами были рассмотрены возможности использования таймеров в прикладной задаче. Задав один или несколько таймеров, мы принуждаем систему вызывать одну или несколько процедур в автоматическом режиме. Посредством таймеров мы тем самым можем реализовать многозадачный режим в рамках одного процесса. Более того, было показано, что такие подзадачи могут взаимодействовать друг с другом. Это и понятно, ведь все эти подзадачи разделяют одно адресное пространство и, следовательно, информация от одной подзадачи к другой может передаваться через глобальные переменные. Это весьма интересный и сложный вопрос о том, как задачи и подзадачи могут взаимодействовать друг с другом. Мы рассмотрим его несколько позднее. Сейчас же рассмотрим многозадачность в операционной системе Windows с самого начала.

I

Под процессом будем понимать объект, создаваемый операционной системой Windows обычно при


загрузке исполняемого модуля и получающий в единоличное пользование:
  1. Виртуальную память, выделяемую для него операционной системой.
  2. Дескрипторы открываемых им файлов.
  3. Список загруженных им в его собственную память динамических модулей (DLL).
  4. Созданные им подпроцессы или потоки, исполняемые независимо друг от друга, в собственной памяти процесса.

Думаю, данное определение весьма ясно раскрывает суть понятия процесс. Но для большинства рассматриваемых в данной главе проблем, достаточно было бы дать и более простое определение. Например, такое: всякий исполняемый модуль (.ЕХЕ), запущенный в операционной системе Windows, становится процессом.

Теперь что касается подпроцесса. Смысл его достаточно прост: каждый процесс в отведенном для него адресном пространстве может порождать еще процессы. Эти процессы выполняются независимо друг от друга и от порождающего их процесса. Однако порождающий процесс может при желании "убить" любой из порожденных им процессов. Такие процессы называют еще потоками, а также цепочками или нитями40. Теперь мы понимаем, что в предыдущей главе при помощи таймера мы создавали потоки. Однако в операционной системе Windows для создания потоков есть и специальные средства, речь о которых пойдет ниже.

Теперь немного поговорим о типах многозадачности. В старой 16-битной Windows переключение между задачами происходило только тогда, когда задача отдавала управление операционной системе. Такая многозадачность называется невытесняющей. В определенном смысле это было даже хуже, чем в операционной системе MS DOS. Там элементы многозадачности осуществлялись при помощи так называемых TSR-программ (см. [1]). Такие программы назывались еще резидентными. Они перехватывали прерывание от таймера, клавиатуры или другого устройства и имели возможность время от времени получать управление.

Положение, существовавшее в старой операционной системе Windows, требовало от программиста выполнения джентльменского правила - не захватывать надолго время микропроцессора. Некоторым решением проблемы являлось использование таймеров (в чем мы уже убедились), а также использование функции PeekMessage вместо GetMessage. Функция PeekMessage, в отличие от GetMessage, возвращает управление сразу, даже если в очереди нет ни одного сообщения.

В 32-битных операционных системах Windows (Windows 9x, Windows NT, Windows 2000) реализована вытесняющая схема многозадачности, в которой переключением между процессами и потоками занимается операционная система. Если процесс слишком долго выполняет некоторую операцию, то курсор над окном процесса преобразуется в песочные часы. При этом другие процессы будут по-прежнему выполняться, и Вы сможете переключаться на них. А вот доступ к окну данного процесса может оказаться затруднительным. Решить данную проблему можно уже упомянутым способом, заменив в цикле ожидания GetMessage на PeekMessage. Однако более правильным решением будет разбиение процесса на некоторое количество потоков.

Созданием потоков мы займемся в следующих разделах, а оставшаяся часть данного раздела будет посвящена созданию процессов. Ваше приложение может создавать процессы, запустив ту или иную ЕХЕ-программу, которые будут работать независимо от основного приложения. Одновременно Ваше приложение может при необходимости удалить запущенное им приложение из памяти. Запустить приложение (создать процесс) можно при помощи функции CreateProcess. Сейчас мы дадим описание этой функции. Ниже объясняются ее параметры.

  • 1-й параметр - указывает на имя запускаемой программы. Имя может содержать полный путь к программе.
  • 2-й параметр - его значение зависит от того, является первый параметр NULL (0) или нет. Если первый параметр указывает на строку, то данный параметр трактуется как командная строка запуска (без имени программы). Если первый параметр равен NULL, то данный параметр рассматривается как командная строка, первый элемент которой представляет собой имя программы. Если путь к программе не указан, то функция CreateProcess осуществляет поиск программы по определенному алгоритму:
    1. Поиск в каталоге, откуда была запущена программа.
    2. Поиск в текущем каталоге.
    3. Поиск в системном каталоге (можно получить через GetSystemDirectory). Обычно системным каталогом является C:\WINDOWS\SYSTEM.
    4. Поиск в каталоге Windows (можно получить через GetWindowsDirectory). Обычно этим каталогом является C:\WINDOWS.
    5. Поиск в каталогах, перечисленных в параметре PATH окружения.
  • 3-й и 4-й параметры. Используются для задания атрибутов доступа порождаемого процесса. Обычно полагают равным 0.
  • 5-й параметр. Если этот параметр 0, то порождаемый процесс не наследует дескрипторы порождающего процесса, в противном случае порождаемый процесс наследует дескрипторы.
  • 6-й параметр. Может менять свойства порождаемого процесса. Если параметр равен нулю, то свойства задаются по умолчанию. В силу большого количества значений этого параметра, мы отсылаем желающих к соответствующим справочным руководствам.
  • 7-й параметр. Является указателем на буфер, содержащий параметры среды. Если параметр равен 0, то порождаемый процесс наследует параметры среды порождающего процесса.
  • 8-й параметр. Задает текущее устройство и каталог для порождаемого процесса. Если параметр равен NULL, порождаемый процесс наследует текущее устройство и каталог порождающего процесса.
  • 9-й параметр. Представляет указатель на структуру, которая содержит информацию об окне создаваемого процесса. Ниже будут рассмотрены поля этой структуры.
  • 10-й параметр. Указывает на структуру, заполняемую при выполнении запуск приложения. Вот эта структура:
    
    PROCINF STRUC
    hProcess DD ? ; дескриптор созданного процесса.
    hThread DD ? ; дескриптор главного потока нового процесса.
    Idproc DD ? ; идентификатор созданного процесса.
    idThr DD ? ; идентификатор главного потока нового процесса.
    PROCINF ENDS

Основное отличие дескриптора от идентификатора заключается в том, что дескриптор уникален лишь в пределах данного процесса, идентификатор же является глобальной величиной. Посредством идентификатора может быть найдена база данных текущего процесса. У читателя, я думаю, сразу возникнет вопрос: а чем дескриптор приложения, который мы получаем при помощи функции GetModuleHandle, от только что упомянутых величин? Дескриптор приложения, или дескриптор модуля есть величина локальная, т.е. действующая в пределах данного процесса и, как правило, равная адресу загрузки модуля в виртуальное адресное пространство. Дескриптор модуля имеется у любого модуля, загруженного в память, в том числе и у подчиненных DLL-библиотек.

Рассмотрим теперь структуру, на которую указывает 9-й параметр функции CreateProcess. Вот эта структура:

STARTUP STRUC
cb DD 0
lpReserved DD 0
lpDesktop DD 0
lpTitle DD 0
dwX DD 0
dwY DD 0
dwXSize DD 0
dwYSize DD 0
dwXCountChars DD 0
dwYCountChars DD 0
dwFillAttribute DD 0
dwFlags DD 0
wShowWindow DW 0
cbReserved2 DW 0
lpReserved2 DD 0
hStdInput DD 0
hStdOutput DD 0
hStdError DD 0
STARTUP ENDS

Итак, разберем смысл полей этой структуры.
cb - размер данной структуры в байтах. Заполняется обязательно.
lpReserved - резерв, должно быть равно нулю.
lpDesktop - имя рабочего стола (и рабочей станции). Имеет смысл только для Windows NT.
lpTitle - название окна для консольных приложений, создающих свое окно. Для остальных приложений должно быть равно 0.
dwX - координата X левого верхнего угла окна.
dwY - координата Y левого верхнего угла окна.
dwXSize - размер окна по X.
dwYSize - размер окна по Y.
dwXCountChars - размер буфера консоли по X.
dwYCountChars - размер буфера консоли по Y.
dwFillAttribute - начальный цвет текста. Имеет значение только для консольных приложений.
dwFlags - флаг значения полей. Вот значение этого флага.

Макро-значение
флага
Значение
константы
Смысл значения
STARTF_USESHOWWINDOW1hРазрешить поле dwShowWindow
STARTF_USESIZE2hРазрешить dwXSize и dwYSize
STARTF_USEPOSITI0N4hРазрешить dwX и dwY
STARTF_USECOUNTCHARS8hРазрешить dwXCountChars и dwYCountChars
STARTF_USEFILLATTR1BUTE10hРазрешить dwFillAttribute
STARTF_FORCEONFEEDBACK40hВключить возврат курсора
STARTF_FORCEOFFFEEDBACK80hВыключить возврат курсора
STARTF_USESTDHANDLES100hРазрешить hStdInput

wShowWindow - определяет способ отображения окна.
cbReserved2 - резерв, должно быть равно 0.
hStdInput - дескриптор ввода (для консоли).
hStdOutput - дескриптор вывода (для консоли).
hStdError - дескриптор вывода сообщения об ошибке (для консоли).

Следующий пример (Рис. 3.2.1) представляет собой простейший пример создания процесса. В качестве программы, порождающей процесс, взят редактор WINWORD.EXE. Для проверки правильности работы примера Вам придется указать путь к WINWORD на Вашем компьютере (переменная PATH). Обратите внимание на то, что приложение появляется на экране в свернутом виде и как это достигается. Как видите, функция CreateProcess совсем не так уж страшна, как кажется на первый взгляд.

// файл proces.rc
// определение констант
#define WS_SYSMENU 0x00080000L
#define WS_POPUP 0x80000000L
#define DS_3DLOOK 0x0004L
//ресурс - меню
MENUP MENU
{
POPUP "&Запуск Word"
{
MENUITEM "&3апустить", 1
MENUITEM "&Удалить", 2
MENUITEM "Выход из &программы", 3
}
}
// определение диалогового окна
DIAL1 DIALOG 0, 0, 240, 120
STYLE WS_POPUP | WS_SYSMENU | DS_3DLOOK
CAPTION "Пример немодального диалогового окна"
FONT 8, "Arial"
{
}
; файл proces.inc
; константы
STARTF_USESHOWWINDOW equ 1h
SW_SHOWMINIMIZED equ 2h
; сообщение приходит при закрытии окна
WM_CLOSE equ 10h
WM_INITDIALOG equ 110h
WM_COMMAND equ 111h
; прототипы внешних процедур
IFDEF MASM
; для MASM
EXTERN TerminateProcess@8:NEAR
EXTERN CreateProcessA@40:NEAR
EXTERN DialogBoxParamA@20:NEAR
EXTERN EndDialog@8:NEAR
EXTERN MessageBoxA@16:NEAR
EXTERN ExitProcess@4:NEAR
EXTERN GetModuleHandleA@4:NEAR
EXTERN LoadMenuA@8:NEAR
EXTERN SetMenu@8:NEAR
EXTERN TranslateMessage@4:NEAR
ELSE
; для TASM
EXTERN TerminateProcess:NEAR
EXTERN CreateProcessA:NEAR
EXTERN DialogBoxParamA:NEAR
EXTERN EndDialog:NEAR
EXTERN MessageBoxA:NEAR
EXTERN ExitProcess:NEAR
EXTERN GetModuleHandleA:NEAR
EXTERN LoadMenuA:NEAR
EXTERN SetMenu:NEAR
EXTERN TranslateMessage:NEAR
TerminateProcess@8 = TerminateProcess
CreateProcessA@40 = CreateProcessA
DialogBoxParamA@20 = DialogBoxParamA
EndDialog@8 = EndDialog
MessageBoxA@16 = MessageBoxA
ExitProcess@4 = ExitProcess
GetModuleHandleA@4 = GetModuleHandleA
LoadMenuA@8 = LoadMenuA
SetMenu@8 = SetMenu
TranslateMessage@4 = TranslateMessage
ENDIF
; структуры
; структура сообщения
MSGSTRUCT STRUC
MSHWND DD ?
MSMESSAGE DD ?
MSWPARAM DD ?
MSLPARAM DD ?
MSTIME DD ?
MSPT DD ?
MSGSTRUCT ENDS
; структура для CreateProcess
STARTUP STRUC
cb DD 0
lpReserved DD 0
lpDesktop DD 0
lpTitle DD 0
dwX DD 0
dwY DD 0
dwXSize DD 0
dwYSize DD 0
dwXCountChars DD 0
dwYCountChars DD 0
dwFillAttribute DD 0
dwFlags DD 0
wShowWindow DW 0
cbReserved2 DW 0
lpReserved2 DD 0
hStdInput DD 0
hStdOutput DD 0
hStdError DD 0
STARTUP ENDS
; структура - информация о процессе
PROCINF STRUC
hProcess DD ?
hThread DD ?
Idproc DD ?
idThr DD ?
PROCINF ENDS
; файл proces.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
include proces.inc
; директивы компоновщику для подключения библиотек
IFDEF MASM
; директивы MASM
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
ELSE
; директивы TASM
includelib c:\tasm32\lib\import32.lib
ENDIF
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
NEWHWND DD 0
MSG MSGSTRUCT <?>
STRUP STARTUP <?>
INF PROCINF <?>
HINST DD 0 ; дескриптор приложения
PA DB "DIAL1",0
PMENU DB "MENUP",0
PATH DB "C:\Program Files\Office\WINWORD.EXE",0
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
PUSH 0
CALL GetModuleHandleA@4
MOV [HINST], EAX
; создать модальный диалог
PUSH 0
PUSH OFFSET WNDPROC
PUSH 0
PUSH OFFSET PA
PUSH [HINST]
CALL DialogBoxParamA@20
PUSH 0
CALL ExitProcess@4
;-----------------------------------
; процедура окна
; расположение параметров в стеке
; [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_CLOSE
JNE L1
; закрыть диалоговое окно
JMP L5
L1:
; сообщение при инициализации окна
CMP DWORD PTR [EBP+0CH],WM_INITDIALOG
JNE L3
; загрузить меню
PUSH OFFSET PMENU
PUSH [HINST]
CALL LoadMenuA@8
; установить меню
PUSH EAX
PUSH DWORD PTR [EBP+08H]
CALL SetMenu@8
JMP FINISH
; проверяем, не случилось ли чего с пунктами
; меню в диалоговом окне
L3:
CMP DWORD PTR [EBP+0CH],WM_COMMAND
JNE FINISH
CMP WORD PTR [EBP+10H],3
JE L5
CMP WORD PTR [EBP+10H],2
JE L7
CMP WORD PTR [EBP+10H],1
JE L6
JMP FINISH
; закрыть диалоговое окно
L5:
PUSH 0
PUSH DWORD PTR [EBP+08H]
CALL EndDialog@8
JMP FINISH
; запустить программу WORD
L6:
; заполняем структуру для запуска
; окно должно появляться в свернутом виде
MOV STRUP.cb,68
MOV STRUP.lpReserved,0
MOV STRUP.lpDesktop,0
MOV STRUP.lpTitle,0
MOV STRUP.dwFlags, STARTF_USESHOWWINDOW
MOV STRUP.cbReserved2,0
MOV STRUP.lpReserved2,0
MOV STRUP.wShowWindow,SW_SHOWMINIMIZED
; запуск приложения Winword
PUSH OFFSET INF
PUSH OFFSET STRUP
PUSH 0
PUSH 0
PUSH 0
PUSH 0
PUSH 0
PUSH 0
PUSH OFFSET PATH
PUSH 0
CALL CreateProcessA@40
JMP FINISH
; удалить из памяти процесс
L7:
PUSH 0 ; код выхода
PUSH INF.hProcess
CALL TerminateProcess@8
FINISH:
MOV EAX,0
POP EDI
POP ESI
POP EBX
POP EBP
RET 16
WNDPROC ENDP
_TEXT ENDS
END START

Рис. 3.2.1. Пример создания процесса.

Трансляция программы PROCES.ASM на Рис. 3.2.1.
Пакет MASM32.

ML /c /coff /DMASM proces.asm
RC proces.rc
LINK /SUBSYSTEM:WINDOWS proces.obj proces.res
Пакет TASM32.
TASM32 /ml proces.asm
BRCC32 proces.rc
TLINK32 -aa proces.obj,,,,,proces.res

Думаю, что другой комментарий для программы на Рис. 3.2.1 не требуется.


40 В английской терминологии поток называется "THREAD", что буквально переводится как нить.


II

Теперь пришла пора вплотную заняться потоками. Вначале я намерен решить задачу из предыдущей главы (Рис. 3.1.2) при помощи потока.

Поток может быть создан при помощи функции CreateThread. Рассмотрим параметры этой функции.

  • 1-й параметр. Указатель на структуру атрибутов доступа. Имеет значение только для Windows NT. Обычно полагается NULL.
  • 2-й параметр. Размер стека потока. Если параметр равен нулю, то берется размер стека по умолчанию, равный размеру стека родительского потока.
  • 3-й параметр. Указатель на потоковую функцию, с вызова которой начинается исполнение потока.
  • 4-й параметр. Параметр для потоковой функции.
  • 5-й параметр. Флаг, определяющий состояние потока. Если флаг равен 0, то выполнение потока начинается немедленно. Если значение флага потока равно CREATE_SUSPENDED (4H), то поток находится в состояние ожидания и запускается по выполнению функции ResumeThread.
  • 6-й параметр. Указатель на переменную, куда будет помещен дескриптор потока.

Как уже было сказано, выполнение потока начинается с потоковой функции. Окончание работы этой функции приводит к естественному окончанию работы потока. Поток также может закончить свою работу, выполнив функцию ExitThread с указанием кода выхода. Наконец, порождающий поток может закончить работу порожденного потока при помощи функции TerminateThread. В нашем примере на Рис. 3.2.2 запускаемый процесс не может сам закончить свою работу и прекращает свою работу вместе с приложением по команде TerminateThread. Надо сказать, что такое завершение, вообще говоря, является аварийным и не рекомендуется к обычному употреблению. Связано это с тем, что при таком завершении не выполняется никаких действий по освобождению занятых ресурсов (блоки памяти, открытые файлы и т.п.). Поэтому стройте свои приложения так, что бы поток завершался по выходу из потоковой процедуры. Таким образом, в некотором смысле приведенный ниже пример является демонстрацией того, чего не рекомендуется делать.

Вообще идеальной нам кажется ситуация, когда функция окна берет на себя только реакцию на события, происходящие с элементами, а всю трудоемкую работу (сложные вычисления, файловая обработка) должны взять на себя потоки. Кстати, поток может создавать новые потоки, так что в результате может возникнуть целое дерево.

Как я уже сказал, ниже представлена программа, использующая поток для вычисления и вывода в окно редактирования текущей даты и времени. Заметим в этой связи, что если бы такая обработка была реализована в оконной функции, Вы бы сразу почувствовали разницу - окно почти бы перестало реагировать на внешнее воздействие.

// файл thread.rc
// определение констант
#define WS_SYSMENU 0x00080000L
// элементы на окне должны быть изначально видимы
#define WS_VISIBLE 0x10000000L
// бордюр вокруг элемента
#define WS_BORDER 0x00800000L
// при помощи TAB можно по очереди активизировать элементы
#define WS_TABSTOP 0x00010000L
// текст в окне редактирования прижат к левому краю
#define ES_LEFT 0x000OL
// стиль всех элементов на окне
#define WS_CHILD 0x40000000L
// запрещается ввод с клавиатуры
#define ES_READONLY 0x0800L
// стиль - кнопка
#define BS_PUSHBUTTON 0x00000000L
// центрировать текст на кнопке
#define BS_CENTER 0x00000300L

#define DS_3DLOOK 0x0004L // определение диалогового окна DIAL1 DIALOG 0, 0, 240, 100 STYLE WS_SYSMENU | DS_3DLOOK CAPTION "Пример использования потока" FONT 8, "Arial" { // окно редактирования, идентификатор 1 CONTROL "", 1, "edit", ES_LEFT | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_READONLY, 100, 5, 130, 12 // кнопка, идентификатор 2 CONTROL "Выход", 2, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 180, 76, 50, 14 } ; файл thread.inc ; константы ; сообщение приходит при закрытии окна WM_CLOSE equ 10h ; сообщение приходит при создании окна WM_INITDIALOG equ 110h ; сообщение приходит при событии с элементом на окне WM_COMMAND equ 111h ; сообщение посылки текста элементу WM_SETTEXT equ 0Ch ; прототипы внешних процедур IFDEF MASM ; для MASM EXTERN SendMessageA@16:NEAR EXTERN GetDlgItem@8:NEAR EXTERN Sleep@4:NEAR EXTERN TerminateThread@8:NEAR EXTERN CreateThread@24:NEAR EXTERN wsprintfA:NEAR EXTERN GetLocalTime@4:NEAR EXTERN ExitProcess@4:NEAR EXTERN GetModuleHandleA@4:NEAR EXTERN DialogBoxParamA@20:NEAR EXTERN EndDialog@8:NEAR ELSE ; для TASM EXTERN SendMessageA:NEAR EXTERN GetDlgItem:NEAR EXTERN Sleep:NEAR EXTERN TerminateThread:NEAR EXTERN CreateThread:NEAR EXTERN _wsprintfA:NEAR EXTERN GetLocalTime:NEAR EXTERN ExitProcess:NEAR EXTERN GetModuleHandleA:NEAR EXTERN DialogBoxParamA:NEAR EXTERN EndDialog:NEAR SendMessageA@16 = SendMessageA GetDlgItem@8 = GetDlgItem Sleep@4 = Sleep TerminateThread@8 = TerminateThread CreateThread@24 = CreateThread wsprintfA = _wsprintfA GetLocalTime@4 = GetLocalTime ExitProcess@4 = ExitProcess GetModuleHandleA@4 = GetModuleHandleA DialogBoxParamA@20 = DialogBoxParamA EndDialog@8 = EndDialog ENDIF ; структуры ; структура сообщения MSGSTRUCT STRUC MSHWND DD ? MSMESSAGE DD ? MSWPARAM DD ? MSLPARAM DD ? MSTIME DD ? MSPT DD ? MSGSTRUCT ENDS ; структура данных дата-время DAT STRUC year DW ? month DW ? dayweek DW ? day DW ? hour DW ? min DW ? sec DW ? msec DW ? DAT ENDS ; файл thread.asm .386P ; плоская модель .MODEL FLAT, stdcall include thread.inc ; директивы компоновщику для подключения библиотек IFDEF MASM ; для компоновщика LINK.EXE includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib includelib c:\masm32\lib\gdi32.lib ELSE ; для компоновщика TLINK32.EXE includelib c:\tasm32\lib\import32.lib ENDIF ;---------------------------------------------------- ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' MSG MSGSTRUCT <?> HINST DD 0 ; дескриптор приложения PA DB "DIAL1",0 TIM DB "Дата %u/%u/%u Время %u:%u:%u",0 STRCOPY DB 50 DUP (?) DATA DAT <0> HTHR DD ? _DATA ENDS ; сегмент кода _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' START: ; получить дескриптор приложения PUSH 0 CALL GetModuleHandleA@4 MOV [HINST], EAX ; создать диалоговое окно PUSH 0 PUSH OFFSET WNDPROC PUSH 0 PUSH OFFSET PA PUSH [HINST] CALL DialogBoxParamA@20 CMP EAX,-1 JNE KOL ; сообщение об ошибке KOL: ;------------------------------------ PUSH 0 CALL ExitProcess@4 ;------------------------------------ ; процедура окна ; расположение параметров в стеке ; [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_CLOSE JNE L1 L3: ; здесь реакция на закрытие окна ; удалить поток PUSH 0 PUSH HTHR CALL TerminateThread@8 ; закрыть диалог PUSH 0 PUSH DWORD PTR [EBP+08H] CALL EndDialog@8 JMP FINISH L1: CMP DWORD PTR [EBP+0CH],WM_INITDIALOG JNE L2 ; здесь начальная инициализация ; получить дескриптор окна редактирования PUSH 1 PUSH DWORD PTR [EBP+08H] CALL GetDlgItem@8 ; создать поток PUSH OFFSET HTHR ; сюда дескриптор потока PUSH 0 PUSH EAX ; параметр PUSH OFFSET GETTIME ; адрес процедуры PUSH 0 PUSH 0 CALL CreateThread@24 JMP FINISH L2: CMP DWORD PTR [EBP+0CH],WM_COMMAND JNE FINISH ; кнопка выхода? CMP WORD PTR [EBP+10H],2 JE L3 FINISH: POP EDI POP ESI POP EBX POP EBP MOV EAX,0 RET 16 WNDPROC ENDP ; потоковая функция ; [EBP+8] параметр=дескриптор окна редактирования GETTIME PROC PUSH EBP MOV EBP,ESP L0: ; задержка в 1 секунду PUSH 1000 CALL Sleep@4 ; получить локальное время PUSH OFFSET DATA CALL GetLocalTime@4 ; получить строку для вывода даты и времени MOVZX EAX,DATA.sec PUSH EAX MOVZX EAX,DATA.min PUSH EAX MOVZX EAX,DATA.hour PUSH EAX MOVZX EAX,DATA.year PUSH EAX MOVZX EAX,DATA.month PUSH EAX MOVZX EAX,DATA.day PUSH EAX PUSH OFFSET TIM PUSH OFFSET STRCOPY CALL wsprintfA ; отправить строку в окно редактирования PUSH OFFSET STRCOPY PUSH 0 PUSH WM_SETTEXT PUSH DWORD PTR [EBP+08H] CALL SendMessageA@16 JMP L0 ; бесконечный цикл POP EBP RET 4 GETTIME ENDP _TEXT ENDS END START

Рис. 3.2.2. Пример создания потока.

Комментарий к программе на Рис. 3.2.2.

Трансляция программы THREAD.ASM на Рис. 3.2.2.
Пакет MASM32.

ML /c /coff /DMASM thread.asm
RC thread.rc
LINK /SUBSYSTEM:WINDOWS thread.obj thread.res
Пакет TASM32.
TASM32 /ml thread.asm
BRCC32 thread.rc
TLINK32 -aa thread.obj,,,,,thread.res

Прошу читателя взять на вооружение весьма полезную функцию Sleep. Эта функция особенно часто используется именно в потоках, дабы несколько высвободить процессорное время.

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. Пример синхронизации двух потоков посредством критической секции.

Комментарий к программе.
Трансляция программы THREAD2.ASM на Рис. 3.2.3.

Пакет MASM32.

ML /c /coff /DMASM thread2.asm
LINK /SUBSYSTEM:WINDOWS thread2.obj
Пакет TASM32.
TASM32 /ml thread2.asm
TLINK32 -aa thread2.obj

При нажатии левой кнопки мыши начинается вывод текстовой строки. При нажатии правой кнопки мыши - выведенная строка стирается. Флаги FLAG1 и FLAG2 введены для того, чтобы вывод строки и вывод пустой строки можно было производить только один раз.

Для того чтобы несколько замедлить вывод текста, мы вводим задержку (Sleep) в цикл вызова процедуры OUTSTR в каждом потоке.

Обратите внимание, что буквы выводятся в окно в основном парами. Объясняется это тем, что пока один из потоков выводит символ, второй уже ждет разрешения, и, как только первый поток выходит из критической секции, второй поток сразу выводит следующий символ.

После в обоих потоках срабатывает задержка (функция Sleep).

Заключая разговор о критических секциях, отмечу, что это наиболее быстрый способ синхронизации. К минусам данного подхода относится невозможность доступа к секции сразу нескольких потоков, а также отсутствие специальных средств, чтобы подсчитывать число обращений к ресурсу.

Взаимоисключения. Мы не упомянули еще один способ синхронизации, отложив его подробное описание на следующие главы. Здесь же отметим, что этот способ называется "взаимоисключением" или мьютексом (Mutex). Данный способ синхронизации не удобен для работы с потоками, он более пригоден для процессов. Данный объект создается при помощи функции CreateMutex. Все процессы, пытающиеся создать уже созданный объект, получают дескриптор уже существующего, созданного другим процессом объекта "взаимоисключение". Особенность данного объекта прежде всего в том, что им может владеть только один процесс. В документации фирмы Microsoft рекомендуется использовать данный объект для определения, запущено уже данное приложение или нет. Но об этом речь пойдет ниже.


41 К слову сказать, в однозадачной операционной системе MS DOS проблема совместного функционирования резидентных программ стояла весьма остро. Не смотря на то, что программисты, их писавшие, добивались весьма больших успехов, все же одновременная работа нескольких резидентных программ часто приводила к весьма заметным конфликтам.