Напомним, что при вытесняющей многозадачности потоки выполняются попеременно, время процессора выделяется потокам квантами ( около 19 мс ). ОС вытесняет поток, когда истечет его квант или когда на очереди поток с большим приоритетом. Приоритеты постоянно пересчитываются, чтобы избежать монополизации процессора одним потоком.
Создание и работа с потоками
Каждый поток начинает свое выполнение с некоторой входной функции. У функции должен быть следующий прототип:
DWORD WINAPI ThreadProc(PVOID pPararn);
Функция потока может выполнять абсолютно любые задачи. Ниже приведена пустая функция, которая ничего не делает.
DWORD WINAPI ThreadProc(PVOID pPararn);
{
return 0;
}
Когда эта функция закончит выполнение – поток автоматически завершится. В этот момент система выполняет следующие действия:
Когда счетчик объекта ядра обнуляется – система его удаляет. Получается, что объект ядра может жить дольше, чем сам поток. Это сделано для того, чтобы остальные части программы могли получать доступ к информации о потоке, даже если его уже не существует. Например, если надо узнать код завершения потока.
Функция потока всегда должна возвращать значение. Именно оно будет использоваться как код завершения потока.
При разработке потоковой функции надо всегда стараться обходиться своими локальными переменными, либо параметрами. Никто не запрещает использовать доступ к глобальным переменным, вызывать статические методы классов или пользоваться указателями/ссылками на внешние объекты. Однако в этом случае надо выполнять дополнительные действия по синхронизации потоков. Об этом вы сможете прочитать в одной из следующих статей.
Итак, у нас есть потоковая функция. Давайте заставим систему создать для нас поток, который выполнит эту функцию.
Создание потока
Создание потока в Windows происходит с помощью вызова API фукнции:
HANDLE CreateThread(PSECURITY_ATTRIBUTES psa, DWORD cbStack,
PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam, DWORD tdwCreate, PDWORD pdwThreadID);
Вызов этой функции создает объект ядра “поток” и возвращает его дескриптор. Система выделяет память под стек нового потока из адресного пространства процесса, инициализирует структуры данных потока и передает управление потоковой функции. Новый поток выполняется в контексте того же процесса, что и родительский поток. Поэтому он имеет доступ ко всем дескрипторам процесса, адресному пространству. Поэтому все потоки могут легко взаимодействовать друг с другом.
Параметры функции CreateThread следующие:
Внимание, не передавайте сюда указатель на локальные переменные! Т.к. родительский поток работает одновременно с новым – локальные переменные могут выйти из области видимости и разрушиться компилятором. В то время, как новый поток будет пытаться получить к ним доступ.
Завершение потока
Поток может завершиться в следующих случаях:
Функцию потока следует проектировать так, чтобы поток завершался только после того, как она возвращает управление. Это единственный способ, гарантирующий корректную очистку всех ресурсов, принадлежавших Вашему потоку. При этом:
Вызов ExitThread выполняет аналогичные действия, за исключением первого пункта. Поэтому могут быть проблемы.
Завершение потока принудительным образом извне (TerminateThread, завершение процесса) может вызвать проблемы не только с корректным освобождением ресурсов, но и с логикой работы программы. Например, “убиенный” поток не освободит доступ к занятым ресурсам и объектам синхронизации. В результате остальная часть программы может повести себя непредсказуемым образом.
Пример 1
В первом примере разработаем диалоговое многопоточное приложение.
Сначала напишем функцию, которая будет сигнализировать системным динамиком. Её будем вызывать при создании потока.
DWORD WINAPI OurFunction (PVOID pParam)
{
Beep(200, 1000); //первый параметр–частота, второй – длительность
return (0);
}
Вызов создания потока можно интегрировать в функцию нажатия кнопки или на любое другое событие по вашему выбору. Для вызова потока по кнопке достаточно в код вставить следующий фрагмент:
DWORD dwID;
CreateThread(NULL, 0, OurFunction, NULL, NULL, &dwID);
После запуска собранного приложения и нажатия на требуемую кнопку мы можем услышать характерное «пищание» системного динамика (в случае если динамик подключён и активирован).
Все способы, за исключением рекомендуемого, являются нежелательными и должны использоваться только в форс-мажорных обстоятельствах.
Функция потока, возвращая управление, гарантирует корректную очистку всех ресурсов, принадлежащих данному потоку. При этом:
- любые С++ объекты, созданные данным потоком, уничтожаются соответствующими деструкторами;
- система корректно освобождает память, которую занимал стек потока;
- система устанавливает код завершения данного потока. Его функция и возвращает;
- счетчик пользователей данного объекта ядра (поток) уменьшается на 1.
При желании немедленно завершить поток изнутри используют функцию ExitThread(DWORD dwExitCode).
При этом освобождаются все ресурсы ОС, выделенные данному потоку, но С С++ ресурсы (например, объекты классов С++) не очищаются. Именно поэтому не рекомендовано завершать поток, используя эту функцию.
Если появилась необходимость уничтожить поток снаружи, то это может сделать функция TeminateThread.
Пример 2
Дана последовательность натуральных чисел a0, …, a99. Создать многопоточное приложение для поиска суммы квадратов Σai Вычисления должны независимо выполнять четыре потока.
Обсуждение. Разобьем последовательность чисел на четыре части и создадим четыре потока, каждый из которых будет вычислять суммы квадратов элементов в отдельной части последовательности. Главный поток создаст дочерние потоки, соберет данные и вычислит окончательный результат, после того, как отработают четыре дочерних потока (рис. 1.1). Приложение сделаем консольным.
Рис. 1.1 Схема потоков для примера 2
#include <stdio.h>
#include <conio.h>
#include <windows.h>
const int n = 4;
int a[100];
DWORD WINAPI ThreadFunc(PVOID pvParam)
{
int num,sum = 0,i;
num = 25*(*((int *)pvParam));
for(i=num;i<num+25;i++) sum += a[i]*a[i];
*(int*)pvParam = sum;
DWORD dwResult = num;
return dwResult;
}
int main(int argc, char** argv)
{
int x[n];
int i,rez = 0;
DWORD dwThreadId[n],dw,dwResult[n];
HANDLE hThread[n];
for (i=0;i<100;i++) a[i] = i;
//создание n дочерних потоков
for (i=0;i<n;i++)
{
x[i] = i;
hThread[i] = CreateThread(NULL,0,ThreadFunc,(PVOID)&x[i], 0, &dwThreadId[i]);
if(!hThread) printf("main process: thread %d not execute!",i);
}
// ожидание завершения n потоков
dw = WaitForMultipleObjects(n,hThread,TRUE,INFINITE);
// получение значений, переданных потоком в return
for (i=0;i<n;i++)
{
GetExitCodeThread(hThread[i],&dwResult[i]);
printf("%d\n",(int)dwResult[i]);
}
for(i=0;i<n;i++) rez+=x[i];
printf("\nСумма квадратов = %d",rez);
getch();
return 0;
}
Варианты заданий
1. Даны последовательности символов А = {а0…аn–1} и С = {с0…ск–1}. В общем случае n ≠ k. Создать многопоточное приложение, определяющее, совпадают ли посимвольно строки А и С. Количество потоков является входным параметром программы, количество символов в строках может быть не кратно количеству потоков.