Добавлен: 29.10.2018
Просмотров: 48014
Скачиваний: 190
2.2. Потоки
131
пространства все потоки, как показано в табл. 2.4, могут совместно использовать одни
и те же открытые файлы, дочерние процессы, ожидаемые и обычные сигналы и т. п.
Поэтому структура, показанная на рис. 2.8, а, может использоваться, когда все три
процесса фактически не зависят друг от друга, а структура, показанная на рис. 2.8, б,
может применяться, когда три потока фактически являются частью одного и того же
задания и активно и тесно сотрудничают друг с другом.
Таблица 2.4. Использование объектов потоками
Элементы, присущие каждому процессу
Элементы, присущие каждому потоку
Адресное пространство
Счетчик команд
Глобальные переменные
Регистры
Открытые файлы
Стек
Дочерние процессы
Состояние
Необработанные аварийные сигналы
Сигналы и обработчики сигналов
Учетная информация
Элементы в первом столбце относятся к свойствам процесса, а не потоков. Например,
если один из потоков открывает файл, этот файл становится видимым в других по-
токах, принадлежащих процессу, и они могут производить с этим файлом операции
чтения-записи. Это вполне логично, поскольку именно процесс, а не поток является
элементом управления ресурсами. Если бы у каждого потока были собственные адрес-
ное пространство, открытые файлы, необработанные аварийные сигналы и т. д., то он
был бы отдельным процессом. С помощью потоков мы пытаемся достичь возможности
выполнения нескольких потоков, использующих набор общих ресурсов с целью тесного
сотрудничества при реализации какой-нибудь задачи.
Подобно традиционному процессу (то есть процессу только с одним потоком), поток
должен быть в одном из следующих состояний: выполняемый, заблокированный,
готовый или завершенный. Выполняемый поток занимает центральный процессор
и является активным в данный момент. В отличие от этого, заблокированный поток
ожидает события, которое его разблокирует. Например, когда поток выполняет си-
стемный вызов для чтения с клавиатуры, он блокируется до тех пор, пока на ней не
будет что-нибудь набрано. Поток может быть заблокирован в ожидании какого-то
внешнего события или его разблокировки другим потоком. Готовый поток планиру-
ется к выполнению и будет выполнен, как только подойдет его очередь. Переходы
между состояниями потока аналогичны переходам между состояниями процесса
(см. рис. 2.2).
Следует учесть, что каждый поток имеет собственный стек (рис. 2.9). Стек каждого по-
тока содержит по одному фрейму для каждой уже вызванной, но еще не возвратившей
управление процедуры. Такой фрейм содержит локальные переменные процедуры
и адрес возврата управления по завершении ее вызова. Например, если процедура X
вызывает процедуру Y, а Y вызывает процедуру Z, то при выполнении Z в стеке будут
фреймы для X, Y и Z. Каждый поток будет, как правило, вызывать различные процедуры
и, следовательно, иметь среду выполнения, отличающуюся от среды выполнения других
потоков. Поэтому каждому потоку нужен собственный стек.
132
Глава 2. Процессы и потоки
Рис. 2.9. У каждого потока имеется собственный стек
Когда используется многопоточность, процесс обычно начинается с использования
одного потока. Этот поток может создавать новые потоки, вызвав библиотечную про-
цедуру, к примеру thread_create. В параметре thread_create обычно указывается имя
процедуры, запускаемой в новом потоке. Нет необходимости (или даже возможности)
указывать для нового потока какое-нибудь адресное пространство, поскольку он ав-
томатически запускается в адресном пространстве создающего потока. Иногда потоки
имеют иерархическую структуру, при которой у них устанавливаются взаимоотноше-
ния между родительскими и дочерними потоками, но чаще всего такие взаимоотноше-
ния отсутствуют и все потоки считаются равнозначными. Независимо от наличия или
отсутствия иерархических взаимоотношений создающий поток обычно возвращает
идентификатор потока, который дает имя новому потоку.
Когда поток завершает свою работу, выход из него может быть осуществлен за счет
вызова библиотечной процедуры, к примеру thread_exit. После этого он прекращает
свое существование и больше не фигурирует в работе планировщика. В некоторых ис-
пользующих потоки системах какой-нибудь поток для выполнения выхода может ожи-
дать выхода из какого-нибудь другого (указанного) потока после вызова про цедуры,
к примеру thread_join. Эта процедура блокирует вызывающий поток до тех пор, пока не
будет осуществлен выход из другого (указанного) потока. В этом отношении создание
и завершение работы потока очень похожи на создание и завершение работы процесса
при использовании примерно одних и тех же параметров.
Другой распространенной процедурой, вызываемой потоком, является thread_yield.
Она позволяет потоку добровольно уступить центральный процессор для выполне-
ния другого потока. Важность вызова такой процедуры обусловливается отсутствием
прерывания по таймеру, которое есть у процессов и благодаря которому фактически
задается режим многозадачности. Для потоков важно проявлять вежливость и время
от времени добровольно уступать центральный процессор, чтобы дать возможность
выполнения другим потокам. Другие вызываемые процедуры позволяют одному по-
току ожидать, пока другой поток не завершит какую-нибудь работу, а этому потоку —
оповестить о том, что он завершил определенную работу, и т. д.
Хотя потоки зачастую приносят пользу, они вносят в модель программирования и ряд
сложностей. Для начала рассмотрим эффект, возникающий при осуществлении си-
2.2. Потоки
133
стемного вызова fork, принадлежащего ОС UNIX. Если у родительского процесса есть
несколько потоков, должны ли они быть у дочернего процесса? Если нет, то процесс
может неверно функционировать из-за того, что все они составляют его неотъемлемую
часть.
Но если дочерний процесс получает столько же потоков, сколько их было у родитель-
ского процесса, что произойдет, если какой-нибудь из потоков родительского процесса
был заблокирован системным вызовом read, используемым, к примеру, для чтения
с клавиатуры? Будут ли теперь два потока, в родительском и в дочернем процессах, за-
блокированы на вводе с клавиатуры? Если будет набрана строка, получат ли оба потока
ее копию? Или ее получит только поток родительского процесса? А может быть, она
будет получена только потоком дочернего процесса? Сходные проблемы существуют
и при открытых сетевых подключениях.
Другой класс проблем связан с тем, что потоки совместно используют многие струк-
туры данных. Что происходит в том случае, если один поток закрывает файл в тот
момент, когда другой поток еще не считал с него данные? Предположим, что один по-
ток заметил дефицит свободной памяти и приступил к выделению дополнительного
объема. На полпути происходит переключение потоков, и новый поток тоже замечает
дефицит свободной памяти и приступает к выделению дополнительного объема. Впол-
не возможно, что дополнительная память будет выделена дважды. Для решения этих
проблем следует приложить ряд усилий, но для корректной работы многопоточных
программ требуется все тщательно продумать и спроектировать.
2.2.3. Потоки в POSIX
Чтобы предоставить возможность создания переносимых многопоточных программ,
в отношении потоков институтом IEEE был определен стандарт IEEE standard 1003.1c.
Определенный в нем пакет, касающийся потоков, называется Pthreads. Он поддержи-
вается большинством UNIX-систем. В стандарте определено более 60 вызовов функ-
ций. Рассмотреть в этой книге такое количество функций мы не в состоянии. Лучше
опишем ряд самых основных функций, чтобы дать представление о том, как они рабо-
тают. В табл. 2.5 перечислены все вызовы функций, которые мы будем рассматривать.
Таблица 2.5. Ряд вызовов функций стандарта Pthreads
Вызовы, связанные с потоком
Описание
pthread_create
Создание нового потока
pthread_exit
Завершение работы вызвавшего потока
pthread_join
Ожидание выхода из указанного потока
pthread_yield
Освобождение центрального процессора, позволяющее
выполняться другому потоку
pthread_attr_init
Создание и инициализация структуры атрибутов потока
pthread_attr_destroy
Удаление структуры атрибутов потока
Все потоки Pthreads имеют определенные свойства. У каждого потока есть свои иден-
тификатор, набор регистров (включая счетчик команд) и набор атрибутов, которые
сохраняются в определенной структуре. Атрибуты включают размер стека, параметры
планирования и другие элементы, необходимые при использовании потока.
134
Глава 2. Процессы и потоки
Новый поток создается с помощью вызова функции pthread_create. В качестве значения
функции возвращается идентификатор только что созданного потока. Этот вызов на-
меренно сделан очень похожим на системный вызов fork (за исключением параметров),
а идентификатор потока играет роль PID, главным образом для идентификации ссылок
на потоки в других вызовах.
Когда поток заканчивает возложенную на него работу, он может быть завершен путем
вызова функции pthread_exit. Этот вызов останавливает поток и освобождает про-
странство, занятое его стеком.
Зачастую потоку необходимо перед продолжением выполнения ожидать окончания
работы и выхода из другого потока. Ожидающий поток вызывает функцию pthread_join,
чтобы ждать завершения другого указанного потока. В качестве параметра этой функ-
ции передается идентификатор потока, чьего завершения следует ожидать.
Иногда бывает так, что поток не является логически заблокированным, но считает, что
проработал достаточно долго, и намеревается дать шанс на выполнение другому потоку.
Этой цели он может добиться за счет вызова функции pthread_yield. Для процессов
подобных вызовов функций не существует, поскольку предполагается, что процессы
сильно конкурируют друг с другом и каждый из них требует как можно больше вре-
мени центрального процессора. Но поскольку потоки одного процесса, как правило,
пишутся одним и тем же программистом, то он добивается от них, чтобы они давали
друг другу шанс на выполнение.
Два следующих вызова функций, связанных с потоками, относятся к атрибутам. Функ-
ция pthread_attr_init создает структуру атрибутов, связанную с потоком, и инициали-
зирует ее значениями по умолчанию. Эти значения (например, приоритет) могут быть
изменены за счет работы с полями в структуре атрибутов.
И наконец, функция pthread_attr_destroy удаляет структуру атрибутов, принадлежа-
щую потоку, освобождая память, которую она занимала. На поток, который использо-
вал данную структуру, это не влияет, и он продолжает свое существование.
Чтобы лучше понять, как работают функции пакета Pthread, рассмотрим простой
пример, показанный в листинге 2.1. Основная программа этого примера работает
в цикле столько раз, сколько указано в константе NUMBER_OF_THREADS (количе-
ство потоков), создавая при каждой итерации новый поток и предварительно сообщив
о своих намерениях. Если создать поток не удастся, она выводит сообщение об ошибке
и выполняет выход. После создания всех потоков осуществляется выход из основной
программы.
Листинг 2.1. Пример программы, использующей потоки
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define NUMBER_OF_THREADS 10
void *print_hello_world(void *tid)
{
/* Эта функция выводит идентификатор потока, а затем осуществляет выход */
printf("Привет, мир. Тебя приветствует поток № %d\n", tid);
pthread_exit(NULL);
}
2.2. Потоки
135
int main(int argc, char *argv[])
{
/* Основная программа создает 10 потоков, а затем осуществляет выход. */
pthread_t threads[NUMBER_OF_THREADS];
int status, i;
for(i=0; i < NUMBER_OF_THREADS; i++) {
printf("Это основная программа. Создание потока № %d\n"", i);
status = pthread_create(&threads[i], NULL, print_hello_world,
(void *)i);
if (status != 0) {
printf("Жаль, функция pthread_create вернула код ошибки %d\n"",
status);
exit(-1);
}
}
exit(NULL);
}
При создании поток выводит однострочное сообщение, объявляя о своем существо-
вании, после чего осуществляет выход. Порядок, в котором выводятся различные
сообщения, не определен и при нескольких запусках программы может быть разным.
Конечно же описанные функции Pthreads составляют лишь небольшую часть много-
численных функций, имеющихся в этом пакете. Чуть позже, после обсуждения син-
хронизации процессов и потоков, мы изучим и некоторые другие функции.
2.2.4. Реализация потоков
в пользовательском пространстве
Есть два основных места реализации набора потоков: в пользовательском пространстве
и в ядре. Это утверждение носит несколько спорный характер, поскольку возможна еще
и гибридная реализация. А теперь мы опишем эти способы со всеми их достоинствами
и недостатками.
Первый способ — это поместить весь набор потоков в пользовательском пространстве.
И об этом наборе ядру ничего не известно. Что касается ядра, оно управляет обычными,
однопотоковыми процессами. Первое и самое очевидное преимущество состоит в том,
что набор потоков на пользовательском уровне может быть реализован в операционной
системе, которая не поддерживает потоки. Под эту категорию подпадают все операци-
онные системы, даже те, которые еще находятся в разработке. При этом подходе потоки
реализованы с помощью библиотеки.
У всех этих реализаций одна и та же общая структура (рис. 2.10, а). Потоки запускают-
ся поверх системы поддержки исполнения программ (run-time system), которая пред-
ставляет собой набор процедур, управляющих потоками. Четыре из них: pthread_create,
pthread_exit, pthread_join и pthread_yield — мы уже рассмотрели, но обычно в наборе
есть и другие процедуры.
Когда потоки управляются в пользовательском пространстве, каждому процессу не-
обходимо иметь собственную таблицу потоков, чтобы отслеживать потоки, имеющиеся
в этом процессе. Эта таблица является аналогом таблицы процессов, имеющейся в ядре,