Хотя эта концепция с успехом использовалась на протяжении многих лет, с функцией
fork
связаны определенные неудобства.
Стоимость функции
fork
довольно высока, так как при ее использовании требуется скопировать все содержимое памяти из родительского процесса в дочерний, продублировать все дескрипторы и т.д. Текущие реализации используют технологию, называемую копированием при записи( copy-on-write), при которой копирование пространства данных из родительского процесса в дочерний происходит лишь тогда, когда дочернему процессу требуется своя собственная копия. Но несмотря на эту оптимизацию, стоимость функции
fork
остается высокой.
Для передачи данных между родительским и дочерним процессами послевызова
функции
fork
требуется использовать средства взаимодействия процессов (IPC). Передача информации перед вызовом
fork
не вызывает затруднений, так как при запуске дочерний процесс получает от родительского копию пространства данных и копии всех родительских дескрипторов. Но возвращение информации из дочернего процесса в родительский требует большей работы.
Обе проблемы могут быть разрешены путем использования программных потоков( threads). Программные потоки иногда называются облегченными процессами( lightweight processes), так как поток проще, чем процесс. В частности, создание потока требует в 10–100 раз меньше времени, чем создание процесса.
Все потоки одного процесса совместно используют его глобальные переменные, поэтому им легко обмениваться информацией, но это приводит к необходимости синхронизации.
Однако общими становятся не только глобальные переменные. Все потоки одного процесса разделяют:
инструкции процесса;
большую часть данных;
открытые файлы (например, дескрипторы);
обработчики сигналов и вообще настройки для работы с сигналами (действие сигнала);
текущий рабочий каталог;
идентификаторы пользователя и группы пользователей.
У каждого потока имеются собственные:
идентификатор потока;
набор регистров, включая счетчик команд и указатель стека;
стек (для локальных переменных и адресов возврата);
переменная
errno
;
маска сигналов;
приоритет.
ПРИМЕЧАНИЕ
Как сказано в разделе 11.18, можно рассматривать обработчик сигнала как некую разновидность потока. В традиционной модели Unix у нас имеется основной поток выполнения и обработчик сигнала (другой поток). Если в основном потоке в момент возникновения сигнала происходит корректировка связного списка и обработчик сигнала также пытается изменить связный список, обычно начинается путаница. Основной поток и обработчик сигнала совместно используют одни и те же глобальные переменные, но у каждого из них имеется свой собственный стек.
В этой книге мы рассматриваем потоки POSIX, которые также называются Pthreads(POSIX threads). Они были стандартизованы в 1995 году как часть POSIX.1c и будут поддерживаться большинством версий Unix. Мы увидим, что все названия функций Pthreads начинаются с символов
pthread_
. Эта глава является введением в концепцию потоков, необходимым для того, чтобы в дальнейшем мы могли использовать потоки в наших сетевых приложениях. Более подробную информацию вы можете найти в [15].
26.2. Основные функции для работы с потоками: создание и завершение потоков
В этом разделе мы рассматриваем пять основных функций для работы с потоками, а в следующих двух разделах мы используем эти функции для написания потоковой модификации клиента и сервера TCP.
Функция pthread_create
Когда программа запускается с помощью функции
exec
, создается один поток, называемый начальным( initial) или главным( main). Дополнительные потоки создаются функцией
pthread_create
.
#include <pthread.h>
int pthread_create(pthread_t* tid, const pthread_attr_t * attr,
void *(* func)(void*), void * arg);
Возвращает: 0
в случае успешного выполнения, положительное значение Exxx в случае ошибки
Каждый поток процесса обладает собственным идентификатором потока( thread ID), относящимся к типу данных
pthread_t
(как правило, это
unsigned int
). При успешном создании нового потока его идентификатор возвращается через указатель
tid
.
У каждого потока имеется несколько атрибутов: его приоритет, исходный размер стека, указание на то, должен ли этот поток являться демоном или нет, и т.д. При создании потока мы можем задать эти атрибуты, инициализируя переменную типа
pthread_attr_t
, что позволяет заменить значение, заданное по умолчанию. Обычно мы используем значение по умолчанию, в этом случае мы задаем аргумент
attr
равным пустому указателю.
Наконец, при создании потока мы должны указать, какую функцию будет выполнять этот поток. Выполнение потока начинается с вызова заданной функции, а завершается либо явно (вызовом
pthread_exit
), либо неявно (когда вызванная функция возвращает управление). Адрес функции задается аргументом
func
, и она вызывается с единственным аргументом-указателем
arg
. Если этой функции необходимо передать несколько аргументов, следует поместить их в некоторую структуру и передать адрес этой структуры как единственный аргумент функции.
Обратите внимание на объявления
func
и
arg
. Функции передается один аргумент — универсальный указатель
void*
. Это позволяет нам передавать потоку с помощью единственного указателя все, что требуется, и точно так же поток возвращает любые данные, используя этот указатель.
Возвращаемое значение функций Pthreads — это обычно 0 в случае успешного выполнения или ненулевая величина в случае ошибки. Но в отличие от функций сокетов и большинства системных вызовов, для которых в случае ошибки возвращается -1 и переменной
errno
присваивается некоторое положительное значение (код ошибки), функции Pthreads возвращают сам код ошибки. Например, если функция
pthread_create
не может создать новый поток, так как мы превысили допустимый системный предел количества потоков, функция возвратит значение
EAGAIN
. Функции Pthreads не присваивают переменной
errno
никаких значений. Соглашение о том, что 0 является индикатором успешного выполнения, а ненулевое значение — индикатором ошибки, не приводит к противоречию, так как все значения
Exxx
, определенные в заголовочном файле
<sys/errno.h>
, являются положительными. Ни одному из имен ошибок Exxx не сопоставлено нулевое значение.
Функция pthread_join
Мы можем приостановить выполнение текущего потока и ждать завершения выполнения какого-либо другого потока, используя функцию
pthread_join
. Сравнивая потоки и процессы Unix, можно сказать, что функция
pthread_create
аналогична функции
fork
, а функция
pthread_join
— функции
waitpid
.
#include <pthread.h>
int pthread_join(pthread_t tid, void ** status);
Возвращает: 0 в случае успешного выполнения, положительное значение Exxx в случае ошибки
Следует указать идентификатор
tid
того потока, завершения которого мы ждем. К сожалению, нет способа указать, что мы ждем завершения любого потока данного процесса (тогда как при работе с процессами мы могли с помощью функции
waitpid
ждать завершения любого процесса, задав аргумент идентификатора процесса, равный -1). Мы вернемся к этой проблеме при обсуждении листинга 26.11.