- Программирование C в Linux — потоки pthreads
- How to Create Threads in Linux (With a C Example Program)
- Thread Identification
- Thread Creation
- A Practical Thread Example
- Create threads с linux
- RETURN VALUES
- ERRORS
- EXAMPLES
- ATTRIBUTES
- SEE ALSO
- NOTES
- Pthreads: Потоки в русле POSIX
- Общие сведения
- Отображение потоков в режим ядра
- Потоки POSIX
- Создание потока
- Завершение потока
- Ожидание потока
- Досрочное завершение потока
- Отсоединение потока
- Потоки versus процессы
Программирование C в Linux — потоки pthreads
Многопоточность в программировании является важным механизмом в наше время. Поэтому я решил посвятить несколько статей этой теме.
В семействах ОС Windows — каждая программа запускает один процесс выполнения, в котором находится как минимум один поток (нить). В процессе может находиться множество потоков, между которыми делится процессорное время. Один процесс не может напрямую обратиться к памяти другого процесса, а потоки же разделяют одно адресное пространство одного процесса. То есть в Windows — процесс это совокупность потоков.
В Linux же немного по-другому. Сущность процесса такая же, как и в Windows — это исполняемая программа со своими данными. Но вот поток в Linux является отдельным процессом (можно встретить название как «легковесный процесс», LWP). Различие такое же — процесс отдельная программа со своей памятью, не может напрямую обратиться к памяти другого процесса, а вот поток, хоть и отдельный процесс, имеет доступ к памяти процесса-родителя [2]. LWP процессы создаются с помощью системного вызова clone() с указанием определенных флагов.
Но также имеется такая вещь, которая называется «POSIX Threads» — библиотечка стандарта POSIX, которая организует потоки (они же нити) внутри процесса. Т.е тут уже распараллеливание происходит в рамках одного процесса.
И тут встает вопрос различия терминов «поток», «процесс», «нить» и т.д. Проблема в том, что в англоязычной литературе данные термины определяются однозначно, у нас же с нашим великим и могучим имеются противоречия, что может привести к дикому диссонансу.
Но это все в общих чертах, для более точной информации следует обратиться к соответствующей литературе, либо к официальной документации, можно почитать man’ы. В конце статьи я приведу несколько полезных ссылок на ресурсы, где более подробно расписано как все работает, а пока займемся практикой.
Я рассмотрю два варианта «распараллеливания» программы — создания потока/нити с помощью функций из pthread.h (POSIX Threads), либо создание отдельного процесса с помощью функции fork().
Сегодня рассмотрим потоки из библиотеки pthread.
Шаблон кода для работы с потоками выглядит следующим образом:
Источник
How to Create Threads in Linux (With a C Example Program)
In the part I of the Linux Threads series, we discussed various aspects related to threads in Linux.
In this article we will focus on how a thread is created and identified. We will also present a working C program example that will explain how to do basic threaded programming.
Linux Threads Series: part 1, part 2 (this article), part 3.
Thread Identification
Just as a process is identified through a process ID, a thread is identified by a thread ID. But interestingly, the similarity between the two ends here.
- A process ID is unique across the system where as a thread ID is unique only in context of a single process.
- A process ID is an integer value but the thread ID is not necessarily an integer value. It could well be a structure
- A process ID can be printed very easily while a thread ID is not easy to print.
The above points give an idea about the difference between a process ID and thread ID.
Thread ID is represented by the type ‘pthread_t’. As we already discussed that in most of the cases this type is a structure, so there has to be a function that can compare two thread IDs.
So as you can see that the above function takes two thread IDs and returns nonzero value if both the thread IDs are equal or else it returns zero.
Another case may arise when a thread would want to know its own thread ID. For this case the following function provides the desired service.
So we see that the function ‘pthread_self()’ is used by a thread for printing its own thread ID.
Now, one would ask about the case where the above two function would be required. Suppose there is a case where a link list contains data for different threads. Every node in the list contains a thread ID and the corresponding data. Now whenever a thread tries to fetch its data from linked list, it first gets its own ID by calling ‘pthread_self()’ and then it calls the ‘pthread_equal()’ on every node to see if the node contains data for it or not.
An example of the generic case discussed above would be the one in which a master thread gets the jobs to be processed and then it pushes them into a link list. Now individual worker threads parse the linked list and extract the job assigned to them.
Thread Creation
Normally when a program starts up and becomes a process, it starts with a default thread. So we can say that every process has at least one thread of control. A process can create extra threads using the following function :
The above function requires four arguments, lets first discuss a bit on them :
- The first argument is a pthread_t type address. Once the function is called successfully, the variable whose address is passed as first argument will hold the thread ID of the newly created thread.
- The second argument may contain certain attributes which we want the new thread to contain. It could be priority etc.
- The third argument is a function pointer. This is something to keep in mind that each thread starts with a function and that functions address is passed here as the third argument so that the kernel knows which function to start the thread from.
- As the function (whose address is passed in the third argument above) may accept some arguments also so we can pass these arguments in form of a pointer to a void type. Now, why a void type was chosen? This was because if a function accepts more than one argument then this pointer could be a pointer to a structure that may contain these arguments.
A Practical Thread Example
Following is the example code where we tried to use all the three functions discussed above.
So what this code does is :
- It uses the pthread_create() function to create two threads
- The starting function for both the threads is kept same.
- Inside the function ‘doSomeThing()’, the thread uses pthread_self() and pthread_equal() functions to identify whether the executing thread is the first one or the second one as created.
- Also, Inside the same function ‘doSomeThing()’ a for loop is run so as to simulate some time consuming work.
Now, when the above code is run, following was the output :
As seen in the output, first thread is created and it starts processing, then the second thread is created and then it starts processing. Well one point to be noted here is that the order of execution of threads is not always fixed. It depends on the OS scheduling algorithm.
Note: The whole explanation in this article is done on Posix threads. As can be comprehended from the type, the pthread_t type stands for POSIX threads. If an application wants to test whether POSIX threads are supported or not, then the application can use the macro _POSIX_THREADS for compile time test. To compile a code containing calls to posix APIs, please use the compile option ‘-pthread’.
Источник
Create threads с linux
The pthread_create() function is used to create a new thread, with attributes specified by attr , within a process. If attr is NULL, the default attributes are used. (See pthread_attr_init (3C)). If the attributes specified by attr are modified later, the thread’s attributes are not affected. Upon successful completion, pthread_create() stores the ID of the created thread in the location referenced by thread .
The thread is created executing start_routine with arg as its sole argument. If the start_routine returns, the effect is as if there was an implicit call to pthread_exit() using the return value of start_routine as the exit status. Note that the thread in which main() was originally invoked differs from this. When it returns from main() , the effect is as if there was an implicit call to exit() using the return value of main() as the exit status.
The signal state of the new thread is initialised as follows: o The signal mask is inherited from the creating thread. o The set of signals pending for the new thread is empty.
Default thread creation:
This would have the same effect as:
User-defined thread creation: To create a thread that is scheduled on a system-wide basis, use:
To customize the attributes for POSIX threads, see pthread_attr_init (3C).
A new thread created with pthread_create() uses the stack specified by the stackaddr attribute, and the stack continues for the number of bytes specified by the stacksize attribute. By default, the stack size is 1 megabyte for 32-bit processes and 2 megabyte for 64-bit processes (see pthread_attr_setstacksize (3C)). If the default is used for both the stackaddr and stacksize attributes, pthread_create() creates a stack for the new thread with at least 1 megabyte for 32-bit processes and 2 megabyte for 64-bit processes. (For customizing stack sizes, see NOTES ).
If pthread_create() fails, no new thread is created and the contents of the location referenced by thread are undefined.
RETURN VALUES
If successful, the pthread_create() function returns 0 . Otherwise, an error number is returned to indicate the error.
ERRORS
The pthread_create() function will fail if:
EAGAIN The system lacked the necessary resources to create another thread, or the system-imposed limit on the total number of threads in a process PTHREAD_THREADS_MAX would be exceeded.
EINVAL The value specified by attr is invalid.
EPERM The caller does not have appropriate permission to set the required scheduling parameters or scheduling policy.
EXAMPLES
Example 1 Example of concurrency with multithreading
The following is an example of concurrency with multithreading. Since POSIX threads and Solaris threads are fully compatible even within the same process, this example uses pthread_create() if you execute a.out 0 , or thr_create() if you execute a.out 1 .
Five threads are created that simultaneously perform a time-consuming function, sleep( 10 ) . If the execution of this process is timed, the results will show that all five individual calls to sleep for ten-seconds completed in about ten seconds, even on a uniprocessor. If a single-threaded process calls sleep( 10 ) five times, the execution time will be about 50-seconds.
The command-line to time this process is:
POSIX threading /usr/bin/time a.out 0
Solaris threading /usr/bin/time a.out 1
If main() had not waited for the completion of the other threads (using pthread_join (3C) or thr_join (3C)), it would have continued to process concurrently until it reached the end of its routine and the entire process would have exited prematurely. See exit (2).
ATTRIBUTES
See attributes (5) for descriptions of the following attributes:
|
SEE ALSO
NOTES
Multithreaded application threads execute independently of each other, so their relative behavior is unpredictable. Therefore, it is possible for the thread executing main() to finish before all other user application threads. The pthread_join (3C)function, on the other hand, must specify the terminating thread (IDs) for which it will wait.
A user-specified stack size must be greater than the value PTHREAD_STACK_MIN . A minimum stack size may not accommodate the stack frame for the user thread function start_func . If a stack size is specified, it must accommodate start_func requirements and the functions that it may call in turn, in addition to the minimum requirement.
It is usually very difficult to determine the runtime stack requirements for a thread. PTHREAD_STACK_MIN specifies how much stack storage is required to execute a NULL start_func . The total runtime requirements for stack storage are dependent on the storage required to do runtime linking, the amount of storage required by library runtimes (as printf() ) that your thread calls. Since these storage parameters are not known before the program runs, it is best to use default stacks. If you know your runtime requirements or decide to use stacks that are larger than the default, then it makes sense to specify your own stacks.
Источник
Pthreads: Потоки в русле POSIX
Современные операционные системы и микропроцессоры уже давно поддерживает многозадачность и вместе с тем, каждая из этих задач может выполняться в несколько потоков. Это дает ощутимый прирост производительности вычислений и позволяет лучше масштабировать пользовательские приложения и сервера, но за это приходится платить цену — усложняется разработка программы и ее отладка.
В этой статье мы познакомимся с POSIX Threads для того, чтобы затем узнать как это все работает в Linux. Не заходя в дебри синхронизации и сигналов, рассмотрим основные элементы Pthreads. Итак, под капотом потоки.
Общие сведения
Множественные нити исполнения в одном процессе называют потоками и это базовая единица загрузки ЦПУ, состоящая из идентификатора потока, счетчика, регистров и стека. Потоки внутри одного процесса делят секции кода, данных, а также различные ресурсы: описатели открытых файлов, учетные данные процесса сигналы, значения umask , nice , таймеры и прочее.
У всех исполняемых процессов есть как минимум один поток исполнения. Некоторые процессы этим и ограничиваются в тех случаях, когда дополнительные нити исполнения не дают прироста производительности, но только усложняют программу. Однако таких программ с каждым днем становится относительно меньше.
В чем польза множественных потоков исполнения? Возьмем какой-нибудь загруженный веб сервер, например habrahabr.ru. Если бы сервер создавал отдельный процесс для обслуживания каждого http запроса, мы бы ожидали вечно пока загрузится наша страница. Создания нового процесса — дорогостоящее удовольствие для ОС. Даже учитывая оптимизацию за счет копирования при записи, системные вызовы fork и exec создают новые копии страниц памяти и списка файловых описателей. В целом ядро ОС может создать новый поток на порядок быстрее, чем новый процесс.
Ядро задействует копирование при записи для страниц с данными, сегментов памяти родительского процесса содержащие стек и кучу. Вследствие того, что процессы часто выполняют вызов fork и сразу после этого exec , копирование их страниц во время выполнения вызова fork становится ненужной расточительностью — их все равно приходится отбрасывать после выполнения exec . Сперва записи таблицы страниц указывают на одни и те же страницы физической памяти родительского процесса, сами же страницы маркируются только для чтения. Копирование страницы происходит ровно в тот момент, когда требуется ее изменить.
Таблицы страниц до и после изменения общей страницы памяти во время копирования при записи.
Существует закономерность между количеством параллельных нитей исполнения процесса, алгоритмом программы и ростом производительности. Это зависимость называется Законом Амдаля.
Закон Амдаля для распараллеливания процессов.
Используя уравнение, показанное на рисунке, можно вычислить максимальное улучшение производительности системы, использующей N процессоров и фактор F, который указывает, какая часть системы не может быть распараллелена. Например 75% кода запускается параллельно, а 25% — последовательно. В таком случае на двухядерном процессоре будет достигнуто 1.6 кратное ускорение программы, на четырехядерном процессоре — 2.28571 кратное, а предельное значение ускорения при N стремящемся к бесконечности равно 4.
Отображение потоков в режим ядра
Практически все современные ОС — включая Windows, Linux, Mac OS X, и Solaris — поддерживают управление потоками в режиме ядра. Однако потоки могут быть созданы не только в режиме ядра, но и в режиме пользователя. При использовании этого уровня ядро не знает о существовании потоков — все управление потоками реализуется приложением с помощью специальных библиотек. Пользовательские потоки по разному отображаются на потоки в режиме ядра. Всего существует три модели, из которых 1:1 является наиболее часто используемой.
Отображение N:1
В данной модели несколько пользовательских потоков отображаются на один поток ядра ОС. Все управление потоками осуществляет особая пользовательская библиотека, и в этом преимущество такого подхода. Недостаток же в том, что если один единственный поток выполняет блокирующий вызов, то тогда тормозится весь процесс. Предыдущие версии Solaris OS использовали такую модель, но затем вынуждены были от нее отказаться.
Отображение 1:1
Это самая проста модель, в которой каждый поток созданный в каком-нибудь процессе непосредственно управляется планировщиком ядра ОС и отображается на один единственный поток в режиме ядра. Чтобы приложение не плодило бесконтрольно потоки, перегружая ОС, вводят ограничение на максимальное количество потоков поддерживаемых в ОС. Данный способ отображения потоков поддерживают ОС Linux и Windows.
Отображение M:N
При таком подходе M пользовательских потоков мультиплексируются в такое же или меньшее N количество потоков ядра. Преодолеваются негативные эффекты двух других моделей: нити по-настоящему исполняются параллельно и нет необходимости в ОС вводить ограничения на их общее количество. Вместе с тем данную модель довольно трудно реализовать с точки зрения программирования.
Потоки POSIX
В конце 1980-х и начале 1990-х было несколько разных API, но в 1995 г. POSIX.1c стандартизовал потоки POSIX, позже это стало частью спецификаций SUSv3. В наше время многоядерные процессоры проникли даже в настольные ПК и смартфоны, так что у большинства машин есть низкоуровневая аппаратная поддержка, позволяющая им одновременно выполнять несколько потоков. В былые времена одновременное исполнение потоков на одноядерных ЦПУ было лишь впечатляюще изобретательной, но очень эффективной иллюзией.
Pthreads определяет набор типов и функций на Си.
- pthread_t — идентификатор потока;
- pthread_mutex_t — мютекс;
- pthread_mutexattr_t — объект атрибутов мютекса
- pthread_cond_t — условная переменная
- pthread_condattr_t — объект атрибута условной переменной;
- pthread_key_t — данные, специфичные для потока;
- pthread_once_t — контекст контроля динамической инициализации;
- pthread_attr_t — перечень атрибутов потока.
В традиционном Unix API код последней ошибки errno является глобальной int переменной. Это однако не годится для программ с множественными нитями исполнения. В ситуации, когда вызов функции в одном из исполняемых потоков завершился ошибкой в глобальной переменной errno , может возникнуть состояние гонки из-за того, что и остальные потоки могут в данный момент проверять код ошибки и оконфузиться. В Unix и Linux эту проблему обошли тем, что errno определяется как макрос, задающий для каждой нити собственное изменяемое lvalue .
Из man errno
Переменная errno определена в стандарте ISO C как изменяемое lvalue int и не объявляемая явно; errno может быть и макросом. Переменная errno является локальным значением нити; её изменение в одной нити не влияет на её значение в другой нити.
Создание потока
В начале создается потоковая функция. Затем новый поток создается функцией pthread_create() , объявленной в заголовочном файле pthread.h. Далее, вызывающая сторона продолжает выполнять какие-то свои действия параллельно потоковой функции.
При удачном завершении pthread_create() возвращает код 0, ненулевое значение сигнализирует об ошибке.
- Первый параметр вызова pthread_create() является адресом для хранения идентификатора создаваемого потока типа pthread_t .
- Аргумент start является указателем на потоковую void * функцию, принимающей бестиповый указатель в качестве единственной переменной.
- Аргумент arg — это бестиповый указатель, содержащий аргументы потока. Чаще всего arg указывает на глобальную или динамическую переменную, но если вызываемая функция не требует наличия аргументов, то в качестве arg можно указать NULL .
- Аргумент attr также является бестиповым указателем атрибутов потока pthread_attr_t . Если этот аргумент равен NULL , то поток создается с атрибутами по умолчанию.
Рассмотрим теперь пример многопоточной программы.
Чтобы подключить библиотеку Pthread к программе, нужно передать компоновщику опцию -lpthread .
О присоединении потока pthread_join расскажу чуть позже. Строка pthread_t tid задает идентификатор потока. Атрибуты функции задает pthread_attr_init(&attr) . Так как мы не задавали их явно, будут использованы значения по умолчанию.
Завершение потока
Поток завершает выполнение задачи когда:
- потоковая функция выполняет return и возвращает результат произведенных вычислений;
- в результате вызова завершения исполнения потока pthread_exit() ;
- в результате вызова отмены потока pthread_cancel() ;
- одна из нитей совершает вызов exit()
- основная нить в функции main() выполняет return , и в таком случае все нити процесса резко сворачиваются.
Синтаксис проще, чем при создании потока.
Если в последнем варианте старшая нить из функции main() выполнит pthread_exit() вместо просто exit() или return , то тогда остальные нити продолжат исполняться, как ни в чем не бывало.
Ожидание потока
Функция pthread_join() ожидает завершения потока обозначенного THREAD_ID . Если этот поток к тому времени был уже завершен, то функция немедленно возвращает значение. Смысл функции в том, чтобы синхронизировать потоки. Она объявлена в pthread.h следующим образом:
При удачном завершении pthread_join() возвращает код 0, ненулевое значение сигнализирует об ошибке.
Если указатель DATA отличается от NULL , то туда помещаются данные, возвращаемые потоком через функцию pthread_exit() или через инструкцию return потоковой функции. Несколько потоков не могут ждать завершения одного. Если они пытаются выполнить это, один поток завершается успешно, а все остальные — с ошибкой ESRCH. После завершения pthread_join() , пространство стека связанное с потоком, может быть использовано приложением.
В каком-то смысле pthread_joini() похожа на вызов waitpid() , ожидающую завершения исполнения процесса, но с некоторыми отличиями. Во-первых, все потоки одноранговые, среди них отсутствует иерархический порядок, в то время как процессы образуют дерево и подчинены иерархии родитель — потомок. Поэтому возможно ситуация, когда поток А, породил поток Б, тот в свою очередь заделал В, но затем после вызова функции pthread_join() А будет ожидать завершения В или же наоборот. Во-вторых, нельзя дать указание одному ожидай завершение любого потока, как это возможно с вызовом waitpid(-1, &status, options) . Также невозможно осуществить неблокирующий вызов pthread_join() .
Досрочное завершение потока
Точно так же, как при управлении процессами, иногда необходимо досрочно завершить процесс, многопоточной программе может понадобиться досрочно завершить один из потоков. Для досрочного завершения потока можно воспользоваться функцией pthread_cancel .
При удачном завершении pthread_cancel() возвращает код 0, ненулевое значение сигнализирует об ошибке.
Важно понимать, что несмотря на то, что pthread_cancel() возвращается сразу и может завершить поток досрочно, ее нельзя назвать средством принудительного завершения потоков. Дело в том, что поток не только может самостоятельно выбрать момент завершения в ответ на вызов pthread_cancel() , но и вовсе его игнорировать. Вызов функции pthread_cancel() следует рассматривать как запрос на выполнение досрочного завершения потока. Поэтому, если для вас важно, чтобы поток был удален, нужно дождаться его завершения функцией pthread_join() .
Небольшая иллюстрация создания и отмены потока.
Чтобы не создалось впечатление, что тут царит произвол и непредсказуемость результатов данного вызова, рассмотрим таблицу параметров, которые определяют поведение потока после получения вызова на досрочное завершение.
Как мы видим есть вовсе неотменяемые потоки, а поведением по умолчанию является отложенное завершение, которое происходит в момент завершения. А откуда мы узнаем, что этот самый момент наступил? Для этого существует вспомогательная функция pthread_testcancel .
Отсоединение потока
Любому потоку по умолчанию можно присоединиться вызовом pthread_join() и ожидать его завершения. Однако в некоторых случаях статус завершения потока и возврат значения нам не интересны. Все, что нам надо, это завершить поток и автоматически выгрузить ресурсы обратно в распоряжение ОС. В таких случаях мы обозначаем поток отсоединившимся и используем вызов pthread_detach() .
При удачном завершении pthread_detach() возвращает код 0, ненулевое значение сигнализирует об ошибке.
Отсоединенный поток — это приговор. Его уже не перехватить с помощью вызова pthread_join() , чтобы получить статус завершения и прочие плюшки. Также нельзя отменить его отсоединенное состояние. Вопрос на засыпку. Что будет, если завершение потока не перехватить вызовом pthread_join() и чем это отлично от сценария, при котором завершился отсоединенный поток? В первом случае мы получим зомбо-поток, а во втором — все будет норм.
Потоки versus процессы
Напоследок предлагаю рассмотреть несколько соображений на тему, следует ли проектировать приложение многопоточным или запускать его в несколько процессов с одним потоком? Сперва выгоды параллельных множественных потоков.
В начальной части статьи мы уже указывали на эти преимущество, поэтому вкратце их просто перечислим.
- Потоки довольно просто обмениваются данными по сравнению с процессами.
- Создавать потоки для ОС проще и быстрее, чем создавать процессы.
Теперь немного о недостатках.
- При программировании приложения с множественными потоками необходимо обеспечить потоковую безопасность функций — т. н. thread safety. Приложения, выполняющиеся через множество процессов, не имеют таких требований.
- Один бажный поток может повредить остальные, так как потоки делят общее адресное пространство. Процессы более изолированы друг от друга.
- Потоки конкурируют друг с другом в адресном пространстве. Стек и локальное хранилище потока, захватывая часть виртуального адресного пространства процесса, тем самым делает его недоступным для других потоков. Для встроенных устройств такое ограничение может иметь существенное значение.
Тема потоков практически бездонна, даже основы работы с потоками может потянуть на пару лекций, но мы уже знаем достаточно, чтобы изучить структуру многопоточных приложений в Linux.
Источник