Потоки создаются функцией pthread_create, определенной в заголовочном файле <pthread.h>.
int pthread_create(&thread1, NULL, thread_func, &size1);
Первый параметр этой функции представляет собой указатель на переменную типа pthread_t, которая служит идентификатором создаваемого потока. Второй параметр, указатель на переменную типа pthread_attr_t, используется для передачи атрибутов потока. Третьим параметром функции pthread_create() должен быть адрес функции потока. Эта функция играет для потока ту же роль, что функция main() – для главной программы. Четвертый параметр функции pthread_create() имеет тип void *. Этот параметр может использоваться для передачи значения, возвращаемого функцией потока. Вскоре после вызова pthread_create() функция потока будет запущена на выполнение параллельно с другими потоками программы. Таким образом, собственно, и создается новый поток. Я говорю, что новый поток запускается «вскоре» после вызова pthread_create() потому, что перед тем как запустить новую функцию потока, нужно выполнить некоторые подготовительные действия, а поток-родитель между тем продолжает выполняться. Непонимание этого факта может привести вас к ошибкам, которые трудно будет обнаружить. Если в ходе создания потока возникла ошибка, функция pthread_create() возвращает ненулевое значение, соответствующее номеру ошибки.
Функция потока должна иметь заголовок вида:
void * func_name(void * arg)
Имя функции, естественно, может быть любым. Аргумент arg, - это тот самый указатель, который передается в последнем параметре функции pthread_create(). Функция потока может вернуть значение, которое затем будет проанализировано заинтересованным потоком, но это не обязательно. Завершение функции потока происходит если:
Функция pthread_exit() представляет собой потоковый аналог функции _exit(). Аргумент функции pthread_exit(), значение типа void *, становится возвращаемым значением функции потока. Как (и кому?) функция потока может вернуть значение, если она не вызывается из программы явным образом? Для того, чтобы получить значение, возвращенное функцией потока, нужно воспользоваться функцией pthread_join(3). У этой функции два параметра. Первый параметр pthread_join(), – это идентификатор потока, второй параметр имеет тип «указатель на нетипизированный указатель». В этом параметре функция pthread_join() возвращает значение, возвращенное функцией потока. Конечно, в многопоточном приложении есть и более простые способы организовать передачу данных между потоками. Основная задача функции pthread_join() заключается, однако, в синхронизации потоков. Вызов функции pthread_join() приостанавливает выполнение вызвавшего ее потока до тех пор, пока поток, чей идентификатор передан функции в качестве аргумента, не завершит свою работу. Если в момент вызова pthread_join() ожидаемый поток уже завершился, функция вернет управление немедленно. Функцию pthread_join() можно рассматривать как эквивалент waitpid(2) для потоков. Эта функция позволяет вызвавшему ее потоку дождаться завершения работы другого потока. Попытка выполнить более одного вызова pthread_join() (из разных потоков) для одного и того же потока приведет к ошибке.
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <pthread.h>
void inline swap(int *i, int *j)
{
int t;
t = *i;
*i = *j;
*j = t;
}
void reverse(int * v, int n)
{
int i;
for (i = 0; i < (n/2); i++)
swap(&v[i], &v[n-1-i]);
}
int next_permutation(int * v, int n)
{
int i, j;
i = n - 1;
while ((i > 1) && (v[i] < v[i-1])) i--;
if (v[i] > v[i-1]) {
j = n - 1;
while (v[j] < v[i-1]) j--;
swap(&v[j], &v[i-1]);
reverse(&v[i], n-i);
return 1;
}
return 0;
}
void print_vect(int * v, int n)
{
int i;
for (i = 0; i < n - 1; i++)
printf("%i ", v[i]);
printf("%i\n", v[n-1]);
}
void * thread_func(void *arg)
{
int i;
int * v;
int size = * (int *) arg;
v = malloc(sizeof(int)*size);
for(i = 0; i < size; i++) v[i] = i+1;
print_vect(v, size);
while(next_permutation(v, size)) {
print_vect(v, size);
sync();
}
free(v);
}
int main(int argc, char * argv[])
{
int size1, size2, result;
pthread_t thread1, thread2;
size1 = 4;
result = pthread_create(&thread1, NULL, thread_func, &size1);
if (result != 0) {
perror("Creating the first thread");
return EXIT_FAILURE;
}
size2 = 3;
result = pthread_create(&thread2, NULL, thread_func, &size2);
if (result != 0) {
perror("Creating the second thread");
return EXIT_FAILURE;
}
result = pthread_join(thread1, NULL);
if (result != 0) {
perror("Joining the first thread");
return EXIT_FAILURE;
}
result = pthread_join(thread2, NULL);
if (result != 0) {
perror("Joining the second thread");
return EXIT_FAILURE;
}
printf("Done\n");
return EXIT_SUCCESS;
}
Строка для компиляции проекта будет выглядеть приблизительно следующим образом:
gcc threads.c -o thread –lpthread
Рассмотрим сначала функцию thread_func(). Как вы, конечно, догадались, это и есть функция потока. Наша функция потока очень проста. В качестве аргумента ей передается указатель на переменную типа int, в которой содержится номер потока. Функция потока распечатывает этот номер несколько раз с интервалом в одну секунду и завершает свою работу. В функции main() вы видите две переменных типа pthread_t. Мы собираемся создать два потока и у каждого из них должен быть свой идентификатор. Вы также видите две переменные типа int, id1 и id2, которые используются для передачи функциям потоков их номеров. Сами потоки создаются с помощью функции pthread_create().В этом примере мы не модифицируем атрибуты потоков, поэтому во втором параметре в обоих случаях передаем NULL. Вызывая pthread_create() дважды, мы оба раза передаем в качестве третьего параметра адрес функции thread_func, в результате чего два созданных потока будут выполнять одну и ту же функцию. Функция, вызываемая из нескольких потоков одновременно, должна обладать свойством реентерабельности (этим же свойством должны обладать функции, допускающие рекурсию). Реентерабельная функция, это функция, которая может быть вызвана повторно, в то время, когда она уже вызвана (отсюда и происходит ее название). Реентерабельные функции используют локальные переменные (и локально выделенную память) в тех случаях, когда их не-реентерабельные аналоги могут воспользоваться глобальными переменными.
Мы вызываем последовательно две функции pthread_join() для того, чтобы дождаться завершения обоих потоков. Если мы хотим дождаться завершения всех потоков, порядок вызова функций pthread_join() для разных потоков, очевидно, не имеет значения.
Перевести приложение, разработанное в теме №1 на платформу ОС Linux
Изучить механизмы синхронизации в ОС Linux.
1. Рассмотреть представленный пример, и разработать приложение на его основе.
2. Разработать и реализовать алгоритм решения второго задания, с учетом разделения вычислений между несколькими потоками. Определить критические фрагменты алгоритма и защитить их мьютексами.
1. W. R. Stevens, S. A. Rago, Advanced Programming in the UNIX® Environment: Second Edition, Addison Wesley Professional, 2005
2. D. P. Bovet, M. Cesati, Understanding the Linux Kernel, 3rd Edition, O'Reilly, 2005
3. А. Боровский. «Потоки». http://www.citforum.ru/programming/unix/threads/
Одним из первых механизмов, предложенных для синхронизации поведения процессов, стали семафоры, концепцию которых описал Дейкстра (Dijkstra) в 1965 году.
емафор представляет собой целую переменную, принимающую неотрицательные значения, доступ любого процесса к которой, за исключением момента ее инициализации, может осуществляться только через две атомарные операции: P (от датского слова proberen — проверять) и V (от verhogen — увеличивать). Классическое определение этих операций выглядит следующим образом:
P(S): | пока S == 0 процесс блокируется; S = S – 1; |
V(S): | S = S + 1; |
Эта запись означает следующее: при выполнении операции P над семафором S сначала проверяется его значение. Если оно больше 0, то из S вычитается 1. Если оно меньше или равно 0, то процесс блокируется до тех пор, пока S не станет больше 0, после чего из S вычитается 1. При выполнении операции V над семафором S к его значению просто прибавляется 1.
В примере мы рассмотрим семафоры POSIX, которые специально предназначены для работы с потоками. Все объявления функций и типов, относящиеся к этим семафорам, можно найти в файле /usr/include/nptl/semaphore.h. Семафоры POSIX создаются (инициализируются) с помощью функции sem_init(3). Первый параметр функции sem_init() – указатель на переменную типа sem_t, которая служит идентификатором семафора. Второй параметр - pshared – указывает что данный семафор будет разграничивать потоки внутри одного процесса или процессы. В наших примерах не используется, и мы оставим его равным нулю. В третьем параметре функции sem_init() передается значение, которым инициализируется семафор. Дальнейшая работа с семафором осуществляется с помощью функций sem_wait(3) и sem_post(3). Единственным аргументом функции sem_wait() служит указатель на идентификатор семафора. Функция sem_wait() приостанавливает выполнение вызвавшего ее потока до тех пор, пока значение семафора не станет большим нуля, после чего функция уменьшает значение семафора на единицу и возвращает управление. Функция sem_post() увеличивает значение семафора, идентификатор которого был передан ей в качестве параметра, на единицу. Когда приложение больше не нуждается в семафорах - мы вызываем функцию sem_destroy(3) для удаления семафора и высвобождения его ресурсов.