Смекни!
smekni.com

Учебно-методическое пособие рекомендовано учебно-методическим советом Международного университета природы, общества и человека (стр. 2 из 11)

Напомним, что при вытесняющей многозадачности потоки выполняются попеременно, время процессора выделяется потокам квантами ( около 19 мс ). ОС вытесняет поток, когда истечет его квант или когда на очереди поток с большим приоритетом. Приоритеты постоянно пересчитываются, чтобы избежать монополизации процессора одним потоком.

Создание и работа с потоками

Каждый поток начинает свое выполнение с некоторой входной функции. У функции должен быть следующий прототип:

DWORD WINAPI ThreadProc(PVOID pPararn);

Функция потока может выполнять абсолютно любые задачи. Ниже приведена пустая функция, которая ничего не делает.

DWORD WINAPI ThreadProc(PVOID pPararn);

{

return 0;

}

Когда эта функция закончит выполнение – поток автоматически завершится. В этот момент система выполняет следующие действия:

  • Останавливает поток
  • Освобождает стек
  • Счетчик пользователей для объекта ядра потока уменьшится на 1.

Когда счетчик объекта ядра обнуляется – система его удаляет. Получается, что объект ядра может жить дольше, чем сам поток. Это сделано для того, чтобы остальные части программы могли получать доступ к информации о потоке, даже если его уже не существует. Например, если надо узнать код завершения потока.

Функция потока всегда должна возвращать значение. Именно оно будет использоваться как код завершения потока.

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

Итак, у нас есть потоковая функция. Давайте заставим систему создать для нас поток, который выполнит эту функцию.

Создание потока

Создание потока в Windows происходит с помощью вызова API фукнции:

HANDLE CreateThread(PSECURITY_ATTRIBUTES psa, DWORD cbStack,

PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam, DWORD tdwCreate, PDWORD pdwThreadID);

Вызов этой функции создает объект ядра “поток” и возвращает его дескриптор. Система выделяет память под стек нового потока из адресного пространства процесса, инициализирует структуры данных потока и передает управление потоковой функции. Новый поток выполняется в контексте того же процесса, что и родительский поток. Поэтому он имеет доступ ко всем дескрипторам процесса, адресному пространству. Поэтому все потоки могут легко взаимодействовать друг с другом.

Параметры функции CreateThread следующие:

  • psa – указатель на структуру SECURITY_ATTRIBUTES. Если вы хотите, чтобы потоку были присвоены параметры защиты по умолчанию – передайте сюда NULL.
  • cbStack – размер стека потока. Если параметр равен нулю – используется размер по умолчанию. Если вы передаете не нулевое значение, система выберет большее между настройками текущего исполняемого файла и вашим значением. Этот параметр резервирует только адресное пространство, а физическая память выделяется по мере необходимости.
  • pfnStartAddr – это указатель на потоковую функцию. Прототип функции мы рассмотрели выше.
  • pParam – произвольное значение. Этот параметр идентичен параметру потоковой функции. CreateThread передаст этот параметр в потоковую функцию. Это может быть число, либо указатель на структуру данных. Можно создавать несколько потоков с одной и той же потоковой функцией. Каждому потоку можно передавать свое значение.

Внимание, не передавайте сюда указатель на локальные переменные! Т.к. родительский поток работает одновременно с новым – локальные переменные могут выйти из области видимости и разрушиться компилятором. В то время, как новый поток будет пытаться получить к ним доступ.

  • tdwCreate – дополнительные параметры создания потока. Может принимать значение 0 если надо начать выполнение потока немедленно, либо CREATE_SlJSPENDED. В последнем случае система выполняет всю инициализацию, но не запускает выполнение потока. Поток можно запустить в любой момент, вызвав WinAPI функцию ResumeThread.
  • pdwThreadID – указатель на переменную, которая на выходе будет содержать идентификатор потока. Windows 2k+ позволяет передавать сюда NULL, если Вам не нужно это значение. Однако, рекомендуется всегда передавать адрес переменной для совместимости с более ранними ОС.

Завершение потока

Поток может завершиться в следующих случаях:

  • Поток самоуничтожается с помощью вызова ExitThread (не рекомендуется)
  • функция потока возвращает управление (рекомендуемый способ)
  • один из потоков данного или стороннего процесса вызывает функцию TerminateThread (нежелательный способ)
  • завершается процесс, содержащий данный поток (тоже нежелательно).

Функцию потока следует проектировать так, чтобы поток завершался только после того, как она возвращает управление. Это единственный способ, гарантирующий корректную очистку всех ресурсов, принадлежавших Вашему потоку. При этом:

  • любые С++-объекты, созданные данным потоком, уничтожаются соответствующими деструкторами;
  • система корректно освобождает память, которую занимал стек потока;
  • система устанавливает код завершения данного потока (поддерживаемый объектом ядра "поток») — его и возвращает Ваша функция потока;
  • счетчик пользователей данного объекта ядра "поток" уменьшается на 1

Вызов 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&bsol;n",(int)dwResult[i]);

}

for(i=0;i<n;i++) rez+=x[i];

printf("&bsol;nСумма квадратов = %d",rez);

getch();

return 0;

}

Варианты заданий

1. Даны последовательности символов А = {а0…аn–1} и С = {с0…ск–1}. В общем случае n ≠ k. Создать многопоточное приложение, определяющее, совпадают ли посимвольно строки А и С. Количество потоков является входным параметром программы, количество символов в строках может быть не кратно количеству потоков.