МІНІСТЕРСТВО ОСВІТИ І НАУКИ УКРАЇНИ
НАЦІОНАЛЬНИЙ УНІВЕРСИТЕТ "ЛЬВІВСЬКА ПОЛІТЕХНІКА"
ІНСТИТУТ ПІСЛЯДИПЛОМНОЇ ОСВІТИ
КАФЕДРА ПРОГРАМНОГО ЗАБЕЗПЕЧЕННЯ
/
ЗВІТ ДО ЛАБОРАТОРНОЇ РОБОТИ №2 на тему:
"Робота з потоками в ОС Windows"
Мета роботи: Ознайомитися з багатопоточністю в ОС Windows. Навчитися працювати з потока-ми, використовуючи WinAPI-функції.
КОРОТКІ ТЕОРЕТИЧНІ ВІДОМОСТІ
Потоком (або завданням) називається фрагмент коду програми, який може виконуватися процесором автономно і незалежно від інших частин коду цього ж додатка, але в рамках одного процесу. Як правило , код потоку представляється в програмі у вигляді окремої процедури (функції). Процесорний час надається кожному потоку незалежно від інших потоків даного процесу і потоків інших процесів . Коли один з потоків процесу блокується (наприклад , для очікування завершення операції вводу-виводу) , активізується інший потік . Таким чином , роздільне виконання потоків дає можливість скористатися перевагами мультипрограммной обробки , і в багатьох випадках скоротити загальний час виконання процесу за рахунок скорочення простоїв процесора.
Потік (thread) в ОС Windows є основним елементом виконання будь-якої програми . Процес Windows може містити кілька незалежних потоків, які поділяють загальний адресний простір та інші ресурси процесу (у простому випадку додаток складається з одного єдиного потоку). Водночас потік є самостійним елементом виконання всередині процесу . Кожен з потоків може мати власний стек і свій рівень пріоритету.
Як відомо , створення процесу в ОС Windows проводиться за допомогою системної функції CreateProcess . Виконання цієї функції призводить до породження потоку , який називають головним потоком процесу (T0 , рис. 1) . Головний потік присутній в будь-якому процесі і часто залишається єдиним . Решта потоки (T1 , T2 і т.д.) можуть створюватися в коді головного потоку по розсуду програміста за допомогою спеціальної функції WIN32 API CreateThread . Потік , який виконав функцію CreateThread називають батьківським потоком по відношенню до створеного ним , а створений потік - дочірнім потоком. Функція CreateThread повертає значення дескриптора створеного потоку. Це значення використовується потім для управління створеним потоком з боку батьківського потоку .
Рис.1 Функції управління процесами і потоками
У Win32 API пропонується цілий ряд функцій для роботи з потоками , включаючи :
• CreateThread - створити і запустити потік ;
• ExitThread - завершити роботу і знищити потік з ініціативи самого потоку ;
• TerminateThread - завершити роботу і знищити дочірній потік ;
• SuspendThread - призупинити ( блокувати ) виконання дочірнього потоку ( потік не отримує квантів процесора ) ;
• ResumeThread - відновити виконання раніше припиненого дочірнього потоку ( деблокування ) ;
• Sleep - призупинити виконання потоку на заданий час ;
• SetThreadPriority - змінити пріоритет дочірнього потоку ;
• GetThreadPriority - визначити пріоритет дочірнього потоку ;
• GetExitCodeThread - отримати код завершення дочірнього потоку ;
• WaitForMultipleObjects - призупинити ( блокувати ) виконання батьківського потоку до завершення одного або декількох дочірніх потоків .
Зверніть увагу , що більша частина функцій доступна тільки з батьківського потоку по відношенню до породженому їм дочірньому і тільки три з них ( CreateThread , ExitThread і Sleep ) можуть бути викликані безпосередньо з дочірнього потоку . Детальний опис найбільш важливих функцій представлено в п.2.
Слід зазначити , що створення багатопоточних програм часто пов'язано з необхідністю координації робіт , виконуваних окремими потоками , особливо , якщо потоки використовують загальні ( що розділяються ) ресурси (наприклад , змінні або файли). У цьому випадку , щоб уникнути помилок у значеннях поділюваних даних , необхідно використовувати спеціальні системні засоби взаимоисключения і синхронізації потоків ( критичні секції , семафори , м'ютекси тощо). Ці засоби дозволяють описати регламент , відповідно до якого потоки зможуть отримувати доступ до загальних ресурсів , дотримуючись принцип черговості .
У даній роботі методи синхронізації потоків не застосовуються (їх вивченню присвячена наступна робота). Однак , при виконанні робочого завдання ( п.4) буде можливість переконатися , до чого призводить ігнорування цього важливого питання.
ФУНКЦІЇ WIN32 API ДЛЯ РОБОТИ З ПОТОКАМИ
Cтворення потоку
HANDLE CreateThread (
LPSECURITY_ATTRIBUTES lpThreadAttributes , / / атрибути
/ / захисту
DWORD dwStackSize , / / розмір стека
LPTHREAD_START_ROUTINE lpStartAddress , / / функція потоку
LPVOID lpThreadParameter , / / параметр функції потоку
DWORD dwCreationFlags , / / параметр запуску потоку
LPDWORD lpThreadID ) ; / / ідентифікатор потоку
Ця функція створює дочірній потік , встановлює його характеристики (атрибути захисту, розмір стека) , вказує на код функції потоку і залежно від значення параметра dwCreationFlags або виробляє його запуск , або зупиняє до спеціального розпорядження . Під функцією потоку розуміють описану в програмі функцію , яка містить код , виконання якого має здійснюватися в рамках даного потоку .
Параметри ( in - вхідні , out - вихідні ) :
lpThreadAttributes - покажчик на структуру SECURITY_ATTRIBUTES , визначальну атрибути захисту для створюваного потоку (in). Рекомендується задавати значення NULL , яке дозволяє використовувати будь-які функції для управління даними потоком.
dwStackSize - розмір стека потоку в байтах (in) . Для використання розміру стека батьківського потоку використовуйте значення 0.
lpStartAddress - покажчик на функцію програми , яку виконуватиме потік (можна задати просто ім'я цієї функції ) ( in ) . Функція потоку приймає як параметр єдиний 32 - розрядний параметр і повертає код завершення типу DWORD . Потік може представити цей параметр як значення типу DWORD або як покажчик . Наприклад , опис функції потоку може виглядати так:
DWORD ThreadFunc ( LPVOID )
lpThreadParameter - покажчик , який передається потоку як параметр і зазвичай інтерпретується їм , як покажчик на деяку структуру ( in ) . За відсутності параметрів слід вказати NULL.
dwCreationFlags - параметр запуску потоку ( in ) . Нульове значення параметра означає , що потік готовий до негайного виконання . Якщо як значення цього параметра вказати константу CREATE_SUSPENDED , то новий потік буде перебувати в стані очікування до тих пір , поки не буде викликана функція ResumeThread .
lpIDThread - покажчик на змінну типу DWORD , в яку буде поміщений ідентифікатор ( системний номер ) створеного потоку ( out ) .
Значення, що повертається : у разі успіху функція CreateThread повертає дескриптор створеного потоку ( тип handle ) , який необхідний для виконання різних операцій над потоком. При помилку функція повертає значення NULL.
Завершення потоку
Потік може завершитися за власною ініціативою або за ініціативою батьківського потоку , формуючи при цьому код завершення .
У першому випадку потік завершується при виконанні оператора повернення з функції потоку ( return ) або за допомогою функції ExitThread :
VOID ExitThread (
DWORD dwExitCode ) ; / / код завершення потоку
В якості єдиного параметра цієї функції задається код завершення потоку .
У другому випадку застосовується функція TerminateThread , за допомогою якої батьківський потік може примусово завершити виконання свого дочірнього потоку :
BOOL TerminateThread (
HANDLE hThread , / / дескриптор потоку
DWORD dwExitCode ) ; / / код завершення потоку
Значення дескриптора потоку визначається за значенням , що повертається функцією CreateThread при створенні потоку .
Відзначимо також , що всі потоки , створені в рамках якого процесу, автоматично завершують своє виконання при завершенні роботи процесу (тобто виконанні функції ExitProcess ) . При цьому звільняються всі значення дескрипторів потоків .
Перевірка стану потоку
Для отримання коду завершення раніше запущеного дочірнього потоку використовується функція GetExitCodeThread :
BOOL GetExitCodeThread (
HANDLE hThread , / / дескриптор потоку
LPDWORD lpdwExitCode ) ; / / адреса для прийому коду
/ / Завершення
Якщо потік , для якого викликана дана функція , все ще працює , замість коду завершення повертається значення STILL_ACTIVE .
Ця функція може бути використана для циклічної перевірки факту завершення роботи дочірнього потоку ( див. приклад у п.3) .
Очікування завершення виконання потоку
Для перекладу батьківського потоку в режим очікування ( блокування ) до моменту завершення декількох запущених їм потоків , доцільно використовувати функцію WaitForMultipleObjects :
DWORD WaitForMultipleObjects (
DWORD cObjects , / / кількість очікуваних потоків
CONST HANDLE * lphObjects , / / адреса масиву
/ / дескрипторів потоків
BOOL fWaitAll , / / тип очікування
DWORD dwTimeout ) ; / / час очікування в мс
Наприклад , якщо запущено три потоку і їх дескриптори представлені у вигляді масиву HANDLE hThread [ 3 ] , то очікування до тих пір , поки всі три потоки не завершаться , можна організувати таким чином:
WaitForMultipleObjects ( 3 , hThreads , TRUE , INFINITE ) ;
Тип очікування TRUE означає очікування завершення всіх потоків ( FALSE - хоча б одного з потоків). Час очікування INFINITE означає нескінченне очікування до настання необхідного події.
Диспетчеризація і керування пріоритетами потоків
В ОС Windows використовується принцип пріоритетною диспетчеризації потоків . Це означає , що кванти процесорного часу частіше виділяються потокам з більш високим пріоритетом. Значення пріоритету встановлюються в діапазоні від 1 до 31 ( 31 відповідає максимальному пріоритету ) . Існують 4 рівня ( класу) пріоритетів , які призначаються процесам при їх створенні (залежно від типу процесу) :
IDLE_PRIORITY_CLASS = 4 - низькопріоритетні процеси ;
NORMAL_PRIORITY_CLASS = 9 - звичайні процеси ;
HIGH_PRIORITY_CLASS = 13 - високопріоритетні процеси ;
REALTIME_PRIORITY_CLASS = 24 - процеси реального часу;
Потоки спочатку отримують таке ж значення пріоритету , як і у процесу . Звичайні користувача процеси (і їх потоки) за замовчуванням отримують значення пріоритету 9 , що відповідає класу NORMAL_PRIORITY_CLASS .
За допомогою функції SetThreadPriority можна змінити відносний пріоритет потоку , але тільки в рамках установленого класу :
BOOL SetThreadPriority (
HANDLE hThread , / / дескриптор потоку
int nPriority ) ;/ / новий рівень пріоритету потоку
Новий рівень пріоритету потоку задається за допомогою спеціальних констант, які встановлюють величину зміни пріоритету потоку щодо пріоритету процесу :
THREAD_PRIORITY_ABOVE_NORMAL +1
THREAD_PRIORITY_HIGHEST +2
THREAD_PRIORITY_NORMAL 0
THREAD_PRIORITY_BELOW_NORMAL -1
THREAD_PRIORITY_LOWEST -2
THREAD_PRIORITY_TIME_CRITICAL = 15 (або = 31 )
Остання із зазначених констант THREAD_PRIORITY_TIME_CRITICAL дозволяє встановити абсолютне значення пріоритету потоку, рівне 31 для процесів класу REALTIME_PRIORITY_CLASS або 15 для решти класів .
У будь-який момент часу можна визначити поточне значення пріоритету потоку c дескриптором hThread за допомогою функції
Int GetThreadPriority ( HANDLE hThread ) ;
Слід зазначити , що операційна система Windows може автоматично змінювати пріоритет потоків залежно від їх поточного стану : збільшувати , коли потік взаємодіє з користувачем або знижувати , коли потік переходить в стан очікування .
Приклад багатопотокової програми
Розглянемо приклад консольної багатопотокової програми , яка створює два дочірніх потоку, виконують рахункові цикли типу for . В якості функцій потоків використовуються функції Thread1Proc і Thread2Proc відповідно. Потоки завершуються за власною ініціативою з використанням оператора return і повертають код завершення . Головний потік після створення дочірніх потоків циклічно перевіряє їх стан за допомогою функції GetExitCodeThread , формує прапори стану потоків flag1 і flag2 і видає відповідні повідомлення ( « потік працює » або « потік завершений »). Програма завершує роботу після того , як обидва дочірніх потоку завершили своє виконання ( flag1 = flag2 = 0).
/ / Приклад багатопотокової програми
# include <windows.h>
# include <conio.h>
# include <iostream.h>
HANDLE hThread1 ; / / дескриптор потоку 1
HANDLE hThread2 ; / / дескриптор потоку 2
DWORD IDThread1 ; / / ідентифікатор потоку 1
DWORD IDThread2 ; / / ідентифікатор потоку 2
DWORD dwExitCode1 ; / / код завершення потоку 1
DWORD dwExitCode2 ; / / код завершення потоку 2
/ / Оголошена функція потоку 1 :
DWORD Thread1Proc ( HWND hwnd1 ) ;
/ / Оголошена функція потоку 2 :
DWORD Thread2Proc ( HWND hwnd2 ) ;
int main ( )
{
cout << " Багаторівнева програма " << endl ;
/ / Створення потоку 1
hThread1 = CreateThread (NULL , 0 , Thread1Proc , NULL , 0 , & IDThread1 ) ;
if ( hThread1 == NULL)
{ Cout << " Помилка при створенні потоку 1 " << endl ;
getch ();
return 0 ;
}
else
cout << " Потік 1 створено; ID = " << IDThread1 << endl ;
/ / Створення потоку 2
hThread2 =
CreateThread (NULL , 0 , Thread2Proc , NULL , 0 , & IDThread2 ) ;
if ( hThread2 == NULL)
{ Cout << " Помилка при створенні потоку 2 " << endl ;
getch ();
return 0 ;
}
else
cout << " Потік 2 створено; ID = " << IDThread2 << endl ;
int flag1 = 1 ; / / прапор стану потоку 1 ( 1 = активний)
int flag2 = 1 ; / / прапор стану потоку 2 ( 1 = активний)
int k = 0 ; / / лічильник циклів перевірки
/ / Циклічна перевірка кодів завершення потоків
/ / З виведенням повідомлень про їх стан
while ( flag1 + flag2 )
{
k + + ;
cout << " Цикл " << k << endl ;
if ( flag1! = 0 )
{
GetExitCodeThread ( hThread1 , & dwExitCode1 ) ;
if ( dwExitCode1 == STILL_ACTIVE )
cout << " Потік 1 поки працює " << endl ;
else
{ cout << " Потік 1 завершено з кодом " << dwExitCode1 << endl ;
flag1 = 0 ;
}
}
if ( flag2! = 0 )
{
GetExitCodeThread ( hThread2 , & dwExitCode2 ) ;
if ( dwExitCode2 == STILL_ACTIVE )
cout << " Потік 2 поки працює " << endl ;
else
{ cout << " Потік 2 завершено з кодом " << dwExitCode2 << endl ;
flag2 = 0 ;
}
}
}
getch ();
return 0 ;
}
/ / Функція потоку 1
DWORD Thread1Proc ( HWND hwnd1 )
{
int i ;
for ( i = 1 ; i < 1500 ; i + +);
return 8 ;
}
/ / Функція потоку 2
DWORD Thread2Proc ( HWND hwnd2 )
{
int i ;
for ( i = 1 ; i < 400 ; i + +);
return 3 ;
}
В результаті виконання цієї програми в консольному вікні з'явиться, наприклад, таке:
Багаторівнева програма
Потік 1 створено; ID = 4294506109
Потік 2 створено; ID = 4294494273
цикл 1
Потік 1 поки працює
Потік 2 поки працює
цикл 2
Потік 1 поки працює
Потік 2 завершено з кодом 3
цикл 3
Потік 1 завершено з кодом 3
Слід зазначити, що, запускаючи дану програму багаторазово, можна отримати інші результати , що відрізняються загальною кількістю циклів перевірки і тривалістю роботи окремих потоків. Це пояснюється різним ступенем завантаження процесора в момент запуску програми (тобто наявністю в даний момент готових до виконання потоків інших процесів, що існують паралельно) .