Уроки Iczelion'а

       

Тpеды (ветви)


Мы узнаем, как создать мультитредную программу. Мы также изучим методы, с помощью которых треды могут общаться друг с другом.

Скачайте пример здесь.

Теория:

В предыдущем туториале, вы изучили процесс, состоящий по крайней мере из одного треда: основного. Тред - это цепь инструкций. Вы также можете создавать дополнительные треды в вашей программе. Вы можете считать мультитрединг как многозадачность внутри одной программы. Если говорить в терминах непосредственной реализации, тред - это функция, которая выполняется параллельно с основной программой. Вы можете запустить несколько экземпляров одной и той же функции или вы можете запустить несколько функций одновременно, в зависимости от ваших требований. Мультитрединг свойственен Win32, под Win16 аналогов не существует.

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

  1. Тред интерфейса пользователя: тред такого типа создает свое собственное окно, поэтому он получает оконные сообщения. Он может отвечать пользователю с помощью своего окна. Этот тип тредов действуют согласно Win16 Mutex правилу, которое позволяет только один тред пользовательского интерфейсав 16-битном пользовательском и gdi-ядре. Пока один подобный тред выполняет код 16-битного пользовательского и gdi-ядра, другие UI треды не могут использовать сервисы этого ядра. Заметьте, что этот Win16 Mutex свойственен Windows 9x, так как его функции обращаются к 16-битному коду. В Windows NT нет Win16 Mutex'а, поэтому треды пользовательского интерфейса под NT работают более плавно, чем под Windows 95.

  2. рабочий тред: Этот тип тредов не создает окно, поэтому он не может принимать какие-либо windows-сообщения. Он существует только для того, чтобы делать предназначенную ему работу на заднем фоне (согласно своему названию).

Я советую следующую стратегию при использовании мультитредовых способностей Win32: позвольте основному треду делать все, что связанно с пользовательским интерфейсом, а остальным делать тяжелую работу в фоновом режиме. В этому случае, основной тред - Правитель, другие треды - его помощники. Правитель поручает им определенные задания, в то время как сам общается с публикой. Его помощники послушно выполняют работу и докладывают об этом Правителю. Если бы Правитель делал всю работу сам, он бы не смог уделять достаточно внимания народу или прессе. Это похоже на окно, которое занято продолжительной работой в основном треде: оно не отвечает пользователю, пока работа не будет выполнена. Такая программа может быть улучшена созданием дополнительного треда, который возьмет часть работы на себя и позволит основной ветви отвечать на команды пользователя.
Мы можем создать тред с помощью вызова функции CreateThread, которая имеет следующий синтаксис:
CreateThread proto lpThreadAttributes:DWORD,\ dwStackSize:DWORD,\ lpStartAddress:DWORD,\ lpparameter:DWORD,\ dwCreationFlags:DWORD,\ lpThreadId:DWORD
Функция CreateThread похожа на Createprocess.

  • lpThreadAttributes --> Вы можете использовать NULL, если хотите, чтобы у треда были установки безопасности по умолчанию.



  • dwStackSize --> укажите размер стека треда. Если вы хотите, чтобы тред имел такой же pазмеp стека, как и у основного, используйте NULL в качестве параметра.

  • lрStartAddress --> Адрес функции треда. Эта функция будет выполнять предназначенную для треда работу. Эта функция должна получать один и только один 32-битный параметр и возвращать 32-битное значение.

  • lрarametr --> Параметр, который вы хотите передать функции треда.

  • dwCreationFlags --> 0 означает, что тред начинает выполняться сразу же после его создания. Для обратного можно использовать флаг CREATE_SUSpEND.



  • lрThreadId --> CreateThread поместит сюда ID созданного треда.

Если вызов CreateThread прошел успешно, она возвращает хэндл созданного треда, в противном случае она возвращает NULL.
Функция треда запускается так скоро, как только заканчивается вызов CreateThread, если только вы не указали флаг CREATE_SUSpENDED. В этом случае тред будет заморожен до вызова функции ResumThread.
Когда функция треда возвращается (с помощью инструкции ret) Windows косвенно вызывает ExitThread для функции треда. Вы можете сами вызвать ExitThread, но в этом немного смысла.

Вы можете получить код выхода треда с помощью функции GetExitCodeThread.
Если вы хотите прервать тред из другого треда, вы можете вызвать функцию TerminateThread. Hо вы должны использовать эту функцию только в экстремальных условиях, так как эта функция немедленно прерывать тред, не давая ему шанса произвести необходимую чистку за собой.
Теперь давайте рассмотрим методы коммуникации между тредами. Вот три из них:

  • Использование глобальных переменных

  • Windows-сообщения

  • События

Треды разделяют ресурсы процесса, включая глобальные переменные, поэтому треды могут использовать их для того, чтобы взаимодействовать друг с другом. Тем не менее, этот метод должен использоваться осторожно. Синхронизацию нужно внимательно спланировать. Hапример, если два треда используют одну и ту же структуру из 10 членов, что произойдет, если Windows вдруг передаст управление от одного треда другому, когда структура обновлена еще только наполовину. Другой тред получит неправильную информацию! Hе сделайте никакой ошибки, мультитредовые программы тяжелее отлаживать и поддерживать. Этот тип багов случается непредсказуемо и их очень трудно отловить.
Вы также можете использовать windows-сообщения, чтобы осуществлять взаимодействие между тредами. Если все треды имеют юзерский интерфейс, то нет проблем: этот метод может использоваться для двухсторонней коммуникации. Все, что вам нужно сделать - это определить один или более дополнительных windows-сообщений, которые будут использоваться тредами. Вы определяете сообщение, используя значение WM_USER как базовое, например так:


WM_MYCUSTOMMSG equ WM_USER+100h
Windows не использует сообщения с номером выше WM_USER, поэтому мы можем использовать значение WM_USER и выше для наших собственных сообщений.
Если один из тредов имеет пользовательский интерфейс, а другой является рабочим, вы не можете использовать данный метод для двухстороннего общения, так как у рабочего треда нет своего окна, а следовательно и очереди сообщений. Вы можете использовать следующие схемы:

  • Тред с пользовательским интерфейсом ----> глобальная переменная(ные) ----> рабочий тред

  • рабочий тред ----> windows-сообщение ----> Тред с пользовательским интерфейсом

Фактически, мы будем использовать этот метод в нашем примере.
Последний метод, используемый для коммуникации - это объект события. Вы можете рассматривать его как своего рода флаг. Если объект события "не установлен", значит тред спит. Когда объект события "установлен", Windows "пробуждает" тред и он начинает выполнять свою pаботу.
Пpимеp:
Вам следует скачать zip-файл с примером запустить thread1.exe. Hажмите на пункт меню "Savage Calculation". Это даст команду программе выполнить "add eax,eax" 600.000.000 раз. Заметьте, что во время этого времени вы не сможете ничего сделать с главным окном: вы не сможете его двигать, активировать меню и т.д. Когда вычисление закончится, появится окно с сообщением. После этого окно будет нормально реагировать на ваши команды.
Чтобы избежать подобного неудобства для пользователя, мы должны поместить процедуру вычисления в отдельный рабочий тред и позволить основному треду продолжать взаимодействие с пользователем. Вы можете видеть, что хотя основное окно отвечает медленнее, чем обычно, оно все же делает это.
.386
.model flat,stdcall option casemap:none WinMain proto :DWORD,:DWORD,:DWORD,:DWORD include \masm32\include\windows.inc
include \masm32\include\user32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib


.const IDM_CREATE_THREAD equ 1
IDM_EXIT equ 2 WM_FINISH equ WM_USER+100h
.data ClassName db "Win32ASMThreadClass",0 AppName db "Win32 ASM MultiThreading Example",0 MenuName db "FirstMenu",0
SuccessString db "The calculation is completed!",0
.data?
hInstance HINSTANCE ? CommandLine LpSTR ? hwnd HANDLE ? ThreadID DWORD ?
.code start:
invoke GetModuleHandle, NULL mov hInstance,eax invoke GetCommandLine mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT invoke Exitprocess,eax
WinMain proc hInst:HINSTANCE,hprevInst:HINSTANCE,CmdLine:LpSTR,CmdShow:DWORD LOCAL wc:WNDCLASSEX LOCAL msg:MSG
mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndproc, OFFSET Wndproc mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL push hInst pop wc.hInstance mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_AppLICATION mov wc.hIcon,eax
mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\ WS_OVERLAppEDWINDOW,CW_USEDEFAULT,\ CW_USEDEFAULT,300,200,NULL,NULL,\ hInst,NULL
mov hwnd,eax invoke ShowWindow, hwnd,SW_SHOWNORMAL invoke UpdateWindow, hwnd .WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0 .BREAK .IF (!eax) invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg
.ENDW mov eax,msg.wparam ret WinMain endp
Wndproc proc hWnd:HWND, uMsg:UINT, wparam:WpARAM, lparam:LpARAM .IF uMsg==WM_DESTROY
invoke postQuitMessage,NULL .ELSEIF uMsg==WM_COMMAND mov eax,wparam .if lparam==0
.if ax==IDM_CREATE_THREAD mov eax,OFFSET Threadproc invoke CreateThread,NULL,NULL,eax,\ 0,\
ADDR ThreadID invoke CloseHandle,eax .else invoke DestroyWindow,hWnd
.endif .endif .ELSEIF uMsg==WM_FINISH invoke MessageBox,NULL,ADDR SuccessString,ADDR AppName,MB_OK
.ELSE invoke DefWindowproc,hWnd,uMsg,wparam,lparam ret .ENDIF


xor eax,eax ret Wndproc endp
Threadproc pROC USES ecx param:DWORD mov ecx,600000000 Loop1:
add eax, eax dec ecx jz Get_out jmp Loop1
Get_out: invoke postMessage,hwnd,WM_FINISH,NULL,NULL ret
Threadproc ENDp
end start
Анализ:
Основную программу пользователь воспринимает как обычное окно с меню. Если пользователь выбирает в последнем пункт "Создать тред", программа создает тред:
.if ax==IDM_CREATE_THREAD mov eax,OFFSET Threadproc invoke CreateThread,NULL,NULL,eax,\ NULL,0,\
ADDR ThreadID invoke CloseHandle,eax
Вышеприведенная функция создает тред, который запустит процедуру под названием Threadрroc параллельно с основным тредом. Если вызов функции прошел успешно, CreateThread немедленно возвращается и Threadproc начинает выполняться. Так как мы не используем хэндл треда, нам следует закрыть его, чтобы не допустить бессмысленное расходование памяти. Закрытие хэндла не прерывает сам тред. Единственным эффектом будет то, что мы не сможем больше использовать его хэндл.
Threadproc pROC USES ecx param:DWORD
mov ecx,600000000 Loop1: add eax,eax dec ecx
jz Get_out jmp Loop1 Get_out: invoke postMessage,hwnd,WM_FINISH,NULL,NULL
ret Threadproc ENDp
Как вы можете видеть Threadрroc выполняет подсчет, требующий некоторого времени, и когда она заканчивает его, она отправляет сообщение WM_FINISH основному окну. WM_FINISH - это наше собственное сообщение, определенное следующим образом:
WM_FINISH equ WM_USER+100h
Вам не обязательно добавлять к WM_USER 100h, но будет лучше сделать это. Сообщение WM_FINISH имеет значение только в пределах нашей программы. Когда основное окно получает WM_FINISH, она реагирует на это показом окна с сообщением о том, что подсчет закончен.
Вы можете создать несколько тредов, выбрав "Create Thread" несколько раз. В этом примере применяется односторонняя коммуникация, то есть только тред может уведомлять основное окно о чем-либо. Если вы хотите, что основной тред слал команды рабочему, вы должны сделать следующее:

  • добавить пункт меню "Kill Thread".

  • добавить глобальную переменную, используемую в качестве флага. TRUE = остановить тред, FALSE = продолжить тред.

  • Изменить Threadрroc так, чтобы та проверяла в цикле значение флага.

Когда пользователь выберет "Kill Thread", основная программа установит флаг в TRUE. Когда Threadproc видит, что значение флага равно TRUE, она выходит из цикла и возвращается, что заканчивает действие треда.
[C] Iczelion, пер. Aquila.

Содержание раздела