Системная функция fork() порождает полную копию родительского процесса, и эта процедура является затратной как с точки зрения расходования процессорного времени, так и с точки зрения расходования оперативной памяти. В параллельном программировании зачастую достаточно использования частичной копии процесса. Такая частичная копия называется потоком, <легковесным процессом> или нитью (thread). Нить - это поток команд, который может самостоятельно управляться планировщиком. В отличие от дочернего процесса нить создается в контексте головного процесса, в том же самом адресном пространстве. На однопроцессорных системах планировщик чередует выполнение нитей посредством квантования процессорного времени, а на многопроцессорных системах нити могут выполняться параллельно на различных процессорах. Потоки не требуют специального механизма взаимодействия между подзадачами, поскольку они имеют равноправный доступ к общей памяти родительского процесса.
Для работы с нитями в разных версиях ОС UNIX существовали свои библиотеки. В настоящее время в качестве стандарта для работы с нитями
принята библиотека Pthread [10]. Пользовательский интерфейс библиотеки определен в стандарте POSIX 1003.1 1995, разработанном комитетами по
выпуску стандартов ANSI/IEEE. Библиотека Pthread содержит более 60 функций, которые можно разделить на три категории: функции управления
потоками, функции управления мьютексами (mutex
В программе pi_thread.с представлена версия вычисления числа π с использованием библиотеки Pthread. Программа создает нить, используя функцию pthread_create (), затем назначает уникальный идентификатор переменной типа pthread_t. Головная программа вызывает вычислительную функцию, которая будет выполняться как нить. Функция pthread_exit () используется для завершения <легковесного> процесса. Функция pthread_join () является аналогом функции wait(), но в этом случае любая нить может связываться с любой другой нитью в процессе, так как для нитей нет жесткой связи родитель-потомок.
Поскольку многопоточное приложение выполняет команды одновременно на нескольких процессорах, то доступ к совместно используемой памяти требует синхронизации. Это реализуется с помощью механизма взаимной блокировки (mutex). Этот механизм защищает структуры данных от параллельной модификации. Функции pthread_mutex_init (), pthread_mutex_lock () и pthread_mutex_unlock () используются для этой цели.
Головная программа запускается из командной строки с двумя аргументами: n - числом точек разбиения интервала интегрирования и num_threads - числом порождаемых нитей. Значение этой глобальной переменной может быть присвоено и за пределами программы в виде переменной окружения оболочки Shell. Первая нить вычислительной функции PIworker, получает свой идентификатор i от головного процесса и выполняет n/num_threads часть вычислительного цикла. Поскольку доступ к глобальной переменной pi не должны получить больше чем одна нить одновременно, то операция ее модификации запирается механизмом взаимной блокировки (mutex).Рассмотренный алгоритм реализует следующая программа.
#include <stdio.h> #include <pthread.h> int n, num_threads; double d, pi; pthread_mutex_t reduction_mutex; pthread_t *tid; void *PIworker(void *arg) { int i, myid; double s, x, mypi; myid = *(int *)arg; s = 0.0; for (i=myid+1; i<=n; i+=num_threads) { x = (i-0.5)*d; s += 4.0/(1.0+x*x); } mypi = d*s; pthread_mutex_lock(&reduction_mutex); pi += mypi; pthread_mutex_unlock(&reduction_mutex); pthread_exit(0); } main(int argc, char **argv) { int i; int *id; n = atoi(argv[1]); num_threads = atoi(argv[2]); d = 1.0/n; pi = 0.0; id = (int *) calloc(n,sizeof(int)); tid = (pthread_t *) calloc(num_threads, sizeof(pthread_t)); if(pthread_mutex_init(&reduction_mutex,NULL)) { fprintf(stderr, "Cannot init lock\n"); exit(0); }; for (i=0; i<num_threads; i++) { id[i] = i; if(pthread_create(&tid[i],NULL, PIworker,(void *)&id[i])) { exit(1); }; }; for (i=0; i<num_threads; i++) pthread_join(tid[i],NULL); printf("pi=%.15f\n", pi); }
Следует отметить, что написание многопоточных программ на языке Си довольно трудоемко и требует чрезвычайной аккуратности. При разработке таких приложений программист обязан обеспечить безопасность параллельно выполняемых функций. На сегодняшний день нет полной гарантии, что все функции системных библиотек безопасны для нитей и обеспечивают полную защиту от несанкционированного доступа в системные области памяти. Поэтому рекомендуется для многопоточного программирования использовать средства более высокого уровня, такие как Java или OpenMP.