В этой статье мы научимся создавать, запускать и останавливать потоки в POSIX-совместимых системах на Plain C.
Сначала вкратце рассмотрим, что такое поток. Воспользуемся последним стандартом POSIX.1-2017, чтобы разобраться в терминологии:
Live Process
An address space with one or more threads executing within that address space, and the required system resources for those threads.
Здесь написано следующее: процесс есть адресное пространство с одним или более потоков выполняющимися в нём и необходимыми системными ресурсами для этих потоков.
Приписка «живой» нас не интересует, в стандарте используется термин Live Process для отличия его от Zombie Process. В этой статье мы будем говорить только о «живых».
Так же в стандарте написано, что многие системные ресурсы определённые стандартом являются общими для всех потоков в процессе. Эти ресурсы включают в себя ID процесса, ID родительского процесса, ID группы процессов, текущую директорию, файловые дескрипторы и пр.
Приписка «живой» нас не интересует, в стандарте используется термин Live Process для отличия его от Zombie Process. В этой статье мы будем говорить только о «живых».
Так же в стандарте написано, что многие системные ресурсы определённые стандартом являются общими для всех потоков в процессе. Эти ресурсы включают в себя ID процесса, ID родительского процесса, ID группы процессов, текущую директорию, файловые дескрипторы и пр.
Thread
A single flow of control within a process. Each thread has its own thread ID, scheduling priority and policy, errno value, floating point environment, thread-specific key/value bindings, and the required system resources to support a flow of control. Anything whose address may be determined by a thread, including but not limited to static variables, storage obtained via malloc(), directly addressable storage obtained through implementation-defined functions, and automatic variables, are accessible to all threads in the same process.
Что означает: поток есть одно «течение контроля» (нам, русскоязычным, привычнее термин «поток выполнения»). Каждый поток имеет собственный ID, приоритет планировщика и политики, значение errno и пр.. Всё, чей адрес может быть определён потоком, включая, но не ограничиваясь статическими переменными, динамически выделенная память и др. доступны всем потокам одного процесса.
Переведя это на русский язык окончательно, получим следующее: процесс — это инстанция программы, которая может содержать один или более потоков, поток — это блок кода, работающий параллельно с основным и с другими потоками.
Я считаю, что разобраться в этом было важно, так как всё (во всяком случае системное) программирование заключается в распределении ресурсов операционной системы и управлении ими, а то, что мы выше почерпнули из стандарта даёт следующие выводы:
Переведя это на русский язык окончательно, получим следующее: процесс — это инстанция программы, которая может содержать один или более потоков, поток — это блок кода, работающий параллельно с основным и с другими потоками.
Я считаю, что разобраться в этом было важно, так как всё (во всяком случае системное) программирование заключается в распределении ресурсов операционной системы и управлении ими, а то, что мы выше почерпнули из стандарта даёт следующие выводы:
- Мы можем пользоваться (читать, писать) ресурсами в своих потоках в пределах одного процесса без использования т.н. IPC (методы межпроцессного взаимодействия).
- Мы не можем обращаться к ресурсам других процессов без применения IPC. Даже из т.н. основного потока выполнения (main (), например), даже из под root'а. А POSIX хоть и предоставляет инструменты IPC, но это достаточно сложные механизмы.
Теперь рассмотрим технологию создания второго и последующих потоков. На всякий случай, напомню, первый у нас всегда есть — его создала ОС при запуске вашей программы (возможно, о том как это происходит я напишу). Этот поток я называю основным.
В самой по себе ANSI C нет механизмов работы с многопоточностью. Есть различные реализации работы с потоками. Например, специфичная GNU Portable Threads, Protothreads и пр. Первая преследовала цель максимальной портируемости, вторая — легковесности. Мы будем рассматривать самый распространённый (хотя и не самый оптимальный, почему я распишу в последующих статьях) стандарт, т.н. POSIX Threads (pthread). POSIX Threads API реализован в библиотеке libpthread.
В самой по себе ANSI C нет механизмов работы с многопоточностью. Есть различные реализации работы с потоками. Например, специфичная GNU Portable Threads, Protothreads и пр. Первая преследовала цель максимальной портируемости, вторая — легковесности. Мы будем рассматривать самый распространённый (хотя и не самый оптимальный, почему я распишу в последующих статьях) стандарт, т.н. POSIX Threads (pthread). POSIX Threads API реализован в библиотеке libpthread.
Приведём пример простой многопоточной программы:
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
/*
* Здесь мы проверяем, есть ли в системе POSIX Threads
*/
#ifndef _POSIX_THREADS
#error No POSIX Threads!
#endif
/*
* Функция, код которой будет выполняться в новом потоке.
* Обратите внимание на то, что мы можем передать сюда
* указатель на область памяти. Это можно использовать
* как механизм передачи некоторых начальных данных потоку.
*/
void *ThreadRoutine (void *_Arg)
{
printf ("String argument passed to thread routine: %s\n",
(char *)_Arg);
while (0)
{
sleep (1);
printf ("Thread is doing infinite work...\n");
}
printf ("Thread is about to exit normally...\n");
/*
* Завершить выполнение потока можно двумя способами:
* return и pthread_exit () с возвращаемым нами значением
* в параметре. Они эквивалентны за исключением некоторой
* (незначительной) разницы в быстродействии, что, в свою
* очередь, связано с функционалом вызова обработчиков
* завершения потока. Но мы их здесь не будем рассматривать.
*/
/* return (void *)(uintptr_t)0xC0FFEE; */
pthread_exit ((void *)(uintptr_t)0xC0FFEE);
/*
* А что будет, если вызвать exit () из функции потока?
* Приложение завершится, так как exit () это не возврат
* из функции, а более сложный системный вызов, который
* "корректно" закрывает приложение вместе со всеми
* потоками. Недопонимание этой разницы может возникнуть
* из-за того, что мы часто завершаем наше приложение
* простым return'ом в main'е. На самом деле, при
* "возврате" из main'а компилятор подставляет функционал
* "корректного" закрытия программы. Поэтому завершать
* работу потока нужно только через return/pthread_exit ().
* Вы можете заменить return/pthread_exit () на exit (0)
* и посмотреть что произойдёт - вы не увидите вывода
* "Thread return".
*/
/* exit (0); */
}
int main (int _argc, char * _argv[])
{
/* Идентификатор потока, на Linux'е это целое число, на
* Mac OS это структура.
*/
pthread_t thread_id;
/* Возвращаемое значение функции потока */
void *ThreadRet = NULL;
/*
* Функция pthread_create создаёт поток. Первый параметр -
* идентификатор потока (для дальнейших операций с ним).
* Второй - аттрибуты, третий - функция потока, четвёртый -
* аргумент (мы передадим туда название исполняемого файла).
*/
pthread_create (&thread_id, NULL, ThreadRoutine, _argv[0]);
/*
* Функция pthread_join ожидает выхода из функции потока.
* Так же здесь мы получаем возвращаемое значение из потока.
* Перед тем как раскомментировать этот вызов, убедитесь, что
* цикл while в функции потока выключен (например, поставьте
* while (0)). Иначе программа зависнет в ожидании завершения
* потока. Ведь pthread_join () ждёт корректного завершения
* работы потока.
*/
pthread_join (thread_id, &ThreadRet);
/*
* Перед тем как включить этот эксперимент, поставьте в функции
* потока while (1), чтобы включить его бесконечную работу.
* Функция pthread_cancel () аварийно завершает поток.
* Дадим ему 5 секунд "пожить" и завершим.
*/
/* sleep (5); */
/* pthread_cancel (thread_id); */
/* Выводим значение, возвращённое из функции потока */
printf ("Thread return: %lX\n", (uintptr_t)ThreadRet);
return EXIT_SUCCESS;
}
NB!
Обратите внимание на то, как при возврате из функции потока, я создаю указатель на некий адрес. Это своеобразный трюк. Чтобы не выделять память для возвращаемого значения, с последующим её освобождением, но вернуть простой int, как результат работы, я вернул указатель на адрес памяти, который придумал из головы. Вы можете возвращать указатели на любые адреса, и анализировать их по возврату из pthread_join (). Но стоит помнить, что обращаться по таким, вымышленным адресам нельзя.
Забрать пример:
https://gitlab.com/daftsoft/pthread1.git
Собирать программы с POSIX Threads нужно с указанием библиотеки pthread:
gcc -std=c89 -pedantic -Wall pthread1.c -lpthread -o pthread1
К слову, в Linux'е не всегда был реализован стандарт POSIX Threads. Как же были реализованы потоки в Linux'е до «принятия» POSIX Threads? В то время (c 2.0 до 2.4 включительно) в Linux'е была своя технология, называлась она LinuxThreads. В общих чертах, она заключалась в копировании кода процесса с предоставлением доступа к памяти родительского процесса. При этом, у потоков были разные идентификаторы процессов (PID) и взаимодействие между ними осуществлялось посредством одного из механизмов IPC — сигналов (USR1 и USR2), что исключало возможность использовать этих сигналов разработчиками. То есть, по современным меркам (или по меркам стандарта), это были не совсем потоки. Всё это несовершенство продолжалось до начала 2000-х годов, когда LinuxThreads была заменена библиотекой потоков POSIX (Native POSIX Thread Library, NPTL), реализующей стандарт POSIX Threads в Linux.
Представленного примера достаточно для того, чтобы начать писать свои первые многопоточные программы.
В следующей статье мы, на примере, рассмотрим как действительно работают «общие ресурсы» в потоках в пределах одного процесса, а так же иные аспекты управления потоками.
No comments:
Post a Comment