Создание нитей (thread) в программе


Главная нить процесса создается автоматически при создании процесса. Если процесс нуждается в дополнительных нитях, то его программа вызывает системные функции создания нити.

В операционной системе OS/2 для создания нити служит функция DosCreateThread, которая имеет прототип

APIRET DosCreateThread(PTID ptid, PFNTHREAD pfn,

ULONG param, ULONG flag, ULONG cbStack),

где ptid – указатель на возвращаемый номер (идентификатор) нити, pfn – имя подпрограммы (описанной выше в программе прототипом), param– передаваемый в процедуру параметр, cbStack - размер стека для нити. Аргумент flag задает, запускается ли в работу нить сразу после создания или создается приостановленной. В первом случае значение flag = 0, во втором flag = 1. Эти значения могут быть указаны символическими константами CREATE_READY и CREATE_SUSPENDED. Тип PFNTHREAD определяет процедуру нити как имеющую тип VOID с единственным аргументом типа ULONG. Используя явное преобразование типов, можно задать при создании нити использование любого вида процедуры.

В операционной системе Unix многопоточное программирование появилось достаточно поздно. К настоящему времени эта возможность входит в стандарт POSIX для Unix и поддерживается во всех современных ОС. Использование нитей при этом требует подключения заголовочного файла pthread.h. Системная функция создания нити в Unix по указанному стандарту имеет прототип

int pthread_create(pthread_t* tid, const pthread_attr_t* att,

void*(*fun)(void*), void* argp).

Первый параметр этой функции возвращает идентификатор созданной нити. (В качестве дополнительных возможностей предлагается задавать в этот параметр исходное значение NULL, тогда идентификатор не возвращается). Важнейшим и обязательным параметром является адрес процедуры нити, этот параметр стоит третьим в списке и ему должна отвечать процедура с возвращаемым типом void* и единственным аргументом типа void*. При ином построении процедуры (что практически не имеет смысла, если эта процедура используется только для работы нити) требуется указывать явное преобразование типа. Последний, четвертый параметр, предназначен для передачи конкретного аргумента при создании нити. Второй параметр вызоваattr – для задания атрибутов, отличных от атрибутов по умолчанию, причем параметр является указателем на место объекта, содержащего эти атрибуты. В простейшем и многих иных случаях этот параметр задается как NULL, что информирует ОС о необходимости использовать атрибуты по умолчанию. (О более сложном использовании этого параметра здесь0расёказываться не будет.) Если функция создания нити возвращает значение >0, то в ходе выполнения системной функции пѐоизошла ошибка, и0нить не была создана. Эту ситуацию рекомендуется анализировать в программе. Нулевое возвращаемое значение соответствует безошибочному выполнению, а положительное значение выдает непосредственно числовое значение кода ошибки.

Действия процедуры нити могут завершаться специальной функцией pthread_exit(). Эта функция имеет прототип

int pthread_exit(void *status),

где аргумент функции является указателем на значение любого удобного программисту типа данных. Указанное значение предназначено для выдачи кода завершения из завершающейся нити. Этот код может быть получен и проанализирован специальной функцией ожидания завершения нити, которая будет рассмотрена в этой же главе. Если предполагается не использовать код завершения нити, то аргумент функции pthread_exit может задаваться как нулевой указатель или просто как нулевое значение. Указанная функция требуется только, если эта нить должна передавать код завершения или при задании завершения внутри программы процедуры. На конечном месте процедуры (перед закрывающей фигурной скобкой тела процедуры нити) вызов функции завершения можно опускать. (Практически она автоматически вызывается после возврата из процедуры нити, когда последняя завершается как обычная процедура.)

Следующая программа, приведенная в листинге 8.2.1, демонстрирует порождение и использование нитей.

 

#include <pthread.h>

#include <stdio.h>

char lbuk[ ]="abcdefghijklmnoprstvuwxy";

pthread_t tid1, tid2, tid3;

 

void procthread1(void *arg)

{int k, j;

for (k=0; k<24; k++)

{printf("\033[%d;20H\033[1;34m",k+1);

for (j=0; j<(int)arg; j++) printf("%c",lbuk[k]);

printf("\n"); usleep(1000000);

}

}

 

void procthread2(void *arg)

{int k, j;

 

for (k=0; k<24; k++)

{printf("\033[%d;40H\033[1;32m",k+1);

for (j=0; j<(int)arg; j++) printf("%c",lbuk[k]);

printf("\n"); usleep(1000000);

}

}

 

void procthread3(void *arg)

{int k, j;

for (k=0; k<24; k++)

{printf("\033[%d;60H\033[31m",k+1);

for (j=0; j<(int)arg; j++) printf("%c",lbuk[k]);

printf("\n"); usleep(1000000);

}

}

 

void main()

{int k;

int rc;

printf("\033[2J\n");

rc=pthread_create(&tid1, NULL, (void*)procthread1, (void*)2);

rc=pthread_create(&tid2, NULL, (void*)procthread2, (void*)3);

rc=pthread_create(&tid3, NULL, (void*)procthread3, (void*)4);

for (k=0; k<24; k++)

{printf("\033[%d;1H\033[37m",k+1);

printf("%c++\n",lbuk[k]);

usleep(1500000);

}

getchar(); printf("\033[0m");

}

Листинг 8.2.1. Программа с многими нитями для Unix.

 

В этой программе описаны три процедуры с именами procthread1, procthread2, procthread3. Они имеют тип результата void и единственный аргумент типа void*. Главная часть программы запускает три нити, используя эти процедуры как основы их. Четвертый аргумент функции pthread_create передает в процедуры параметр, который в примере для всех вызовов является числовым (числом 2, 3, 4), что соответствует описанию аргументов в прототипе.

В каждой процедуре осуществляется многократный вызов (на основе цикла с повторением 24 раза) функции вывода на экран. Причем последовательно выводятся символы, описанные в символьном массиве lbuk, общем для всех процедур. Каждая процедура выводит символы своим цветом (первая – ярко-синим, вторая – ярко-зеленым, третья – красным), а начиная со своей колонки экрана (соответственно с 20-й, 40-й и 60-й). Каждый шаг во внешнем цикле процедуры выводит символы в строку с номером, на единицу большим номера цикла (отсчет строк ведется с единицы, номера шага переменной k – c нуля). Все процедуры выводят подряд столько одинаковых символов, сколько задает параметр, передаваемый процедуре. Здесь же демонстрируется преобразование общей формы передачи параметра, описанного в прототипе как void*, в конкретный тип, нужный в месте использования. Для этого аргумент в месте использования записан как (int)arg. Для удобства наблюдений используется функция приостановки usleep, использование которой позволяет наглядно воспринимать происходящее в процессе и видеть как по очереди работают отдельные нити.

В главной части программы также выполняется циклический вывод на экран символов из того же массива lbuk, но только уже в белом цвете. Причем символы выводятся по одному в первую колонку экрана. Для упрощения программы код возврата функций при создании нитей в этой программе не анализируется (такое упрощение не следует применять в реальных программах). Особенностью демонстрационной программы для операционной системы Unix является необходимость дополнительных выводов управляющих символов "перевода строки" (в обозначениях Си '\n'). Причиной этого служит встроенная в реализацию Linux внутренняя оптимизация вывода на экран консоли. Этот вывод не отображается на экране, пока вывод строки не завершен. Практически вывод идет во внутренний буфер, но на пользовательском экране появляется после передачи указанного управляющего символа. Следствием такого механизма является включение в программу отдельных операторов printf("\n"). Обучаемому рекомендуется, удалив такие операторы, пронаблюдать поведение полученной программы. (Иным, но не лучшим решением является применение не стандартного вывода, неявно используемого функцией printf(), а стандартного вывода ошибок вместе с использованием функции fprintf() в виде fprintf(stderr, "текст_формата", аргументы)).

Наиболее сложная ситуация оказывается с порождением нитей в операционных системах Windows. И дело все в том, что для порождения нитей в этих системах имеется не одна функция, а несколько. Самой ранней является системная функция CreateThread, но в ее реализации при использовании на языке программирования Си позже были обнаружены некоторые проблемы некорректного поведения и было предложено заменить ее функцией _begintread. Еще позже непредусмотренное поведение было обнаружено (в редких, но возможных ситуациях) и у этой функции, в связи с чем разработчики систем программирования на языке Си ввели в использование новый вариант последней функции. Особо отметим, что проблемы использования функции создания возникают при написании программ именно на языке Си, который в свое время создавался в неявном предположении единственной нити в программе (как потом оказалось!).

В системах программирования фирмы Microsoft, называемых традиционно Visual C++, модифицированная функция создания нитей получила название _beginthreadex. В системах программирования фирмы Borland/Inprise Inc. модифицированную функцию назвали _beginthreadNT. Все эти перечисленные функции отличаются по порядку, а частично и по набору своих аргументов. Введение в использование новых функций для порождения нитей имело целью сохранить старые программы с их сложившимся поведением и одновременно исправить ошибки и обеспечить более совершенное поведение для новых программ. Основная нагрузка принятия решений при программировании и ответственности за эти решения переносилась с разработчиков системных компонентов Windows на далеких от них прикладных разработчиков-программистов.

К настоящему времени наиболее безопасными функциями порождения нитей являются _beginthreadNT и _beginthreadex, именно их и рассмотрим в первую очередь, предлагая для ближайшего использования.

Функция _beginthreadex от Microsoft имеет прототип

unsigned long _beginthreadex(void* security_attrib, unsigned stack_size,

unsigned (*proc)(void*), void *arglist, unsigned flag,

unsigned *tid).

Функция _beginthreadNT имеет прототип

unsigned long _beginthreadNT(void (*proc)(void *), unsigned stack_size,

void *arglist, void *security_attrib,

unsigned long flags, unsigned long *tid).

Параметр proc, записанный в форме определения подпрограммы, задает адрес процедуры нити, в стандартном случае эта процедура должна быть функцией без возвращаемого значения (формально – с возвращаемым значением типа void) и нетипизированным аргументом (формально типа void*). Аргумент stack_size функций создания нити указывает размер стека, практически он округляется до кратного величине 4096. Аргумент arglist задает указатель на строку аргументов, в простейшем случае отсутствия аргументов этот параметр может быть задан значением NULL. Аргумент security_attrib применяется в более сложных программах, использующих всю мощь средств встроенной защиты в Windows NT. В более простых случаях его можно всегда полагать равным NULL. Аргумент flag служит для задания режима приостановленной при создании нити, что обозначается символической константой CREATE_SUSPENDED. В противном случае (запуск тут же функционирующей нити) этот параметр-флаг устанавливается равным нулю. Последний параметр функций создания предназначен для возвращаемого значения идентификатора нити. Возвращает функция в случае неудачи число -1, в остальных случаях возвращаемое число – хэндл созданной нити, который как переменная должно быть описана с типом HANDLE. Заметим, что в системе разработки программ Watcom C++ используется вариант Microsoft функции создания нити.

Процедура нити, используемая функциями _beginthreadex и _beginthreadNT, может для завершения своих действий использовать системный вызов функции _endthreadex() или соответственно _endthread().

Исходной системной функцией для построения всех описанных функций создания нитей в Windows служит функция CreateThread, которой можно пользоваться в тех программах, процедуры задания нитей которых не содержат стандартных функций базовой библиотеки языка Си и неявно не используют их. Такая ситуация имеет место, когда процедура нити строится только на основе собственно Windows API – только этого программного интерфейса для ввода-вывода. Заметим, что именно этот вариант имеет место для большинства примеров в данном пособии. Функция CreateThread имеет прототип

HANDLE CreateThread(SECURITY_ATTRIBUTES *security_attrib,

DWORD stack_size, THREAD_START_ROUTINE *proc,

VOID *arglist, DWORD flags, DWORD * tid),

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

void ExitThread(DWORD dwExitCode),

если такое завершение требуется задать до "естественного" конца программы процедуры.

 

Следующий пример, представленный программой в листинге 8.2.2, демонстрирует построение и использование нитей в Windows. Эта программа аналогична рассмотренным ранее для других операционных систем и ведет себя аналогично. Отметим, что для точного согласования типов (чтобы не появлялись предупреждения о возможных ошибках) потребовалось задать явное преобразование типа для возвращаемого функцией значения, а именно в тип HANDLE.

 

#include <windows.h>

#include <process.h>

#include <stdio.h>

char lbuk[ ]="abcdefghijklmnoprqstuwxy";

HANDLE hstdout;

DWORD actlen;

CRITICAL_SECTION csec;

 

void procthread1(void *arg)

{int k, j;

COORD pos;

for (k=0; k<24; k++) {

pos.X=20; pos.Y=k+1;

EnterCriticalSection(&csec);

SetConsoleCursorPosition(hstdout,pos); // установить курсор в позицию (20,k+1)

SetConsoleTextAttribute(hstdout,FOREGROUND_BLUE); // и синий цвет

for (j=0; j<(int)arg; j++) printf("%c",lbuk[k]);

LeaveCriticalSection(&csec);

Sleep(800);

}

}

 

void procthread2(void *arg)

{int k, j;

COORD pos;

for (k=0; k<24; k++) {

pos.X=40; pos.Y=k+1;

EnterCriticalSection(&csec);

SetConsoleCursorPosition(hstdout,pos); // установить курсор в позицию (40,k+1)

SetConsoleTextAttribute(hstdout,FOREGROUND_GREEN); // и зеленый цвет

for (j=0; j<(int)arg; j++) printf("%c",lbuk[k]);

LeaveCriticalSection(&csec);

Sleep(1300);

}

}

 

void procthread3(void *arg)

{int k, j;

COORD pos;

for (k=0; k<24; k++) {

pos.X=60; pos.Y=k+1;

EnterCriticalSection(&csec);

SetConsoleCursorPosition(hstdout,pos); // установить курсор в позицию (60,k+1)

SetConsoleTextAttribute(hstdout,FOREGROUND_RED); // и красный цвет

for (j=0; j<(int)arg; j++) printf("%c",lbuk[k]);

LeaveCriticalSection(&csec);

Sleep(1100);

}

}

 

void main()

{HANDLE hthread1, hthread2, hthread3;

unsigned long threadid1, threadid2, threadid3;

int k;

COORD pos;

hstdout=GetStdHandle(STD_OUTPUT_HANDLE);

InitializeCriticalSection(&csec);

hthread1=(HANDLE)_beginthreadNT(procthread1, 4096, (void *)2, NULL, 0,

&threadid1);

hthread2=(HANDLE)_beginthreadNT(procthread2, 4096, (void *)3, NULL, 0,

&threadid2);

hthread3=(HANDLE)_beginthreadNT(procthread3, 4096, (void *)4, NULL, 0,

&threadid3);

Sleep(600);

for (k=0; k<24; k++) {

pos.X=1; pos.Y=k+1;

EnterCriticalSection(&csec);

SetConsoleCursorPosition(hstdout,pos); // установить курсор в позицию (10,k+1)

SetConsoleTextAttribute(hstdout, // и белый цвет

FOREGROUND_BLUE|FOREGROUND_GREEN|FOREGROUND_RED);

printf("%c++",lbuk[k]);

LeaveCriticalSection(&csec);

Sleep(1000); }

getchar();

DeleteCriticalSection(&csec);

CloseHandle(hthread1); CloseHandle(hthread2); CloseHandle(hthread3);

}

Листинг 8.2.2. Программа с многими нитями для Windows

В программах для Windows при выводе на консоль несколькими нитями возможно нежелательное взаимное влияние их друг на друга, выражающееся в искажении выводимой информации. Для принципиального устранения такого влияния можно использовать универсальное средство, называемое критическими интервалами. В данной программе это средство представлено описанием специальной общей для процедур переменной csec со структурным типом CRITICAL_SECTION и системными функциями, использующими адрес этой переменной, и называемыми Initialize­CriticalSection, EnterCriticalSection, LeaveCriticalSection, DeleteCritical­Section. Они относятся к средствам организации взаимодействий и позволяют правильно согласовывать параллельную работу нескольких нитей. Подробно назначение и использование этих функций будут рассматриваться в п. 9.6.

Для среды разработки MS Visual C++ последняя программа должна будет содержать несколько иные вызовы функций создания нитей. Текст главной подпрограммы модификации последней программы для указанной среды приведен в листинге 8.2.3.

 

#define PFMSTHREAD unsigned ( __stdcall *)( void * )

void main()

{HANDLE hthread1, hthread2, hthread3;

unsigned threadid1, threadid2, threadid3;

int k;

COORD pos;

DWORD actlen;

 

hstdout=GetStdHandle(STD_OUTPUT_HANDLE);

InitializeCriticalSection(&csec);

hthread1=(HANDLE)_beginthreadex(NULL, 4096,

(PFMSTHREAD)procthread1, (void *)2, 0, &threadid1);

hthread2=(HANDLE)_beginthreadex(NULL, 4096,

(PFMSTHREAD)procthread2, (void *)3, 0, &threadid2);

hthread3=(HANDLE)_beginthreadex(NULL, 4096,

(PFMSTHREAD)procthread3, (void *)4, 0, &threadid3);

for (k=0; k<24; k++)

{pos.X=1; pos.Y=k+1;

EnterCriticalSection(&csec);

SetConsoleCursorPosition(hstdout,pos);

SetConsoleTextAttribute(hstdout,

FOREGROUND_BLUE|FOREGROUND_GREEN|FOREGROUND_RED);

printf("%c++",lbuk[k]);

LeaveCriticalSection(&csec);

Sleep(1000); }

getchar();

DeleteCriticalSection(&csec);

CloseHandle(hthread1); CloseHandle(hthread2); CloseHandle(hthread3);

}

Листинг 8.2.3. Фрагмент программы с многими нитями для MS Visual C++

 

Заметим, что в компиляторе системы MS Visual C++ очень строго отслеживается соответствие типов формальных и фактических аргументов, поэтому в приведенной программе использовано предварительное соотнесение прототипа вызова процедуры нити имени PFMSTHREAD для типа этой функции, выбранное программистом. Вместо кратких указаний позиционирования и задания цвета, присутствующих в наборе дополнительных функций пакета Borland, здесь приходится использовать полные API функции для позиционирования и управления цветом в Windows. Аналогичные изменения управления позиционированием и цветом следует произвести во всех подпрограммах данного примера для Visual C++.