Последующие изменения зависят от средства синхронизации и от того, что происходит с лежащим в основе этого средства типом данных при вызове fork. Детали описаны в разделе 10.12.
■ Взаимное исключение Posix: должно храниться в разделяемой памяти (вместе со счетчиком) и инициализироваться с установленным атрибутом PTHREAD_ PROCESS_SHARED. Код программы будет приведен ниже.
■ Блокировка чтения-записи Posix: должна храниться в разделяемой памяти (вместе со счетчиком) и инициализироваться с установленным атрибутом PTHREAD_PROCESS_SHARED.
■ Семафоры Posix, размещаемые в памяти: семафор должен храниться в разделяемой памяти (вместе со счетчиком), и вторым аргументом при вызове sem_init должна быть единица (указывающая на то, что семафор используется несколькими процессами).
■ Именованные семафоры Posix: следует либо вызывать sem_open из родительского и дочерних процессов по отдельности, либо вызывать sem_open в родительском процессе, учитывая, что семафор станет общим после вызова fork.
■ Семафоры System V: никакого специального кодирования не требуется, поскольку эти семафоры всегда могут использоваться как процессами, так и потоками. Дочерним процессам достаточно знать идентификатор семафора.
■ Блокировка записей fcntl: изначально предназначена для использования несколькими процессами.
Мы приведем код только для программы с взаимными исключениями Posix.
Взаимные исключения Posix между процессами
Функция main первой программы использует взаимное исключение Posix для обеспечения синхронизации. Текст ее приведен в листинге А.32.
Листинг А.32. Функция main для измерения быстродействия взаимных исключений между процессами
//bench/incr_pmutex5.с
1 #include "unpipc.h"
2 #define MAXNPROC 100
3 int nloop;
4 struct shared {
5 pthread_mutex_t mutex;
6 long counter;
7 } *shared; /* указатель, сама структура в общей памяти */
8 void *incr(void *);
9 int
10 main(int argc, char **argv)
11 {
12 int i, nprocs;
13 pid_t childpid[MAXNPROC];
14 pthread_mutexattr_t mattr;
15 if (argc != 3)
16 err_quit("usage: incr_pxmutex5 <#loops> <#processes>");
17 nloop = atoi(argv[l]);
18 nprocs = min(atoi(argv[2]), MAXNPROC);
19 /* получение разделяемой памяти для родительского и дочерних процессов */
20 shared = My_shm(sizeof(struct shared));
21 /* инициализация взаимного исключения и его блокировка */
22 Pthread_mutexattr_init(&mattr);
23 Pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED);
24 Pthread_mutex_init(&shared->mutex, &mattr);
25 Pthread_mutexattr_destroy(&mattr);
26 Pthread_mutex_lock(&shared->mutex);
27 /* порождение дочерних процессов */
28 for (i = 0; i < nprocs; i++) {
29 if ((childpid[i] = Fork()) == 0) {
30 incr(NULL);
31 exit(0);
32 }
33 }
34 /* родительский процесс: запуск таймера и разблокирование взаимного исключения */
35 Start_time();
36 Pthread_mutex_unlock(&shared->mutex);
37 /* ожидание завершения всех дочерних процессов */
38 for (i = 0; i < nprocs; i++) {
39 Waitpid(childpid[i], NULL, 0);
40 }
41 printf("microseconds: %.0f usecn", Stop_time());
42 if (shared->counter != nloop * nprocs)
43 printf("error: counter = %ldn", shared->counter);
44 exit(0);
45 }
19-20 Поскольку мы запускаем несколько процессов, структура shared должна располагаться в разделяемой памяти. Мы вызываем функцию my_shm, текст которой приведен в листинге А.31.
21-26 Поскольку взаимное исключение помещено в разделяемую память, мы не можем статически инициализировать его, поэтому мы вызываем pthread_mutex_init после установки атрибута PTHREAD_PROCESS_SHARED. Взаимное исключение блокируется.
27-36 После создания дочерних процессов и запуска таймера блокировка снимается.
37-43 Родительский процесс ожидает завершения всех дочерних, после чего останавливает таймер.
Листинг А.33. Увеличение счетчика с использованием взаимных исключений между процессами
//bench/incr_pxmutex5.с
46 void *
47 incr(void *arg)
48 {
49 int i;
50 for (i = 0; i < nloop; i++) {
51 Pthread_mutex_lock(&shared->mutex);
52 shared->counter++;
53 Pthread_mutex_unlock(&shared->mutex);
54 }
55 return(NULL);
56 }
ПРИЛОЖЕНИЕ Б
Основы многопоточного программирования
В этом приложении приведены основные функции, используемые для работы с потоками. В традиционной модели Unix процесс, которому нужно, чтобы какое-то действие было выполнено не им самим, порождает дочерний процесс вызовом fork. Большая часть сетевых серверов под Unix написана именно так.
Хотя эта парадигма хорошо работала на протяжении многих лет, вызов fork обладает некоторыми недостатками:
■ вызов fork ресурсоемок. Память копируется от родительского процесса к дочернему, копируются все дескрипторы и т. д. Существующие реализации используют метод копирования при записи (copy-on-write), что исключает необходимость копирования адресного пространства родительского процесса, пока оно не понадобится клиенту, но, несмотря на эту оптимизацию, вызов fork остается ресурсоемким;
■ для передачи информации между родительским и дочерним процессами необходимо использовать одну из форм IPC после вызова fork. Передать информацию дочернему процессу легко: это можно сделать до вызова fork. Однако передать ее обратно может быть достаточно сложно.
Потоки помогают решить обе проблемы. Часто они называются «облегченными процессами» (lightweight processes), поскольку поток проще, чем процесс. Создание потока может занимать по времени меньше одной десятой создания процесса.
Все потоки одного процесса совместно используют его глобальные переменные, поэтому им легко обмениваться информацией, но это приводит к необходимости синхронизации. Однако общими становятся не только глобальные переменные. Все потоки одного процесса разделяют:
■ инструкции процесса;
■ большую часть данных;
■ открытые файлы (дескрипторы);
■ обработчики сигналов и вообще настройки для работы с сигналами;
■ текущий рабочий каталог;
■ идентификатор пользователя и группы.
Однако каждый поток имеет свои собственный:
■ идентификатор потока;
■ набор регистров, включая PC и указатель стека;
■ стек (для локальных переменных и адресов возврата);
■ errno;
■ маску сигналов;
■ приоритет.
Б.2. Основные функции для работы с потоками: создание и завершение
В этом разделе мы опишем пять основных функций для работы с потоками.
Функция pthread_create
При запуске пpoгрaммы вызовом exec создается единственный поток, называемый начальным потоком, или главным (initial thread). Добавочные потоки создаются вызовом pthread_create:
#include <pthread.h>
int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*func) (void *), void *arg);
/* Возвращает 0 в случае успешного завершения, положительное значение Еххх – в случае ошибки */
Каждый поток процесса обладает собственным идентификатором потока, который имеет тип pthread_t. При успешном создании нового потока его идентификатор возвращается через указатель tid.
Каждый поток обладает некоторым количеством атрибутов: приоритетом, начальным размером стека, признаком демона и т. п. При создании потока эти атрибуты могут быть указаны с помощью переменной типа pthread_attr_t, значение которой имеет более высокий приоритет, чем значения по умолчанию. Обычно мы используем значения по умолчанию. При этом аргумент attr является нулевым указателем.
Наконец, при создании потока мы должны указать функцию, которую он будет выполнять, — начальную функцию потока (thread start function). Поток запускается вызовом этой функции и завершается либо явно (вызовом pthread_exit), либо неявно (возвратом из этой функции). Адрес функции указывается в аргументе func, и вызывается она с единственным аргументом — указателем arg. Если функции нужно передать несколько аргументов, следует упаковать их в структуру и передать ее адрес в качестве единственного аргумента начальной функции.
Обратите внимание на объявления func и arg. Функция принимает один аргумент — указатель типа void, и возвращает один аргумент — такой же указатель. Это дает нам возможность передать потоку указатель на что угодно и получить в ответ такой же указатель.
Функции Posix для работы с потоками обычно возвращают 0 в случае успешного завершения работы и ненулевое значение в случае ошибки. В отличие от большинства системных функций, возвращающих –1 в случае ошибки и устанавливающих значение errno равным коду ошибки, функции Pthread возвращают положительный код ошибки. Например, если pthread_create не сможет создать новый поток из-за превышения системного oгрaничeния на потоки, эта функция вернет значение EAGAIN. Функции Pthread не устанавливают значение переменной errno. Несоответствий при их вызове не возникает, поскольку ни один из кодов ошибок не имеет нулевого значения (<sys/errno.h>).