Студопедия

Главная страница Случайная страница

Разделы сайта

АвтомобилиАстрономияБиологияГеографияДом и садДругие языкиДругоеИнформатикаИсторияКультураЛитератураЛогикаМатематикаМедицинаМеталлургияМеханикаОбразованиеОхрана трудаПедагогикаПолитикаПравоПсихологияРелигияРиторикаСоциологияСпортСтроительствоТехнологияТуризмФизикаФилософияФинансыХимияЧерчениеЭкологияЭкономикаЭлектроника






Визначення потоку






Потоком у Windows називається об’єкт ядра, якому ОС виділяє процесорний час для виконання застосування. Кожному потоку належать наступні ресурси:

 

Ø код виконуваної функції;

Ø набір регістрів процесора;

Ø стек для роботи застосування;

Ø стек для роботи операційної системи;

Ø маркер доступу, який містить інформацію для системи безпеки.

 

Усі ці ресурси утворюють контекст потоку у Windows. Окрім дескриптора кожен потік у Windows також має свій ідентифікатор, який унікальний для потоків тих, які виконуються в системі. Ідентифікатори потоків використовуються службовими програмами, які дозволяють користувачам системи відстежувати роботу потоків.

В операційних системах Windows розрізняються потоки двох типів:

 

Ø системні потоки;

Ø потоки користувача.

 

Системні потоки виконують різні сервіси ОС і запускаються ядром ОС.

Потоки користувача служать для вирішення завдань користувача і запускаються застосуванням. На рис. 2.1 показана діаграма станів потоку, який працює в середовищі ОС Windows 2000.

У працюючому застосуванні розрізняються потоки двох типів:

 

Ø робочі потоки (working threads);

Ø потоки інтерфейсу користувача (user interface threads).

 
 

 


Рис. 2.1. Модель стану потоку у Windows 2000

 

Робочі потоки виконують різні фонові завдання в застосуванні. Потоки інтерфейсу користувача пов’язані з вікнами і виконують обробку повідомлень, які надходять цим вікнам. Кожне застосування має, у крайньому випадку, один потік, який називається первинним (primary) або головним (main) потоком. У консольних застосуваннях це потік, який виконує функцію main. У застосуваннях з графічним інтерфейсом це потік який виконує функцію WinMain.

 

2.2. Створення потоків

Створюється потік функцією CreateThread, яка має наступний прототип:

 

HANDLE Createthread(

Lpsecurity_attributes lpThreadAttributes, // атрибути захисту

DWORD dwStackSize, // розмір стека потоку в байтах

Lpthread_start_routine IpStartAddress, // адреса функції

LPVOID lpParameter, // адреса параметра

DWORD dwCreationFlags, // прапорці створення потоку

LPDWORD lpThreadId // ідентифікатор потоку

)

 

При успішному завершенні функція CreateThread повертає дескриптор створеного потоку та його ідентифікатор, який є унікальним для всієї системи. Інакше ця функція повертає значення null. Стисло опишемо призначення параметрів функції CreateThread.

Параметр lpThreadAttributes встановлює атрибути захисту створюваного потоку. До тих пір, поки ми не вивчимо систему безпеки у Windows, ми встановлюватимемо значення цього параметра в null при виклику майже всіх функцій ядра Windows. У даному випадку це означає, що ОС сама встановить атрибути захисту потоку, використовуючи налаштування за замовчуванням. З процесами ми познайомилися в роботі 1.

Параметр dwStackSize визначає розмір стека, який виділяється потоку при запуску. Якщо цей параметр дорівнює нулю, то потоку виділяється стек розмір якого за замовчуванням дорівнює 1 Мбайт. Це найменший розмір стека, який може бути виділений потоку. Якщо величина параметра dwStackSize менше значення, заданого за замовчуванням, то все одно потоку виділяється стек розміром в 1 Мбайт. ОС Windows округляє розмір стека до однієї сторінки пам’яті, який зазвичай дорівнює 4 Кбайт.

Параметр IpStartAddress вказує на виконувану потоком функцію. Ця функція повинна мати наступний прототип:

 

DWORD WINAPI ім’я_функції_потоку(LPVOID lpParameters);

 

Видно, що функції потоку може бути переданий єдиний параметр lpParameter, який є покажчиком на порожній тип. Це обмеження випливає з того, що функція потоку викликається ОС, а не прикладною програмою. Програми ОС є виконуваними модулями і тому вони повинні викликати тільки функції, сигнатура яких заздалегідь визначена. Тому для потоків визначили найпростіший список параметрів, який містить тільки покажчик. Через те, що функції потоків викликаються ОС, то вони також отримали назву функції зворотного виклику.

Параметр dwCreationFiags визначає, в якому стані буде створений потік. Якщо значення цього параметра дорівнює 0, то функція потоку починає виконуватися відразу після створення потоку. Якщо ж значення цього параметра рівне create_suspended, то потік створюється в підвішеному стані. Потім цей потік можна запустити викликом функції ResumeThread.

Параметр lpThreadId є вихідним, тобто його значення встановлює Windows. Цей параметр повинен вказувати на змінну, в яку Windows помістить ідентифікатор потоку. Цей ідентифікатор унікальний для всієї системи і може надалі використовуватися для посилань на потік. Ідентифікатор потоку головним чином використовується системними функціями і рідко функціями застосування. Дійсний ідентифікатор потоку тільки на час існування потоку. Після завершення потоку той же ідентифікатор може бути присвоєний іншому потоку. В ОС Windows 98 цей параметр не може бути рівний null. У Windows NT і 2000 допускається встановити його значення в null - тоді ОС не поверне ідентифікатор потоку.

 

У лістингу 2.1 приведений приклад програми, яка використовує функцію CreateThread для створення потоку і демонструє спосіб передачі параметрів виконуваної потоком функції.

 

Лістинг 2.1. Створення потоку функцією CreateThread

 

#include " stdafx.h"

#include < windows.h>

#include < conio.h>

#include < iostream>

 

using namespace std;

volatile int n;

 

DWORD WINAPI Add(LPVOID iNum)

{

cout < < " Thread is started." < < endl;

n += (int)iNum;

cout < < " Thread is finished." < < endl;

return 0;

}

int main()

{

int inc = 10;

HANDLE hThread;

DWORD IDThread;

 

cout < < " n = " < < n < < endl;

 

// запускаємо потік Add

hThread = CreateThread(NULL, 0, Add, (void*)inc, 0, & IDThread);

if (hThread == NULL)

return GetLastError();

 

// чекаємо, поки потік Add завершить роботу

WaitForSingleObject(hThread, INFINITE);

 

// закриваємо дескриптор потоку Add

CloseHandle(hThread);

 

cout < < " n = " < < n < < endl;

_getch();

return 0;

}

 

Відзначимо, що в програмі використовується функція WaitForSingleObject(), яка чекає завершення потоку Add. Детально ця функція буде розглянута далі, в розділі, присвяченому об’єктам синхронізації і функціям очікування.

Для створення потоків можна також використовувати макрокоманду _beginthreadex, яка описана у файлі заголовка process.h і має ті ж параметри, що і функція CreateThread(). Як cтверджує Джеффрі Ріхтер в своїй книзі " Програмування застосувань для Windows" використання цієї макрокоманди надійніше, ніж безпосередній виклик функції CreateThread().

У лістингу 2.2 приведений приклад програми, яка використовує макрокоманду _beginthreadex для створення потоку і демонструє спосіб передачі параметрів виконуваній потоком функції.

 

Лістинг 2.2. Створення потоку макрокомандою _beginthreadex

 

#include " stdafx.h"

#include < windows.h>

#include < iostream>

#include < conio.h>

#include < string.h>

#include < process.h>

 

using namespace std;

 

UINT WINAPI thread(void *pString)

{

int i = 1;

char *pLexema;

pLexema = strtok((char*) pString, " ");

while (pLexema! = NULL)

{

cout < < " Thread find the lexema " < < i < < ": " < < pLexema < < endl;

pLexema = strtok(NULL, " ");

i++;

}

return 0;

}

 

int main()

{

char sentence[80];

int i, j, k = 0;

HANDLE hThread;

UINT IDThread;

 

cout < < " Input string: ";

cin.getline(sentence, 80);

j = strlen(sentence);

 

// створюємо потік для підрахунку лексем

hThread = (HANDLE)

_beginthreadex(NULL, 0, thread, sentence, 0, & IDThread);

if (hThread == NULL)

return GetLastError();

 

// самі підраховуємо число букв " а" в рядку

for (i=0; i< j; i++)

if (sentence[i] == 'a')

k++;

cout < < " Number of symbols 'a' in the string = " < < k < < endl;

 

// чекаємо завершення розкладання на лексеми

WaitForSingleObject(hThread, INFINITE);

 

// закриваємо дескриптор потоку thread

CloseHandle(hThread);

_getch();

return 0;

}

 

2.3. Завершення потоків

Потік завершується викликом функції ExitThread(), яка має наступний прототип:

 

VOID ExitThread(

DWORD dwExitCode // код завершення потоку

);

Ця функція може викликатися як явно, так і неявно при поверненні значення з функції потоку. При виконанні цієї функції система посилає динамічним бібліотекам, які завантажені процесом, повідомлення dll_thread_detach, яке говорить про те, що потік завершує свою роботу.

Якщо потік створюється за допомогою макрокоманди _beginthreadex, то для завершення потоку потрібно використовувати макрокоманду _endthreadex єдиним параметром якої є код повернення з потоку. Ця макрокоманда описана у файлі заголовку process.h. Причина використання в цьому випадку макрокоманди _endthreadex полягає в тому, що вона не тільки виконує вихід з потоку, але і звільняє пам’ять, яка була розподілена макрокомандою _beginthreadex. Якщо потік створений функцією _beginthreadex, то для виходу з потоку функція _endthreadex може викликатися як явно, так і неявно при поверненні значення з функції потоку.

Один потік може завершити інший потік, викликавши функцію TerminateThread(), яка має наступний прототип:

 

BOOL Terminatethread(

HANDLE hThread, // дескриптор потоку

DWORD dwExitThread // код завершення потоку

);

 

У разі успішного завершення функція TerminateThread() повертає ненульове значення, інакше - FALSE. Функція TerminateThread() завершує потік, але не звільняє всі ресурси, які належать цьому потоку. Це відбувається тому, що при виконанні цієї функції система не надсилає динамічним бібліотекам, завантаженим процесом повідомлення про те, що потік завершує свою роботу. У результаті динамічна бібліотека не звільняє ресурси, які були захоплені для роботи з цим потоком. Тому ця функція повинна викликатися тільки в аварійних ситуаціях при зависанні потоку.

У лістингу 2.3 приведена програма, яка демонструє роботу функції TerminateThread(). У цій програмі потрібно звернути увагу на кваліфікатор типу volatile, який вказує компілятору, що значення змінної count повинно зберігатися в пам’яті, бо до цієї змінної мають доступ паралельні потоки. Річ у тому, що сам компілятор мови програмування C або C++ не знає, що таке потік. Для нього це просто функція. А в мовах програмування C і C++ будь-яка функція викликається тільки синхронно, тобто функція, яка викликала іншу функцію, чекає завершення цієї функції. Якщо не використовувати кваліфікатор volatile, то компілятор може оптимізувати код і в одному потоці зберігати значення змінної в регістрі, а в іншому потоці - в оперативній пам’яті.

В результаті паралельно працюючі потоки звертатимуться до різних змінних.

 

Лістинг 2.3. Завершення потоку функцією TerminateThread()

 

#include " stdafx.h"

#include < windows.h>

#include < iostream>

 

using namespace std;

 

volatile UINT count; /* кваліфікатор типу volatile, вказує компілятору, що значення змінної count

повинно зберігатися в пам’яті, бо до цієї змінної мають доступ паралельні потоки.

Компілятор мови програмування C або C++ не знає, що таке потік. Для нього це

просто функція. А в мовах програмування C і C++ будь-яка функція викликається тільки

синхронно, т.т. функція, яка викликала іншу функцію, чекає завершення цієї

функції. Якщо не використовувати кваліфікатор volatile, то компілятор може

оптимізувати код і в одному потоці зберігати значення змінної в регістрі, а в іншому

потоці - в оперативній пам’яті. */

 

void thread()

{

for (;;)

{

++count;

Sleep(100); // трохи зачекаємо

}

}

 

int main()

{

HANDLE hThread;

DWORD IDThread;

char c;

hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)thread, NULL, 0, & IDThread);

if (hThread == NULL)

return GetLastError();

for (;;)

{

cout < < " Input 'y' to display the count or any char to finish: ";

cin > > c;

if (c == 'y')

cout < < " count = " < < count < < endl;

else

break;

}

 

// перериваємо виконання потоку thread

TerminateThread(hThread, 0);

 

// закриваємо дескриптор потоку

CloseHandle(hThread);

 

return 0;

}

 

2.4. Припинення і відновлення потоків

Кожен створений потік має лічильник призупинень, максимальне значення якого рівне maximum_suspend_count. Лічильник призупинень показує, скільки разів виконання потоку було припинене. Потік може виконуватися тільки за умови, що значення лічильника призупинень дорівнює нулю. Інакше потік не виконується або, як то кажуть знаходиться в підвішеному стані. Виконання кожного потоку може бути призупинено викликом функції SuspendThread(), яка має наступний прототип:

 

DWORD SuspendThread(

HANDLE hThread // дескриптор потоку

);

 

Ця функція збільшує значення лічильника призупинень на 1 і, при успішному завершенні, повертає поточне значення цього лічильника. У випадку невдачі функція SuspendThread() повертає значення, рівне -1.

Відзначимо, що потік може припинити також і сам себе. Для цього він повинен передати функції SuspendThread() свій псевдодескриптор, який можна отримати за допомогою функції GetCurrentThread(). Докладніше псевдодескриптори потоків будуть розглянуті в п. 2.5.

Для відновлення виконання потоку використовується функція ResumeThread(), яка має наступний прототип:

 

DWORD ResumeThread(

HANDLE hThread // дескриптор потоку

)

 

Функція ResumeThread() зменшує значення лічильника призупинень на 1 за умови, що це значення було більше нуля. Якщо отримане значення лічильника призупинень дорівнює 0, то виконання потоку поновлюється, інакше потік залишається в підвішеному стані. Якщо при виклику функції ResumeThread() значення лічильника призупинень було рівним 0, то це означає, що потік не знаходиться в підвішеному стані. У цьому випадку функція не виконує ніяких дій. При успішному завершенні функція ResumeThread() повертає поточне значення лічильника призупинень, інакше - значення -1.

Потік може затримати своє виконання викликом функції Sleep(), яка має наступний прототип:

 

VOID Sleep(

DWORD dwMilliseconds // мілісекунди

);

 

Єдиний параметр функції Sleep() визначає число мілісекунд, на які потік, який викликав цю функцію, припиняє своє виконання. Якщо значення цього параметра дорівнює 0, то виконання потоку просто переривається, а потім поновлюється за умови, що немає інших потоків, які чекають виділення процесорного часу. Якщо ж значення цього параметра рівне infinite, тo потік припиняє своє виконання назавжди, що приводить до блокування роботи застосування.

У лістингу 3.4 приведена програма, яка демонструє роботу функцій SuspendThread(), ResumeThread() і Sleep().

 

Лістинг 2.4. Приклад роботи функцій SuspendThread(), ResumeThread() і Sleep()

 

#include " stdafx.h"

#include < windows.h>

#include < iostream>

 

using namespace std;

volatile UINT nCount;

volatile DWORD dwCount;

 

void thread()

{

for (;;)

{

nCount++;

Sleep(100); // призупиняємо потік на 100 мілісекунд

}

}

 

int main()

{

HANDLE hThread;

DWORD IDThread;

char c;

 

hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)thread, NULL, 0, & IDThread);

if (hThread == NULL)

return GetLastError();

 

for (;;)

{

cout < < " Input: " < < endl;

cout < < " \t'n' to exit" < < endl;

cout < < " \t'y' to display the count" < < endl;

cout < < " \t's' to suspend thread" < < endl;

cout < < " \t'r' to resume thread" < < endl;

cin > > c;

 

if (c == 'n')

break;

switch (c)

{

case 'y':

cout < < " count = " < < nCount < < endl;

break;

case 's':

// призупиняємо потік thread

dwCount = SuspendThread(hThread);

cout < < " Thread suspend count = " < < dwCount < < endl;

break;

case 'r':

// відновлюємо потік thread

dwCount = ResumeThread(hThread);

cout < < " Thread suspend count = " < < dwCount < < endl;

break;

}

}

// перериваємо виконання потоку thread

TerminateThread(hThread, 0);

 

// закриваємо дескриптор потоку thread

CloseHandle(hThread);

return 0;

}

 

2.5. Псевдодескриптори потоків

Іноді потоку потрібно знати свій дескриптор, щоб змінити якісь свої характеристики. Наприклад, потік може змінити свій пріоритет.

Для цих цілей в Win32 API існує функція GetCurrentThread(), яка має наступний прототип:

 

HANLDE GetCurrentThread(VOID);

 

і повертає псевдодескриптор поточного потоку. Псевдодескриптор поточного потоку відрізняється від справжнього дескриптора потоку тим, що він може використовуватися тільки самим поточним потоком і, отже, може успадковуватися іншими процесами. Псевдодескриптор потоку не потрібно закривати після його використання. З псевдодескриптора потоку можна отримати справжній дескриптор потоку, для цього псевдодескриптор потрібно продублювати, викликавши функцію DuplicateHandle(). Докладне дублювання дескрипторів розглядається в розділі 1.

У лістингу 3.5 приведений приклад програми, яка викликає функцію GetCurrentThread(), а потім виводить на консоль отриманий псевдодескриптор.

 

Лістинг 2.5. Приклад роботи функції GetCurrentThread()

 

#include " stdafx.h"

#include < windows.h>

#include < iostream>

#include < conio.h>

 

using namespace std;

 

int main()

{

HANDLE hThread;

 

// отримуємо псевдодескриптор поточного потоку

hThread = GetCurrentThread();

 

// виводимо псевдодескриптор на консоль

cout < < hThread < < endl;

 

// cin.get();

_getch();

return 0;

}

 






© 2023 :: MyLektsii.ru :: Мои Лекции
Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав.
Копирование текстов разрешено только с указанием индексируемой ссылки на источник.