Многопоточность является важнейших аспектов в современном программировании. Ключевым понятием при работе с многоопоточностью является поток. Поток предствляет некоторую часть кода программы. При выполнении программы каждому потоку выделяется определенный квант времени. И при помощи многопоточности мы можем выделить в приложении несколько потоков, которые будут выполнять различные задачи одновременно. Если у нас, допустим, графическое приложение, которое посылает запрос к какому-нибудь серверу или считывает и обрабатывает огромный файл, то без многопоточности у нас бы блокировался графический интерфейс на время выполнения задачи. А благодаря потокам мы можем выделить отправку запроса или любую другую задачу, которая может долго обрабатываться, в отдельный поток. Поэтому, к примеру, клиент-серверные приложения (и не только они) практически не мыслимы без многопоточности.
В языке Си поток представляет объект типа pthread_t, который определен в заголовочном файле pthread.h. А для создания потока применяется функция pthread_create(), которая имеет следующий прототип:
int pthread_create( pthread_t *thread, const pthread_attr_t *attr, void *(*routine) (void *), void *arg);
Эта функция принимает следующие параметры:
*thread
: указатель на объект pthread_t, который инициализируется.
*attr
: указатель на набор атрибутов потока в виде объекта pthread_attr_t. Если атрибуты не нужны, можно передать NULL
*routine
: указатель на функцию, которая запускается в потоке. Подобная функция должна принимать в качестве параметра указатель
void*
, вместо которого можно передать указатель на любой объект. Однако допускается только один аргумент. Соответственно если надо передать несколько
значений, то можно оформить их в массив или структуру.
Возвращаемое значение функции также должно представлять указатель void*
. Соответственно также можно возвратить указатель на любой объект. Данное возвращаемое
значение будет выступать в качестве результата потока. Опять же здесь мы можем возвратить значение NULL
, если оно нам не нужно.
*arg
: указатель void*, через который передается значение в запускаемую функцию.
Например, определим следующую программу:
#include <pthread.h> #include <stdio.h> void* some_work(void* arg) { for(int i = 0; i < 5; ++i) { puts(arg); } return NULL; } int main(void) { pthread_t thread1, thread2; // создаем потоки pthread_create(&thread1, NULL, some_work, "Hello World" ); pthread_create(&thread2, NULL, some_work, "Hello METANIT.COM" ); printf("End...\n"); return 0; }
В функции main сначала объявляем две переменных, которые будут представлять потоки:
pthread_t thread1, thread2;
Затем инициализируем их:
pthread_create(&thread1, NULL, some_work, "Hello World" ); pthread_create(&thread2, NULL, some_work, "Hello METANIT.COM" );
Инициализация обоих потоков аналогична. В качестве первого параметра передаем в функцию адрес переменной. Атрибуты потока нам сейчас не важны, поэтому вторым аргументом передаем значение NULL. В качестве выполняемой функции передаем функцию some_work, которая определена выше. Четвертое значение - аргумент для функции - в нашем случае это две разных строки.
Функция some_work просто в цикле выводит пять раз переданную в качестве параметра строку:
void* some_work(void* arg) { for(int i = 0; i < 5; ++i) { puts(arg); } return NULL; }
Результат функции здесь нам не важен, поэтому возвращаем NULL
Скомпилируем и запустим программу:
eugene@Eugene:~/Documents/metanit$ gcc -Wall -pedantic -std=c2x main.c -o main eugene@Eugene:~/Documents/metanit$ ./main End... eugene@Eugene:~/Documents/metanit$
На примере моего вывода можно увидеть, что потоки не отработали до конца - ни один из них не вывел переданную в него строку. В реальности вывод в данном случае недетерминирован. Например, он мог бы быть следующим:
eugene@Eugene:~/Documents/metanit$ ./main End... Hello World Hello World Hello World Hello World Hello World eugene@Eugene:~/Documents/metanit$
В любом случае мы сталкиваемся с проблемой - потоки не отрабатывают полностью, а программа завершается. Дело в том, что оператор return в функции main приводит к завершению программы и соответственно процесса программы и всех ее потоков. Если поток не успел отработать до завершения процесса, то он тоже завершается.
Функция pthread_exit() позволяет подождать, пока не завершаться все потоки, и после их завершения завершает вызывающий поток (в нашем случае главный поток программы, в котором запущена функция main) и позволяет выполниться всем другим потокам. Эта функция принимает один параметр - возвращаемый результат. Если он не важен, то можно передать NULL. Далее мы посмотрим, где это значение может пригодиться.
Итак, изменим код программы, применив функцию pthread_exit():
#include <pthread.h> #include <stdio.h> void* some_work(void* arg) { for(int i = 0; i < 5; ++i) { puts(arg); } return NULL; } int main(void) { pthread_t thread1, thread2; // создаем потоки pthread_create(&thread1, NULL, some_work, "Hello World" ); pthread_create(&thread2, NULL, some_work, "Hello METANIT.COM" ); pthread_exit(NULL); printf("End...\n"); return 0; }
Теперь оба потока будут выполняться полностью:
eugene@Eugene:~/Documents/metanit$ ./main Hello World Hello World Hello World Hello World Hello World Hello METANIT.COM Hello METANIT.COM Hello METANIT.COM Hello METANIT.COM Hello METANIT.COM eugene@Eugene:~/Documents/metanit$
Однако, поскольку функция pthread_exit() завершает вызывающий поток (поток функции main), то консоль НЕ выведет строку "End...".
Чтобы сделать более показательной одновременную работу потоков, применим функцию sleep(). Она определена в заголовочном файле "unistd.h" и позволяет приостановить текущий поток на определенное количество секунд. Так, изменим код программы следующим образом:
#include <pthread.h> #include <stdio.h> #include <unistd.h> void* some_work(void* arg) { for(int i = 0; i < 5; ++i) { puts(arg); sleep(1); // задержка в 1 секунду } return NULL; } int main(void) { pthread_t thread1, thread2; pthread_create(&thread1, NULL, some_work, "Hello World" ); pthread_create(&thread2, NULL, some_work, "Hello METANIT.COM" ); pthread_exit(NULL); printf("End...\n"); return 0; }
В данном случае применяем задержку в 1 секунду, благодаря чему потоки могут выводить строку на консоль почти по перемено. Например:
eugene@Eugene:~/Documents/metanit$ ./main Hello World Hello METANIT.COM Hello METANIT.COM Hello World Hello World Hello METANIT.COM Hello World Hello METANIT.COM Hello World Hello METANIT.COM eugene@Eugene:~/Documents/metanit$
Точный вывод, какой поток начнет первым выведлет строку на консоль опять же неопределен.