• Что такое поток?
  • Достоинства и недостатки потоков
  • Первая программа с применением потоков
  • Одновременное выполнение
  • Синхронизация
  • Синхронизация с помощью семафоров
  • Синхронизация с помощью мьютексов
  • Атрибуты потока
  • Атрибуты планирования потока
  • Отмена потока
  • Потоки в изобилии
  • Резюме
  • Глава 12

    Потоки POSIX

    В главе 11 вы видели, как обрабатываются процессы в ОС Linux (и конечно в UNIX). Эти средства обработки множественных процессов долгое время были характерной чертой UNIX-подобных операционных систем. Порой бывает полезно заставить одну программу делать два дела одновременно или, по крайней мере, создать впечатление такой работы. А может быть, вы хотите, чтобы несколько событий произошло одновременно и все они были тесно связаны, но при этом накладные расходы на создание нового процесса с помощью функции

    fork
    считаете слишком большими. В таких ситуациях можно применить потоки, позволяющие одному процессу стать многозадачным.

    В этой главе мы рассмотрим следующие темы:

    □ создание новых потоков в процессе;

    □ синхронизацию доступа к данным потоков одного процесса;

    □ изменение атрибутов потока;

    □ управление в одном и том же процессе одним потоком из другого.

    Что такое поток?

    Множественные нити исполнения в одной программе называют потоками. Более точно поток — это последовательность или цикл управления в процессе. Все программы, которые вы видели до настоящего момента, выполняли единственный процесс, хотя, как и многие другие операционные системы, ОС Linux вполне способна выполнять множественные процессы одновременно. В действительности у всех процессов есть как минимум один поток исполнения. У всех процессов, с которыми вы пока познакомились в этой книге, был только один поток исполнения.

    Важно понять разницу между системным вызовом fork и созданием новых потоков. Когда процесс выполняет системный вызов fork, создается новая копия процесса с ее собственными переменными и собственным PID. Время выполнения этого нового процесса планируется независимо и выполняется он (в основном) независимо от создавшего его процесса. Когда мы создаем в процессе новый поток, этот поток исполнения в противоположность новому процессу получает собственный стек (и, следовательно, локальные переменные), но использует совместно с создавшим его процессом глобальные переменные, файловые дескрипторы, обработчики сигналов и положение текущего каталога.

    Идея потоков была популярна какое-то время, но пока Комитет IEEE POSIX не опубликовал некоторые стандарты, потоки не были широко распространены в UNIX-подобных операционных системах и существовавшие реализации разных поставщиков сильно отличались друг от друга. С появлением стандарта POSIX 1003.1c все изменилось; потоки теперь не только лучше стандартизованы, но также реализованы в большинстве дистрибутивов Linux. В наше время многоядерные процессоры стали обычными даже в настольных компьютерах, так что у большинства машин есть низкоуровневая аппаратная поддержка, позволяющая им выполнять несколько потоков одновременно. Раньше при наличии одноядерных ЦПУ одновременное исполнение потоков было лишь изобретательной, хотя и очень эффективной иллюзией.

    Впервые ОС Linux обзавелась поддержкой потоков около 1996 г. благодаря появлению библиотеки, которую часто называют "LinuxThreads" (потоки Linux). Она почти соответствует стандарту POSIX (на самом деле в большинстве случаев отличия не заметны) и стала важным шагом на пути первого применения потоков программистами Linux. Но между реализацией потоков в Linux и стандартом POSIX есть слабые расхождения, в основном касающиеся обработки сигналов. Ограничения накладываются не столько реализацией библиотеки, сколько низкоуровневой поддержкой ядра Linux.

    Разные проекты рассматривали возможности улучшения поддержки потоков в Linux, касающиеся не только устранения слабых расхождений со стандартом POSIX, но и повышения производительности и удаления любых ненужных ограничений. Основная работа была направлена на поиск способов отображения потоков пользовательского уровня на потоки уровня ядра системы. Двумя главными проектами были New Generation POSIX Threads (NGPT, потоки POSIX нового поколения) и Native POSIX Thread Library (NPTL, библиотека истинных потоков POSIX). Оба проекта должны были внести изменения в ядро Linux, обеспечивающие поддержку новых библиотек, и оба предлагали существенное повышение производительности по сравнению с прежней реализацией потоков в Linux.

    В 2002 г. команда NGPT объявила, что не хочет разделять сообщество и приостанавливает разработку новых средств для проекта NGPT, но продолжит работу по улучшению поддержки потоков в ОС Linux, присоединив свои усилия к стараниям NPTL. Библиотека NPTL стала новым стандартом для потоков в Linux, выпустив первую основную версию в дистрибутиве Red Hat Linux 9. Вы можете найти интересную основополагающую информацию о NPTL в статье "The Native POSIX Thread Library for Linux" ("Библиотека истинных потоков POSIX для Linux") Ульриха Дреппера (Ulrich Drepper) и Инго Мольнара (Ingo Molnar), которая во время написания книги была доступна в Интернете по адресу http://people.redhat.com/drepper/nptl-design.pdf.

    Большая часть программного кода из этой главы будет работать с любой библиотекой потоков, поскольку основана на стандарте POSIX, общем для всех библиотек потоков. Но вы сможете заметить небольшие отличия, если пользуетесь старой версией дистрибутива Linux, особенно когда примените команду

    ps
    для просмотра примеров во время их выполнения.

    Достоинства и недостатки потоков

    В определенных обстоятельствах создание нового потока обладает явно выраженными преимуществами по сравнению с созданием нового процесса. Накладные расходы при создании нового потока существенно меньше, чем при создании нового процесса (несмотря на то, что создание новых процессов в Linux очень эффективно по сравнению с другими операционными системами).

    Далее перечислены некоторые достоинства потоков.

    □ Иногда очень полезно создать программу, которая выполняет два дела одновременно. Классический пример — подсчет в режиме реального времени слов в документе в ходе редактирования текста. Один поток может управлять пользовательским вводом и выполнять редактирование. Другой, способный видеть то же содержимое документа, может непрерывно обновлять переменную-счетчик количества слов. Первый поток (или даже третий) может использовать эту переменную для информирования пользователя. Другой пример — многопоточный сервер базы данных, в котором единый наблюдаемый процесс обслуживает множество клиентов, улучшая общую пропускную способность за счет обслуживания одних запросов и одновременной блокировки других, ожидающих готовности диска. Серверу базы данных реализовать эту скрытую многозадачность в разных процессах очень трудно, т.к. требования блокировки и непротиворечивости данных приводят к тесной связи двух этих процессов. С помощью множественных потоков воплотить в жизнь этот алгоритм гораздо легче.

    □ Производительность приложения, в котором смешаны ввод, вычисления и вывод, можно повысить, запустив эти операции как три отдельных потока. Пока поток ввода или вывода ждет подсоединения, один из оставшихся потоков может продолжить вычисления. Серверное приложение, обрабатывающее многочисленные сетевые подключения, также может подойти для организации программы с множественными потоками.

    □ Сейчас, когда многоядерные ЦПУ обычны в настольных и портативных компьютерах, применение множественных потоков внутри процесса может при наличии подходящего приложения позволить одному процессу лучше использовать доступные аппаратные ресурсы.

    □ Вообще переключение между потоками требует от операционной системы гораздо меньше усилий, чем переключение между процессами. Таким образом, множественные потоки гораздо менее требовательны к ресурсам, чем множественные процессы, и с ними гораздо практичнее выполнять в однопроцессорных системах программы, логика которых требует применения нескольких потоков исполнения. Считается, что трудности разработки при написании многопоточной программы весьма значительны, и это утверждение нельзя не принимать всерьез.

    У потоков есть и недостатки.

    □ Создание многопоточной программы требует очень тщательной разработки. Вероятность появления незначительных временных сбоев или ошибок, вызванных нечаянным совместным использованием переменных, в такой программе весьма значительна. Алан Кокс (Alan Сох, всеми уважаемый гуру Linux) сказал, что потоки равнозначны умению "выстрелить в обе собственные ноги одновременно".

    □ Отладка многопоточной программы гораздо труднее, чем отладка одного потока исполнения, поскольку взаимосвязи потоков очень трудно контролировать.

    □ Программа, в которой громоздкие вычисления разделены на две части, и эти две части выполняются как отдельные потоки, необязательно будет работать быстрее на машине с одним процессором, если только вычисление не позволяет выполнять обе ее части одновременно и у машины, на которой выполняется программа, нет многоядерного процессора для поддержки истинной многопоточности.

    Первая программа с применением потоков

    Существует целый ряд библиотечных вызовов, связанных с потоками, большинство имен которых начинается с префикса pthread. Для применения этих библиотечных вызовов вы должны определить макрос

    _REENTRANT
    , включить файл pthread.h и скомпоновать программу с библиотекой потоков, используя опцию
    -lpthread
    .

    Когда разрабатывались первые версии библиотечных подпрограмм UNIX и POSIX, предполагалось, что в каждом процессе будет только один поток исполнения. Яркий пример — переменная

    errno
    , применяемая для хранения сведений об ошибке после аварийного завершения вызова. В многопоточной программе по умолчанию будет одна переменная
    errno
    , совместно используемая всеми потоками. Переменная может легко быть изменена вызовом в одном потоке до того, как другой поток успеет извлечь код предыдущей ошибки. Аналогичные проблемы есть и у функций, таких как
    fputs
    , которые, как правило, используют одну глобальную область для буферизации вывода.

    Вам нужны реентерабельные подпрограммы. Реентерабельный программный код может вызываться несколько раз либо разными потоками, либо каким-то образом вложенными вызовами и при этом работать корректно. Следовательно, реентерабельная часть программного кода обычно должна применять локальные переменные таким образом, чтобы любой и каждый вызов кода получал собственную уникальную копию данных.

    В многопоточных программах вы сообщаете компилятору, что вам нужно это средство, определяя в вашей программе макрос

    _REENTRANT
    до любых директив
    #include
    . При этом делаются три вещи и столь искусно, что обычно вам даже не нужно знать, какая работа проделана.

    □ Некоторые функции получают безопасный реентерабельный вариант прототипа или объявления. При этом имя функции остается обычно прежним, но в конце добавляется суффикс

    _r
    , например функция
    gethostbyname
    заменяется функцией
    gethostbyname_r
    .

    □ Некоторые функции из файла stdio.h, которые обычно реализованы как макросы, становятся соответствующими реентерабельными безопасными функциями.

    □ Переменная

    errno
    из файла errno.h заменяется вызовом функции, которая может определить действительное значение
    errno
    безопасным образом с точки зрения многопоточности.

    Включение файла pthread.h предоставляет другие прототипы и определения, которые нужны в вашем программном коде, во многом так же, как делает stdio.h для подпрограмм стандартного ввода и вывода. В заключение следует убедиться в том, что вы включили в программу соответствующий заголовочный файл потоков и скомпоновали программу с подходящей библиотекой потоков, в которой реализованы функции семейства

    pthread
    . Позже в упражнении данного раздела приведены подробности, касающиеся компиляции вашей программы, но сначала рассмотрим новые функции, необходимые для управления потоками. Функция
    pthread_create
    создает новый поток во многом так же, как функция
    fork
    создает новый процесс.

    #include <pthread.h>

    int pthread_create(pthread_t * thread, pthread_attr_t *attr,

     void *(*start_routine)(void *), void *arg);

    Прототип выглядит внушительно, но функцию очень легко применять. Первый аргумент — указатель на переменную типа

    pthread_t
    . Когда поток создан, в область памяти, на которую указывает эта переменная, записывается идентификатор. Этот идентификатор позволяет ссылаться на поток. Следующий аргумент задает атрибуты потока. Обычно нет нужды в особых атрибутах, и вы можете просто передать в этом аргументе
    NULL
    . Позже в этой главе вы увидите, как применять атрибуты потока. В последних двух аргументах потоку передается функция, которую он должен начать выполнять, и аргументы, которые нужно передать этой функции.

    void *(*start_routine)(void *)

    Предыдущая строка просто говорит о том, что вы должны передать адрес функции, принимающей бестиповой указатель

    void
    как параметр, и функция вернет указатель на
    void
    . Следовательно, вы можете передать единственный аргумент любого типа и вернуть указатель на любой тип. Применение функции
    fork
    заставит продолжить выполнение в том же месте, но с другим кодом возврата, в то время как использование нового потока непосредственно предоставит указатель на функцию, которую новый поток должен начать выполнять.

    Возвращаемое значение равно 0 в случае успеха и номеру ошибки, если что-то пошло не так. В интерактивном справочном руководстве есть подробная информация об ошибочных ситуациях для этой и других функций, применяемых в данной главе.

    Примечание

    pthread_create
    как большинство функций семейства
    pthread_
    относится к тем немногим функциям Linux, которые не соблюдают соглашение об использовании значения -1 для обозначения ошибок. Если нет полной уверенности, всегда безопаснее всего дважды проверить справочное руководство перед проверкой кода возврата.

    Когда поток завершается, он вызывает функцию

    pthread_exit
    , во многом так же, как процесс во время завершения вызывает
    exit
    . Функция завершает вызванный поток, возвращая указатель на объект. Никогда не применяйте ее для возврата указателя на локальную переменную, потому что переменная перестает существовать, когда поток завершается, вызывая серьезную ошибку. Функция
    pthread_exit
    объявляется следующим образом:

    #include <рthread.h>

    void pthread_exit(void *retval);

    Функция

    pthread_join
    — эквивалент функции
    wait
    , которую процессы применяют для ожидания дочерних процессов. Она объявляется так:

    #include <рthread.h>

    int pthread_join(pthread_t th, void** thread_return);

    Первый параметр — это поток, который следует ждать, идентификатор, который для вас добывает функция

    pthread_create
    . Второй аргумент — указатель на указатель, который указывает на возвращаемое из потока значение. Как и
    pthread_create
    , эта функция возвращает ноль в случае успешного завершения и код ошибки при сбое.

    Выполните упражнение 12.1.

    Упражнение 12.1. Простая программа с потоками

    Данная программа создает один дополнительный поток, показывает, что он совместно с исходным потоком использует переменные и заставляет новый поток вернуть результат исходному потоку. Далее приведена программа thread1.с.

    #include <stdio.h>

    #include <unistd.h>

    #include <stdlib.h>

    #include <string.h>

    #include <pthread.h>


    void *thread_function(void *arg);

    char message[] = "Hello World";


    int main() {

     int res;

     pthread_t a_thread;

     void *thread_result;

     res = pthread_create(&a_thread, NULL, thread_function, (void *)message);

     if (res ! = 0) {

      perror("Thread creation failed");

      exit(EXIT_FAILURE);

     }

     printf("Waiting for thread to finish...\n");

     res = pthread_join(a_thread, &thread_result);

     if (res != 0) {

      perror("Thread join-failed");

      exit(EXIT_FAILURE);

     }

     printf("Thread-joined, it returned %s\n", (char *)thread_result);

     printf("Message is now %s\n", message);

     exit(EXIT_SUCCESS);

    }


    void *thread_function(void *arg) {

     printf("thread_function is running. Argument was %s\n", (char *)arg);

     sleep(3);

     strcpy(message, "Bye!");

     pthread_exit("Thank you for the CPU time");

    }

    Итак:

    1. Перед компиляцией программы вы должны убедиться в том, что определен макрос

    _REENTRANT
    . В некоторых системах вы также должны определить
    _POSIX_C_SOURCE
    , но обычно в этом нет необходимости.

    2. Далее вы должны убедиться в том, что программа скомпонована с подходящей библиотекой потоков. В случае маловероятной ситуации применения старой версии дистрибутива Linux, в которой NPTL не является библиотекой потоков по умолчанию, возможно, у вас возникнет желание обновить ее, хотя большая часть программного кода, приведенного в этой главе, совместима со старой реализацией потоков в Linux. Легкий способ проверить — заглянуть в файл /usr/include/pthread.h. Если в этом файле приведен в качестве даты авторского права (copyright date) 2003 г. или более поздний, почти наверняка у вас реализация NPTL. Если указана более ранняя дата, может быть, самое время получить современную версию дистрибутива Linux.

    3. Определив и установив нужные файлы, вы можете откомпилировать и скомпоновать вашу программу следующим образом:

    $ cc -D_REENTRANT -I/usr/include/nptl threadl.с -о thread1 -L/usr/lib/nptl -lpthread

    Примечание

    Если в вашей системе по умолчанию установлена NPTL (что очень вероятно), почти наверняка вам не нужны опции

    -I
    и
    -L
    , и можно применить более простой вариант:

    $ cc -D_REENTRANT thread1.с -о thread1 -lpthread

    В данной главе мы будем применять этот более простой вариант строки компиляции.

    4. Когда вы выполните эту программу, то увидите следующие строки:

    $ ./thread1

    Waiting for thread to finish...

    thread_function is running. Argument was Hello World

    Thread joined, it returned Thank you for the CPU time

    Message is now Bye!

    Стоит потратить немного времени на анализ данной программы, поскольку мы будем использовать ее как основу в большинстве примеров этой главы.

    Как это работает

    Вы объявляете прототип функции, которую вызовет поток, когда вы его создадите:

    void *thread_function(void *arg);

    Как требует функция

    pthread_create
    , данная функция принимает в качестве своего единственного параметра указатель на
    void
    и возвращает указатель на
    void
    . (Мы перейдем к реализации
    thread_function
    через минуту.)

    В функции

    main
    объявлено несколько переменных и затем осуществляется вызов функции
    pthread_create
    , чтобы начать выполнение нового потока.

    pthread_t a_thread;

    void *thread_result;

    res = pthread_create(&a_thread, NULL, thread_function, (void *)message);

    Вы передаете адрес объекта типа

    pthread_t
    , который можете применять в дальнейшем для ссылки на поток. Вы не хотите менять атрибуты потока, заданные по умолчанию, поэтому во втором параметре передаете
    NULL
    . Последние два параметра — вызываемая функция и передаваемый ей параметр.

    Если вызов завершился нормально, теперь выполняются два потока. Исходный поток (

    main
    ) продолжается и выполняет код, расположенный следом за функцией
    pthread_create
    , а новый поток начинает выполнение в функции, образно названной
    thread_function
    .

    Исходный поток проверяет, запустился ли новый поток, и затем вызывает функцию

    pthread_join
    :

    res = pthread_join(a_thread, &thread_result);

    Здесь вы передаете идентификатор потока, который ждете, чтобы присоединить, и указатель на результат. Эта функция, прежде чем вернуть управление, будет ждать, пока другой поток не завершится. Затем она выводит возвращаемое из потока значение и содержимое переменной и завершается.

    Новый поток начинает выполнение, запуская функцию

    thread_function
    , которая выводит свои аргументы, засыпает на короткий период, обновляет глобальные переменные и затем завершается, возвращая строку в поток
    main
    . Новый поток пишет в тот же массив
    message
    , к которому у исходного потока есть доступ. Если бы вы вызвали функцию
    fork
    вместо
    pthread_create
    , массив представлял бы собой копию массива
    message
    , а не сам массив.

    Одновременное выполнение

    В упражнении 12.2 показано, как написать программу, которая проверяет одновременное выполнение двух потоков. (Вы, конечно, применяете однопроцессорную систему, ЦП будет искусно переключаться между потоками, а не одновременно выполнять оба потока, используя отдельные ядра процессора аппаратными средствами.) Поскольку вы не встречались еще с какими-либо функциями синхронизации потоков, это будет очень неэффективная программа, делающая нечто, именуемое опросом (polling) двух потоков. И снова вы воспользуетесь тем, что все, за исключением локальных переменных функции, совместно используется двумя потоками в процессе.

    Упражнение 12.2. Одновременное выполнение двух потоков

    Программа thread2.c в этом упражнении создается за счет небольших изменений программы thread1.c. Вы добавите дополнительную глобальную переменную для определения выполняющегося потока.

    Примечание

    Файлы с полными текстами примеров можно загрузить с Web-сайта книги.

    int run_now = 1;

    Задайте

    run_now
    равной 1, когда выполняется функция
    main
    , и 2, когда выполняется новый поток.

    В функцию

    main
    после создания нового потока добавьте следующий код:

    int print_count1 = 0;

    while (print_count1+ < 20) {

     if (run_now == 1) {

      printf("1");

      run_now = 2;

     } else {

      sleep(1);

     }

    }

    Если переменная

    run_now
    равна 1, выведите "1" и присвойте переменной значение 2. В противном случае вы на короткое время засыпаете и снова проверяете значение. Вы ждете, пока значение изменится на 1, проверяя время от времени снова. Этот прием называется циклам активного или деятельного ожидания (busy wait), несмотря, на то, что в данном случае программа засыпает на секунду между очередными проверками. Позже в этой главе вы увидите, как сделать это лучше.

    В функции

    thread_function
    , где выполняется ваш новый поток, вы делаете примерно то же самое, но с противоположными значениями.

    int print_count2 = 0;

    while (print_count2++ < 20) {

     if (run_now == 2) {

      printf("2");

      run_now = 1;

     } else {

      sleep(1);

     }

    }

    Вы удаляете переданные параметр и возвращаемое значение, т.к. они вас больше не интересуют.

    Когда вы выполните программу, то увидите следующий вывод. (Вы можете обнаружить, что для формирования вывода, особенно на машине с одноядерным ЦП, программе потребуется несколько секунд.)

    $ cc -D_REENTRANT thread2.с -о thread2 -lpthread

    $ ./thread2

    12121212121212121212

    Waiting for thread to finish...

    Thread joined

    Как это работает

    Каждый поток заставляет другой поток выполняться, задавая переменную

    run_now
    и затем ожидая, пока другой поток не изменит значение, чтобы можно было продолжить выполнение. Из программы видно, что выполнение переходит от одного потока к другому автоматическими кроме того, она демонстрирует точку, совместно используемую обоими потоками, — переменную
    run_now
    .

    Синхронизация

    В предыдущем разделе вы видели, что два потока выполняются одновременно, но метод переключения между ними топорный и очень неэффективный. К счастью, существует ряд функций, специально разработанных для предоставления лучших способов управления исполнением потоков и доступа к важным фрагментам кода.

    В этом разделе мы рассмотрим два основных метода: семафоры, действующие как сторожа, охраняющие фрагменты кода, и мьютексы или исключающие семафоры, действующие как устройство взаимного исключения (отсюда и имя — исключающий семафор) для защиты фрагментов программного кода. На самом деле эти методы похожи, и один может быть описан в терминах другого. Тем не менее существуют ситуации, в которых семантика проблемы делает один более выразительным, чем другой. Например, управление доступом к некоторой области совместно используемой памяти, к которой может обращаться только один поток в каждый момент времени, более естественным кажется исключающий семафор или мьютекс. Для управления доступом к ряду идентичных объектов в целом, например, предоставление потоку одной телефонной линии из набора, включающего пять доступных линий, больше подходит семафор. Какой метод выберите вы, зависит от личных предпочтений и наиболее подходящего для вашей программы алгоритма.

    Синхронизация с помощью семафоров

    Для семафоров есть два набора интерфейсных функций: один взят из POSIX Realtime Extensions (дополнения POSIX для режима реального времени) и применяется для потоков, а другой, известный как семафоры System V, обычно применяется для синхронизации процессов. (Мы обсудим второй тип в главе 14.) Оба набора не гарантируют взаимозаменяемости и хотя очень похожи, используют вызовы разных функций.

    Дейкстра, голландский ученый, специалист по компьютерным наукам, первым сформулировал идею семафоров. Семафор — это переменная особого типа, которая может изменяться с положительным или отрицательным приращением, но обращение к переменной в ответственный момент всегда атомарно даже в многопоточных программах. Это означает, что если два потока (или несколько) в программе пытаются изменить значение семафора, система гарантирует, что все операции будут на самом деле выполняться одна за другой. В случае обычных переменных результат конфликтных операций разных потоков в одной программе произволен.

    В этом разделе мы рассмотрим простейший тип семафора, двоичный или бинарный семафор, который принимает только значения 0 и 1. Существует и более обобщенный вид семафора, считающий (counting) семафор, принимающий более широкий диапазон значений. Обычно семафоры используются для защиты фрагмента программного кода, так чтобы только один поток исполнения мог изменить его в любой конкретный момент времени. Для этого нужен двоичный семафор. Порой вам необходимо разрешить ограниченному числу потоков выполнять заданный фрагмент кода, для этого вам следует применять считающий семафор. Поскольку считающие семафоры гораздо менее популярны, мы не будем их обсуждать в дальнейшем, отметив лишь, что они представляют собой логическое расширение двоичного семафора и что реальные вызовы функций должны быть идентичны.

    Имена функций семафоров начинаются не с префикса

    pthread_
    , как большинство функций, относящихся к потокам, а с
    sem_
    . Для работы с потоками применяют четыре базовые функций семафоров. Они все очень просты.

    Семафор создается с помощью функции

    sem_init
    , которая объявляется следующим образом.

    #include <semaphore.h>

    int sem_init(sem_t *sem, int pshared, unsigned int value);

    Эта функция инициализирует объект-семафор, на который указывает параметр

    sem
    , задает вариант его совместного использования (который мы обсудим через минуту) и присваивает ему начальное целочисленное значение. Параметр
    pshared
    управляет типом семафора. Если
    pshared
    равен 0, семафор локален по отношению к текущему процессу. В противном случае семафор может быть совместно использован разными процессами. Нас сейчас интересуют семафоры, которые не используются совместно разными процессами. Во время написания книги ОС Linux не поддерживала такое совместное использование и передача ненулевого значения параметру
    pshared
    приводила к аварийному завершению вызова.

    Следующая пара функций управляет значением семафора и объявляется следующим образом.

    #include <semaphore.h>

    int sem_wait(sem_t* sem);

    int sem_post(sem_t* sem);

    Обе они принимают указатель на объект-семафор, инициализированный вызовом

    sem_init
    .

    Функция

    sem_post
    атомарно увеличивает значение семафора на 1. Атомарно в данном случае означает, что если два потока одновременно пытаются увеличить значение единственного семафора на 1, они не мешают друг другу, как в случае двух программ, которые читают, увеличивают и записывают значение в файл в одно и то же время. Если обе программы пытаются увеличить значение на 1, семафор всегда будет корректно увеличивать значение на 2.

    Функция

    sem_wait
    атомарно уменьшает значение семафора на единицу, но всегда ждет до тех пор, пока сначала счетчик семафора не получит ненулевое значение. Таким образом, если вы вызываете
    sem_wait
    для семафора со значением 2, поток продолжит выполнение, а семафор будет уменьшен до 1. Если
    sem_wait
    вызывается для семафора со значением 0, функция будет ждать до тех пор, пока какой-нибудь другой поток не увеличит значение, и оно станет ненулевым. Если оба потока ждут в функции
    sem_wait
    , чтобы один и тот же семафор стал ненулевым, и он увеличивается когда-нибудь третьим потоком, только один из двух ждущих потоков получит возможность уменьшить семафор и продолжиться; другой поток так и останется ждущим. Эта атомарная способность "проверить и установить" в одной функции и делает семафор столь ценным.

    Примечание

    Есть и другая функция семафора

    sem_trywait
    — это неблокирующий партнер
    sem_wait
    . Мы не будем ее обсуждать в книге в дальнейшем, дополнительную информацию см. в интерактивном справочном руководстве.

    Последняя функция семафоров —

    sem_destroy
    . Она очищает семафор, когда вы закончили работу с ним, и объявляется следующим образом:

    #include <semaphore.h>

    int sem_destroy(gem_t* sem);

    И снова эта функция принимает указатель на семафор и очищает любые ресурсы, которые у него могли быть. Если вы попытаетесь уничтожить семафор, которого дожидается какой-либо поток, то получите ошибку.

    Как и большинство других, функций, все перечисленные функции возвращают 0 в случае успешного завершения.

    А теперь выполните упражнение 12.3.

    Упражнение 12.3. Семафор потока

    Текст этой программы thread3.c также основан на тексте программы thread1.c. Поскольку изменения значительны, мы приводим новый вариант полностью.

    #include <stdio.h>

    #include <unistd.h>

    #include <stdlib.h>

    #include <string.h>

    #include <pthread.h>

    #include <semaphore.h>


    void *thread_function(void *arg);

    sem_t bin_sem;


    #define WORK_SIZE 1024

    char work_area[WORK_SIZE];


    int main() {

     int res;

     pthread_t a_thread;

     void *thread result;

     res = sem_init(&bin_sem, 0, 0);

     if (res != 0) {

      perror("Semaphore initialization failed");

      exit(EXIT_FAILURE);

     }

     res = pthread_create(&a_thread, NULL, thread_function, NULL);

     if (res != 0) {

      perror("Thread creation failed");

      exit(EXIT_FAILURE);

     }

     printf("Input some text. Enter 'end' to finish\n");

     while (strncmp("end", work_area, 3) != 0) {

      fgets(work_area, WORK_SIZE, stdin);

      sem_post(&bin_sem);

     }

     printf("\nWaiting for thread to finish...\n");

     res = pthread_join(a_thread, &thread_result);

     if (res != 0) {

      perror("Thread join failed");

      exit(EXIT_FAILURE);

     }

     printf("Thread joined\n");

     sem_destroy(&bin_sem);

     exit(EXIT_SUCCESS);

    }


    void *thread function(void *arg) { sem_wait(&bin_sem);

     while(strncmp("end", work area, 3) != 0) {

      printf("You input %d characters\n", strlen(work_area)-1);

      sem_wait(&bin_sem);

     }

     pthread_exit(NULL);

    }

    Первое важное изменение — включение файла semaphore.h для обеспечения доступа к функциям семафоров. Далее вы объявляете семафор и несколько переменных и инициализируете семафор перед тем, как создать новый поток.

    sem_t bin_sem;

    #define WORK_SIZE 1024

    char work_area[WORK_SIZE];


    int main() {

     int res;

     pthread_t a_thread;

     void *thread_result;

     res = sem_init(&bin_sem, 0, 0);

     if (res != 0) {

      perror("Semaphore initialization failed");

      exit(EXIT_FAILURE);

     }

    Обратите внимание на то, что начальное значение семафора равно 0.

    В функции

    main
    , после того как вы запустили новый поток, вы читаете некоторый текст с клавиатуры, загружаете вашу рабочую область и затем наращиваете счетчик семафора с помощью
    sem_post
    :

     printf("Input some text. Enter 'end' to finish\n");

     while(strncmp("end", work_area, 3) != 0) {

      fgets(work_area, WORK_SIZE, stdin);

      sem_post(&bin_sem);

     }

    В новом потоке вы ждете семафор и затем подсчитываете символы ввода:

     sem_wait(&bin_sem);

     while(strncmp("end", work_area, 3) != 0) {

      printf("You input %d characters\n", strlen(work_area)-1);

      sem_wait(&bin_sem);

     }

    Пока семафор установлен, вы ждете ввода с клавиатуры. Когда вы получите некоторый ввод, то освобождаете семафор, разрешив второму потоку сосчитать символы перед тем, как первый поток начнет снова считывать ввод с клавиатуры.

    И опять потоки совместно используют один и тот же массив

    work_area
    . Для того чтобы программный код был короче и за ним легче было следить, мы опять пропустили некоторые проверки ошибок, например значения, возвращаемые из функции
    sem_wait
    . Но в рабочем программном коде вы всегда должны проверять ошибочные возвращаемые значения, если нет достаточных оснований для отказа от проверки.

    Дайте программе отработать:

    $ cc -D_REENTRANT thread3.с -о threads -lpthread

    $ ./thread3

    Input some text. Enter 'end', to finish

    The Wasp Factory

    You input 16 characters

    Iain Banks

    You input 10 characters

    end


    Waiting for thread to finish...

    Thread joined

    В программах с потоками временные ошибки всегда трудно найти, но программа кажется приспособленной и к быстрому вводу текста, и более неспешным паузам.

    Как это работает

    Когда вы инициализируете семафор, то задаете ему начальное значение, равное 0. Следовательно, когда запускается функция потока, вызов

    sem_wait
    приостанавливает выполнение и ждет, когда семафор станет ненулевым.

    В потоке

    main
    вы ждете до тех пор, пока у вас не будет некоторого текста, и затем увеличиваете счетчик семафора с помощью функции
    sem_post
    , которая немедленно разрешает другому потоку вернуться из своей функции
    sem_wait
    и начать выполнение. После того как он сосчитает символы, поток вновь вызывает
    sem_wait
    и приостанавливает выполнение до тех пор, пока поток main не вызовет снова
    sem_post
    для того, чтобы увеличить семафор.

    Неочевидные недочеты в разработке, которые заканчиваются в результате неявными ошибками, легко пропустить. Давайте слегка изменим программу на thread3a.c, так чтобы вводимый с клавиатуры текст временами заменялся автоматически формируемым текстом. Замените цикл чтения в

    main
    следующим:

    printf("Input some text. Enter 'end' to finish\n");

    while (strncmp("end", work_area, 3) != 0) {

     if (strncmp(work_area, "FAST", 4) == 0) {

      sem_post(&bin_sem);

      strcpy(work_area, "Wheeee...");

     } else {

      fgets(work_area, WORK_SIZE, stdin);

     }

     sem_post(&bin_sem);

    }

    Теперь, если вы введете

    FAST
    , программа вызовет
    sem_post
    , чтобы запустить счетчик символов, но немедленно обновит
    work_area
    чем-то другим.

    $ cc -D_REENTRANT thread3a.с -о thread3a -lpthread

    $ ./thread3a

    Input some text. Enter 'end' to finish

    Excession

    You input 9 characters

    FAST

    You input 7 characters

    You input 7 characters

    You input 7 characters

    end

    Waiting for thread to finish...

    Thread joined

    Проблема этой программы заключается в том, что она рассчитывала на то, что ввод текста из программы продлится так долго, что у другого потока хватит времени для подсчета символов до того, как поток

    main
    подготовится передать ему новую порцию текста для подсчета. Когда вы попытались предложить ему два набора слов для подсчета, быстро следующих друг за другом (
    FAST
    с клавиатуры и затем
    Wheeee...
    , формируемое автоматически), у второго потока не было времени для выполнения. Но семафор наращивался несколько раз, поэтому считающий поток продолжал считать слова и уменьшал значение семафора до тех пор, пока оно снова не стало нулевым.

    Этот пример показывает, как аккуратны вы должны быть с временны́ми условиями в многопоточных программах. Исправить программу можно, применяя дополнительный семафор для того, чтобы заставить поток

    main
    ждать, пока у считающего потока не появится возможность закончить свой подсчет, но гораздо легче применить мьютекс или исключающий семафор, который мы рассмотрим далее.

    Синхронизация с помощью мьютексов

    Другой способ синхронизации доступа в многопоточных программах — применение мьютексов (сокращение от mutual exclusions — взаимные исключения) или исключающих семафоров, которые разрешают программистам "запирать" объект так, что только один поток может обратиться к нему.

    Базовые функции, необходимые для использования мьютексов, очень похожи на функции семафоров. Они объявляются следующим образом:

    #include <рthread.h>

    int pthread_mutex_init(pthread_mutex_t* mutex,

     const pthread_mutexattr_t *mutexattr);

    int pthread_mutex_lock(pthread_mutex_t* mutex);

    int pthread_mutex_unlock(pthread mutex_t* mutex);

    int pthread_mutex_destroy(pthread_mutex_t *mutex);

    Как обычно, в случае успешного завершения возвращается 0 и код ошибки в случае аварийного завершения, но переменная

    errno
    не задается, вам придется использовать код возврата.

    Как и функции семафоров, функции мьютексов принимают указатель на предварительно объявленный объект, в данном случае типа

    pthread_mutex_t
    . Дополнительный параметр атрибутов в функции
    pthread_mutex_init
    позволяет задать атрибуты мьютекса, управляющие его поведением. По умолчанию тип атрибута — "fast". У него есть небольшой недостаток: если ваша программа попытается вызвать функцию
    pthread_mutex_lock
    для мьютекса, который уже заблокирован, программа блокируется. Поскольку поток, удерживающий блокировку, в данный момент заблокирован, мьютекс никогда не будет открыт, и программа попадает в тупиковую ситуацию. Есть возможность изменить атрибуты мьютекса так, чтобы он либо проверял наличие такой ситуации и возвращал ошибку, либо действовал рекурсивно и разрешал множественные блокировки тем же самым потоком, если будет такое же количество разблокировок в дальнейшем.

    Установка атрибутов мьютекса в этой книге не рассматривается, поэтому мы будем передавать

    NULL
    в указателе на атрибуты, и использовать поведение по умолчанию. Дополнительную информацию об изменении атрибутов можно найти в интерактивном справочном руководстве к функции
    pthread_mutex_init
    .

    Выполните упражнение 12.4. 

    Упражнение 12.4. Мьютекс потока

    Далее приводится еще одна модификация исходной программы thread1.с, но значительно измененная. На этот раз вы уделите особое внимание доступу к вашим важным переменным и примените мьютекс для того, чтобы быть уверенными в том, что они доступны в любой момент времени только одному потоку. Для легкости чтения текста примера мы пропустили некоторые проверки ошибок при возвратах из мьютекса, заблокированного и открытого. В рабочем программном коде вы обязательно должны проверять эти возвращаемые значения. Далее приведен текст новой программы thread4.c.

    #include <stdio.h>

    #include <unistd.h>

    #include <stdlib.h>

    #include <string.h>

    #include <pthread.h>

    #include <semaphore.h>


    void *thread_function(void *arg);

    pthread_mutex_t work_mutex; /* защищает work_area и time_to_exit */


    #define WORK_SIZE 1024

    char work_area[WORK_SIZE];

    int time_to_exit = 0;


    int main() {

     int res;

     pthread_t a_thread;

     void *thread_result;

     res = pthread_mutex_init(&work_mutex, NULL);

     if (res != 0) {

      perror("Mutex initialization failed");

      exit(EXIT_FAILURE);

     }

     res pthread_create(&a_thread, NULL, thread_function, NULL);

     if (res != 0) {

      perror("Thread creation failed");

      exit(EXIT_FAILURE);

     }

     pthread_mutex_lock(&work_mutex);

     printf("Input same text. Enter 'end' to finish\n");

     while (!time_to_exit) {

      fgets (work_area, WORK_SIZE, stdin);

      pthread_mutex_unlock(&work_mutex);

      while(1) {

       pthread_mutex_lock(&work_mutex);

       if (work_area[0] != '\0') {

        pthread_mutex_unlock(&work_mutex);

        sleep(1);

       } else {

        break;

       }

      }

     }

     pthread_mutex_unlock(&work_mutex);

     printf("\nWaiting for thread to finish...\n");

     res = pthread_join(a_thread, &thread_result);

     if (res ! = 0) {

      perror("Thread join failed");

      exit(EXIT_FAILURE);

     }

     printf("Thread joined\n");

     pthread_mutex_destroy(&work_mutex);

     exit(EXIT_SUCCESS);

    }


    void *thread_function(void *arg) {

     sleep(1);

     pthread_mutex_lock(&work_mutex);

     while(strncmp("end", work_area, 3) ! = 0) {

      printf("You input %d characters\n", strlen(work_area)-1);

      work_area[0] = '\0';

      pthread_mutex_unlock(&work_mutex);

      sleep(1);

      pthread_mutex_lock(&work_mutex);

      while (work_area[0] == '\0') {

       pthread_mutex_unlock(&work_mutex);

       sleep(1);

       pthread_mutex_lock(&work_mutex);

      }

     }

     time_to_exit = 1;

     work_area[0] = '\0';

     pthread_mutex_unlock(&work_mutex);

     pthread_exit(0);

    }

    После запуска вы получите следующий вывод:

    $ cc -D_REENTRANT thread4.с -о thread4 -lpthread

    $ ./thread4

    Input some text. Enter 'end' to finish

    Whit

    You input 4 characters

    The Crow Road

    You input 13 characters

    end

    Waiting for thread to finish...

    Thread joined

    Как это работает

    Вы начинаете с объявления мьютекса вашей рабочей области и на сей раз дополнительной переменной

    time_to_exit
    :

    pthread_mutex_t work_mutex; /* защищает work_area и time_to_exit */

    #define WORK_SIZE 1024

    char work_area[WORK_SIZE];

    int time_to_exit = 0;

    Далее инициализируется мьютекс:

    res = pthread_mutex_init(&work_mutex, NULL);

    if (res != 0) {

     perror("Mutex initialization failed");

     exit(EXIT_FAILURE);

    }

    Затем запускается новый поток. Далее приведен код, выполняемый в функции потока:

    pthread_mutex_lock(&work_mutex);

    while(strncmp("end", work_area, 3) != 0) {

     printf("You input id characters\n", strlen(work_area)-1);

     work_area[0] = '\0';

     pthread_mutex_unlock(&work_mutex);

     sleep(1);

     pthread_mutex_lock(&work_mutex);

     while (work_area[0] == '\0') {

      pthread_mutex_unlock(&work_mutex);

      sleep(1);

      pthread_mutex_lock(&work_mutex);

     }

    }

    time_to_exit = 1;

    work_area[0] = '\0';

    pthread_mutex_unlock(&work_mutex);

    Сначала новый поток пытается заблокировать мьютекс. Если он уже заблокирован, вызов задерживается до тех пор, пока мьютекс не освободится. После получения доступа вы проверяете, нет ли к вам запроса на завершение выполнения. Если запрашивается завершение, просто задайте переменную

    time_to_exit
    , сотрите первый символ в рабочей области и завершите выполнение. 

    Если вы не хотите завершать выполнение, сосчитайте символы и очистите первый символ, сделав его пустым (null). Пустой первый символ применяется как способ информирования считывающей программы о завершении подсчета символов. Далее вы открываете мьютекс и ждете выполнения потока

    main
    . Периодически вы пытаетесь заблокировать мьютекс и, когда вам это удается, проверяете, подготовил ли поток main новую работу для вас. Если нет, вы открываете мьютекс и ждете какое-то время. Если работа есть, вы считаете символы и выполняете проход цикла снова.

    Далее приведен поток

    main
    .

    pthread_mutex_lock(&work_mutex)

    printf("Input some text. Enter 'end' to finish\n");

    while (!time_to_exit) {

     fgets(work_area, WORK_SIZE, stdin);

     pthread_mutex_unlock(&work_mutex);

     while(1) {

      pthread_mutex_lock(&work_mutex);

      if (work_area[0] != '\0') {

       pthread_mutex_unlock(&work_mutex);

       sleep(1);

      } else {

       break;

      }

     }

    }

    pthread_mutex_unlock(&work_mutex);

    Он аналогичен второму потоку. Вы блокируете рабочую область и можете читать в нее текст, а затем вы снимаете блокировку, чтобы открыть доступ другому потоку для подсчета слов. Периодически вы блокируете мьютекс, проверяете, сосчитаны ли слова (элемент

    work_area[0]
    равен пустому символу), и освобождаете мьютекс, если нужно продолжить ожидание. Как уже отмечалось ранее, этот вид опроса и получения ответа в основном не слишком удачный прием и в реальной жизни вам, возможно, придется применить семафор для его замены. Тем не менее, программный код справляется с задачей демонстрации примера применения мьютекса.

    Атрибуты потока

    Когда мы начали рассматривать потоки, то не обсуждали более сложную тему — атрибуты потока. Теперь, рассказав о синхронизации потоков — ключевой теме главы, мы можем вернуться назад и остановиться на этих характеристиках потока. Существует лишь несколько атрибутов потока, которыми вы можете управлять; здесь мы собираемся обсудить только те, которые вам понадобятся, скорее всего. Подробную информацию о других атрибутах вы можете найти в интерактивном справочном руководстве.

    Во всех предыдущих примерах вы должны были повторно синхронизовать потоки с помощью функции

    pthread_join
    , прежде чем разрешить программе завершить выполнение. Это необходимо сделать, если вы хотите, чтобы один поток вернул данные другому потоку, создавшему данный. Иногда вам не нужно ни возвращать информацию из второго потока в поток
    main
    , ни заставлять поток
    main
    ждать этого.

    Предположим, что вы создаете второй поток для записи в буфер резервной копии файла данных, который редактируется, пока поток

    main
    продолжает обслуживать пользователя. Когда создание копии закончено, второй поток может тут же завершиться. Ему не нужно присоединяться к потоку
    main
    .

    Вы можете создать потоки, ведущие себя подобным образом. Они называются отсоединенными или обособленными потоками, и вы создаете их, изменяя атрибуты потока или вызывая функцию

    pthread_detach
    . Поскольку мы хотим продемонстрировать атрибуты, то применим здесь первый метод.

    Самая важная функция, которая вам понадобится, —

    pthread_attr_init
    , инициализирующая объект атрибутов потока:

    #include <pthread.h>

    int pthread_attr_init(pthread_attr_t *attr);

    И снова 0 возвращается в случае успешного завершения и код ошибки в случае аварийного.

    Есть и функция для уничтожения:

    pthread_attr_destroy
    . Ее задача — обеспечить чистое уничтожение объекта атрибутов. После того как объект уничтожен, он не может быть использован снова до тех пор, пока не будет инициализирован повторно.

    Когда вы инициализировали объект атрибутов потока, можно использовать множество дополнительных функций, с помощью которых задается поведение разных атрибутов. Далее перечислены основные из них (полный список вы можете найти в интерактивном справочном руководстве, в разделе, посвященном pthread.h), но мы рассмотрим подробно только два:

    detechedstate
    и
    schedpolicy
    .

    #include <рthread.h>

    int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

    int pthread_attr_getdetachstate(const pthread_attr_t *attr,

     int *detachstate);

    int pthread_attr_setschedpolicy(pthread_attr_t* attr, int policy);

    int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int* policy);

    int pthread_attr_setschedparam(pthread_attr_t *attr,

     const struct sched_param *param);

    int pthread_attr_getschedparam(const pthread_attr_t *attr,

     struct sched_param *param);

    int pthread_attr_setinheritsched(pthread_attr_t *attr, int inherit);

    int pthread_attr_getinheritsched(const pthread_attr_t *attr,

     int *inherit);

    int pthread_attr_setscope(pthread_attr_t *attr, int scope);

    int pthread_attr_getscope(const pthread_attr_t *attr, int *scope);

    int pthread_attr_setstacksize(pthread_attr_t *attr, int scope);

    int pthread_attr_getstacksize(const pthread_attr_t *attr, int* scope);

    Как видите, существует лишь несколько атрибутов, которые вы можете применять, но к счастью у вас, как правило, не возникнет необходимости в использовании большинства из них.

    □ 

    detachedstate
    — этот атрибут позволяет избежать необходимости присоединения потоков (rejoin). Как и большинство этих функций с префиксом
    _set
    , эта функция принимает указатель на атрибут и флаг для определения требуемого состояния. Два возможных значения флага для функции
    attr_setdetachstate
    PTHREAD_CREATE_JOINABLE
    и
    PTHREAD_CREATE_DETACHED
    . По умолчанию у атрибута будет значение
    PTHREAD_CREATE_JOINABLE
    , поэтому вы сможете разрешить двум потокам объединяться (один ждет завершения другого). Если задать состояние
    PTHREAD_CREATE_DETACHED
    , вы не сможете вызвать функцию
    pthread_join
    , чтобы выяснить код завершения другого потока.

    □ 

    schedpolicy
    — этот атрибут управляет планированием потоков. Возможные значения —
    SCHED_OTHER
    ,
    SCHED_RR
    и
    SCHED_FIFO
    . По умолчанию атрибут равен
    SCHED_OTHER
    . Два других типа планирования доступны только для процессов, выполняющихся с правами суперпользователя, поскольку они оба задают планирование в режиме реального времени, но с немного разным поведением.
    SCHED_RR
    использует круговую или циклическую схему планирования, a
    SCHED_FIFO
    — алгоритм "первым прибыл, первым обслужен". Оба эти алгоритма не обсуждаются в этой книге.

    □ 

    schedparam
    — это напарник атрибута
    schedpolicy
    и позволяет управлять планированием потоков, выполняющихся с типом планирования
    SCHED_OTHER
    . Мы рассмотрим пример его применения чуть позже в этой главе.

    □ 

    inheritsched
    — этот атрибут принимает одно из двух значений:
    PTHREAD_EXPLICIT_SCHED
    и
    PTHREAD_INHERIT_SCHED
    . По умолчанию значение атрибута
    PTHREAD_EXPLICIT_SCHED
    , что означает планирование, явно заданное атрибутами. Если задать
    PTHREAD_INHERIT_SCHED
    , новый поток будет вместо этого применять параметры, используемые потоком, создавшим его.

    □ 

    scope
    — этот атрибут управляет способом вычисления параметров планирования потока. Поскольку ОС Linux в настоящее время поддерживает единственное значение
    PTHREAD_SCOPE_SYSTEM
    , мы не будем рассматривать его в дальнейшем.

    □ 

    stacksize
    — этот атрибут управляет размером стека при создании потока, задается в байтах. Это часть необязательного раздела стандарта и поддерживается только в тех реализациях, у которых определено значение
    _PTHREAD_THREAD_ATTR_STACKSIZE
    . Linux по умолчанию реализует потоки со стеком большого размера, поэтому этот атрибут в ОС Linux избыточен.

    Выполните упражнение 12.5.

    Упражнение 12.5. Установка атрибута отсоединенного состояния

    В примере с отсоединенным или обособленным потоком thread5.c вы создаете атрибут потока, задаете состояние потока как отсоединенное и затем создаете с помощью этого атрибута поток. Теперь, когда закончится дочерний поток, он вызовет обычным образом

    pthread_exit
    . В это время исходный поток больше не ждет созданный им поток для присоединения. В данном примере используется простой флаг
    thread_finished
    , чтобы позволить потоку
    main
    определить, закончился ли дочерний поток, и показать, что потоки все еще совместно используют переменные.

    #include <stdio.h>

    #include <unistd.h>

    #include <stdlib.h>

    #include <pthread.h>


    void *thread_function(void *arg);

    char message[] = "Hello World";

    int thread_finished = 0;


    int main() {

     int res;

     pthread_t a_thread;

     pthread_attr_t thread_attr;

     res = pthread_attr_init(&thread_attr);

     if (res != 0) {

      perror("Attribute creation failed");

      exit(EXIT_FAILURE);

     }

     res = pthread_attr_setdetachstate(&thread_attr,

      PTHREAD_CREATE_DETACHED);

     if (res != 0) {

      perror("Setting detached attribute failed");

      exit(EXIT_FAILURE);

     }

     res = pthread_create(&a_thread, &thread_attr,

      thread_function, (void *)message);

     if (res != 0) {

      perror("Thread creation failed");

      exit(EXIT_FAILURE);

     }

     (void)pthread_attr_destroy(&thread_attr);

     while (!thread_finished) {

      printf("Waiting for thread to say it's finished...\n");

      sleep(1);

     }

     printf("Other thread finished, bye!\n");

     exit(EXIT_SUCCESS);

    }


    void *thread_function(void *arg) {

     printf("thread_function is running. Argument was %s\n", (char *)arg);

     sleep(4);

     printf("Second thread setting finished flag, and exiting now\n");

     thread_finished = 1;

     pthread_exit(NULL);

    }

    Вывод не принесет сюрпризов:

    $ ./threads

    Waiting for thread to say it's finished...

    thread_function is running. Argument was Hello World

    Waiting for thread to say it's finished...

    Waiting for thread to say it's finished...

    Waiting for thread to say it's finished...

    Second thread setting finished flag, and exiting now

    Other thread finished, bye!

    Как видите, установка отсоединенного состояния позволяет второму потоку завершиться независимо, без необходимости исходному потоку ждать этого события.

    Как это работает

    В исходном тексте программы два важных фрагмента:

    pthread_attr_t thread_attr;

    res = pthread_attr_init(&thread_attr);

    if (res != 0) {

     perror("Attribute creation failed");

     exit(EXIT_FAILURE);

    }

    который объявляет атрибут потока и инициализирует его, и

    res = pthread_attr_setdetachstatе(&thread_attr, PTHREAD_CREATE_DETACHED);

    if (res != 0) {

     perror("Setting detached attribute failed");

     exit(EXIT_FAILURE);

    }

    который устанавливает значения атрибутов для задания отсоединенного состояния потока.

    К другим незначительным отличиям относится создание потока с передачей адреса атрибутов:

    res = pthread_create(&a_thread, &thread_attr, thread_function, (void*)message);

    и для завершенности уничтожение атрибутов после их использования:

    pthread_attr_destroy(&thread_attr);

    Атрибуты планирования потока

    Давайте рассмотрим второй атрибут потока, который вам, возможно, захочется изменить, — атрибут планирования. Изменение этого атрибута очень похоже на установку отсоединенного состояния потока, но есть дополнительные функции, которые можно применять для подбора допустимых уровней приоритета,

    sched_get_priority_max
    и
    sched_get_priority_min
    .

    Выполните упражнение 12.6.

    Упражнение 12.6. Планирование

    Поскольку данная программа thread6.c очень похожа на программу предыдущего упражнения, мы рассмотрим только отличия.

    1. Прежде всего, вам понадобится несколько дополнительных переменных:

    int max_priority;

    int min_priority;

    struct sched_param scheduling_value;

    2. После того как установлен атрибут отсоединения, вы задаете политику планирования:

    res = pthread_attr_setschedpolicy(&thread_attr, SCHED_OTHER);

    if (res != 0) {

     perror("Setting scheduling policy failed");

     exit(EXIT_FAILURE);

    }

    3. Далее находите диапазон допустимых приоритетов

    max_priority = sched_get_priority_max(SCHED_OTHER);

    min_priority = sched_get_priority_min(SCHED_OTHER);

    и задаете один из них:

    scheduling_value.sched_priority = min_priority;

    res = pthread_attr_setschedparam(&thread_attr, &scheduling_value);

    if (res != 0) {

     perror("Setting scheduling priority failed");

     exit(EXIT_FAILURE);

    }

    Когда вы запустите программу, то получите следующий вывод:

    $ ./thread6

    Waiting for thread to say it's finished...

    thread_function is running. Argument was Hello World

    Waiting for thread to say it's finished...

    Waiting for thread to say it's finished...

    Waiting for thread to say it's finished...

    Second thread setting finished flag, and exiting now

    Other thread finished, bye!

    Как это работает

    Этот пример очень похож на установку атрибута отсоединенного состояния за исключением того, что вы задаете вместо него способ планирования.

    Отмена потока

    Иногда требуется, чтобы один поток попросил другой завершиться досрочно способом, очень похожим на отправку ему сигнала. Сделать это можно с помощью потоков и параллельно с помощью обработки сигнала; у потоков появляется возможность изменить свое поведение, когда их просят завершиться.

    Давайте сначала рассмотрим функцию для создания запроса на завершение потока.

    #include <pthread.h>

    int pthread_cancel(pthread_t thread);

    Она достаточно проста: имея идентификатор потока, вы можете запросить его аннулирование. На приемном конце запроса на отмену все немного сложнее, но не слишком. Поток может установить состояние отмены с помощью функции

    pthread_setcancelstate
    .

    #include <pthread.h>

    int pthread_setcancelstate(int state, int *oldstate);

    Первый параметр равен либо значению

    PHTREAD_CANCEL_ENABLE
    , позволяющему получать запросы на отмену, либо
    PTHREAD_CANCEL_DISABLE
    , заставляющему игнорировать подобные запросы. Указатель
    oldstate
    дает возможность получить предыдущее состояние. Если оно вас не интересует, можно просто передать в этом параметре
    NULL
    . Если запросы на отмену принимаются, есть второй уровень управления, принимаемый потоком, — тип отмены, который задается функцией
    pthread_setcanceltype
    .

    #include <pthread.h>

    int pthread_setcanceltype(int type, int *oldtype);

    Тип отмены может принимать одно из следующих значений:

    PTHREAD_CANCEL_ASYNCHRONOUS
    , заставляющее обрабатывать запросы на отмену немедленно, и
    PTHREAD_CANCEL_DEFERRED
    , заставляющее запросы на отмену ждать, пока поток не выполнит одну из следующих функций:
    pthread_join
    ,
    pthread_cond_wait
    ,
    pthread_cond_timedwait
    ,
    pthread_testcancel
    ,
    sem_wait
    или
    sigwait
    .

    Мы не описываем все эти функции в данной главе, поскольку, как правило, не все они нужны. Когда они понадобятся, вы сможете найти дополнительную информацию на страницах интерактивного справочного руководства.

    Примечание

    В соответствии со стандартом POSIX системные вызовы, способные задерживать выполнение, такие как

    read
    ,
    wait
    и т.д., должны также быть точками отмены потока. Во время написания книги поддержка этого стандарта в ОС Linux представлялась незавершенной. Но кое-какая работа была проделана, скажем, некоторые задерживающие вызовы, такие как
    sleep
    , на самом деле допускают отмену. Для того чтобы обезопасить себя, добавляйте вызовы
    pthread_testcancel
    в программный код, который по вашим расчетам может быть отменен.

    Параметр

    oldtype
    позволяет получить предыдущее состояние, если оно вас не интересует, можно передать
    NULL
    . По умолчанию потоки запускаются с состоянием отмены, равным
    PTHREAD_CANCEL_ENABLE
    , и типом отмены —
    PTHREAD_CANCEL_DEFERRED
    .

    Выполните упражнение 12.7.

    Упражнение 12.7. Отмена потока

    Программа thread7.c — ещё один потомок программы thread1.с. На этот раз основной поток отправляет запрос на отмену потока, который он создал.

    #include <stdio.h>

    #include <unistd.h>

    #include <stdlib.h>

    #include <pthread.h>


    void *thread_function(void *arg);


    int main() {

     int res;

     pthread_t a_thread;

     void *thread_result;

     res = pthread_create(&a_thread, NULL, thread_function, NULL);

     if (res != 0) {

      perror("Thread creation failed");

      exit(EXIT_FAILURE);

     }

     sleep(3);

     printf("Canceling thread...\n");

     res = pthread_cancel(a_thread);

     if (res != 0) {

      perror("Thread cancelation failed");

      exit(EXIT_FAILURE);

     }

     printf("Waiting for thread to finish...\n");

     res = pthread_join(a_thread, &thread_result);

     if (res != 0) {

      perror("Thread join failed");

      exit(EXIT_FAILURE);

     }

     exit(EXIT_SUCCESS);

    }


    void *thread_function(void *arg) {

     int i, res;

     res = pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);

     if (res != 0) {

      perror("Thread pthread_setcancelstate failed");

      exit(EXIT_FAILURE);

     }

     res = pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);

     if (res != 0) {

      perror{"Thread pthread_setcanceltype failed");

      exit(EXIT_FAILURE);

     }

     printf("thread_function is running\n");

     for(i = 0; i < 10; i++) {

      printf("Thread is still running (%d)...\n", i);

      sleep(1);

     }

     pthread_exit(0);

    }

    Когда вы выполните эту программу, то увидите следующий вывод, демонстрирующий отмену потока:

    $ ./thread7

    thread_function is running

    Thread is still running (0)...

    Thread is still running (1)...

    Thread is still running (2)...

    Canceling thread...

    Waiting for thread to finish...

    $

    Как это работает

    После того как новый поток был создан обычным способом, основной поток засыпает (чтобы дать новому потоку время для запуска) и затем отправляет запрос на отмену потока.

    sleep(3);

    printf("Cancelling thread...\n");

    res = pthread_cancel(a_thread);

    if (res != 0) {

     perror("Thread cancelation failed");

     exit(EXIT_FAILURE);

    }

    В созданном потоке вы сначала задаете состояние отмены, чтобы разрешить отмену потока:

    res = pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);

    if (res != 0) {

     perror("Thread pthread_setcancelstate failed");

     exit(EXIT_FAILURE);

    }

    Далее вы задаете тип отмены

    PTHREAD_CANCEL_DEFERRED
    :

    res = pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);

    if (res != 0) {

     perror("Thread pthread_setcanceltype failed");

     exit(EXIT_FAILURE);

    }

    И в конце поток ждет отмену:

    for (i = 0; i < 10; i++) {

     printf("Thread is still running (%d)...\n", i);

     sleep(1);

    }

    Потоки в изобилии

    До настоящего момента у нас всегда был обычный поток исполнения программы, создающий еще только один поток. Тем не менее мы не хотим, чтобы вы думали, что можно создать только один дополнительный поток (упражнение 12.8).

    Упражнение 12.8. Много потоков

    В заключительном примере этой главы thread8.c мы покажем, как создать несколько потоков в одной и той же программе и затем собрать их снова в последовательности, отличающейся от порядка их создания.

    #include <stdio.h>

    #include <unistd.h>

    #include <stdlib.h>

    #include <pthread.h>


    #define NUM_THREADS 6

    void *thread_function(void *arg);


    int main() {

     int res;

     pthread_t a_thread[NUM_THREADS];

     void *thread_result;

     int lots_of_threads;

     for (lots_of_threads = 0; lots_of_threads < NUM_THREADS; lots_of_threads++) {

      res = pthread_create(&(a_thread[lots_of_threads]), NULL, thread_function, (void*)&lots_of_threads);

      if (res != 0) {

       perror("Thread creation failed");

       exit(EXIT_FAILURE);

      }

      sleep(1);

     }

     printf("Waiting for threads' to finish...\n");

     for(lots of_threads = NUM_THREADS - 1; lots_of_threads >= 0; lots_of_threads--) {

      res = pthread_join(a_thread[lots_of_threads], &thread_result);

      if (res == 0) {

       printf("Picked up a thread\n");

      } else {

       perror("pthread_join failed");

      }

     }

     printf("All done\n");

     exit(EXIT_SUCCESS);

    }


    void *thread_function(void *arg) {

     int my_number = *(int*)arg;

     int rand_num;

     printf("thread_function is running. Argument was %d\n", my_number);

     rand_num = 1 + (int)(9.0*rand() / (RAND_MAX+1.0));

     sleep(rand_num);

     printf("Bye from %d\n", my_number);

     pthread_exit(NULL);

    }

    Выполнив эту программу, вы получите следующий вывод:

    $ ./thread8

    thread_function is running. Argument was 0

    thread_function is running. Argument was 1

    thread_function is running. Argument was 2

    thread_function is running. Argument was 3

    thread_function is running. Argument was 4

    Bye from 1

    thread_function is running. Argument was 5

    Waiting for threads to finish...

    Bye from 5

    Picked up a thread

    Bye from 0

    Bye from 2

    Bye from 3

    Bye from 4

    Picked up a thread

    Picked up a thread

    Picked up a thread

    Picked up a thread

    Picked up a thread

    All done

    Как видите, вы создали много потоков и разрешили им завершаться в произвольной последовательности. В этой программе есть маленькая ошибка, которая проявит себя, если вы удалите вызов sleep из цикла, запускающего потоки. Мы включили ее, чтобы показать, как вы должны быть внимательны при написании программ, применяющих потоки. Вы нашли ее? В следующем разд. "Как это работает" будет дано объяснение.

    Как это работает

    На сей раз вы создаете массив идентификаторов потоков:

    pthread_t a_thread[NUM_THREADS];

    и заключаете в цикл создание нескольких потоков:

    for (lots_of_threads = 0; lots_of_threads < NUM_THREADS; lots_of_threads++) {

     res = pthread_create(&(a_thread[lots_of_threads]), NULL,

      thread_function, (void *)&lots_of_threads);

     if (res != 0) {

      perror("Thread creation failed");

      exit(EXIT_FAILURE);

     }

     sleep(1);

    }

    Затем потоки сами по себе ждут в течение случайного промежутка времени, прежде чем начать выполнение:

    void *thread_function(void *arg) {

     int my_number = *(int *)arg;

     int rand_num;

     printf("thread_function is running. Argument was %d\n", my_number);

     rand_num = 1+(int)(9.0* rand()/(RAND_MAX+1.0));

     sleep(randnum);

     printf("Bye from %d\n", my_number);

     pthread_exit(NULL);

    }

    В это время в основном (исходном) потоке вы ждете, чтобы собрать потоки, но не в том порядке, в каком вы их создали:

    for (lots_of_threads = NUM_THREADS - 1; lots_of_threads >= 0; lots_of_threads--) {

     res = pthread_join(a_thread[lots_of__threads], &thread_result);

     if (res == 0) {

      printf("Picked up a thread\n");

     } else {

      perror("pthread_join failed");

     }

    }

    Если вы попробуете выполнить программу без вызова

    sleep
    , то увидите странный эффект: некоторые потоки запускаются с одним и тем же номером, например, вы можете получить вывод, похожий на следующий:

    thread_function is running. Argument was 0

    thread_function is running. Argument was 2

    thread_function is running. Argument was 2

    thread_function is running. Argument was 4

    thread_function is running. Argument was 4

    thread_function is running. Argument was 5

    Waiting for threads to finish...

    Bye from 5

    Picked up a thread

    Bye from 2

    Bye from 0

    Bye from 2

    Bye from 4

    Bye from 4

    Picked up a thread

    Picked up a thread

    Picked up a thread

    Picked up a thread

    Picked up a thread

    All done

    Вы догадались, что произошло? Потоки запускаются, используя локальную переменную как аргумент функции потока. Эта переменная обновляется в цикле. Далее приведены ошибочные строки:

    for (lots_of_threads = 0; lots_of_threads < NUM_THREADS; lots_of_threads++) {

     res = pthread_create(&(a_thread[lots_of_threads]), NULL,

      thread_function, (void *)&lots_of_threads);

    Если поток

    main
    выполняется достаточно быстро, он может искажать аргумент (
    lots_of_threads
    ) для некоторых потоков. Поведение, подобное этому, наблюдается, когда недостаточно внимания уделяется совместно используемым переменным и множественным путям исполнения (multiple execution paths). Мы предупреждали вас о том, что программирование потоков требует повышенного внимания при разработке! Для исправления ошибки вам следует передавать непосредственно значение следующим образом:

    res = pthread_create(&(a_thread[lots_of_threads]), NULL,

     thread_function, (void *)lots_of_threads);

    и конечно изменить

    thread_function
    :

    void *thread_function(void *arg) {

     int my_number = (int)arg;

    Все исправления, выделенные цветом, показаны в программе thread8a.c.

    #include <stdio.h>

    #include <unistd.h>

    #include <stdlib.h>

    #include <string.h>

    #include <pthread.h>

    #define NUM_THREADS 6


    void *thread_function(void *arg);


    int main() {

     int res;

     pthread_t a_thread[NUM_THREADS];

     void *thread_result;

     int lots_of_threads;

     for (lots_of_threads = 0; lots_of_threads < NUM_THREADS; lots_of_threads++) {

      res = pthread_create(&(a_thread[lots_of_thread]), NULL,

       thread_function, (void*)lots_оf_threads);

      if (res != 0) {

       perror("Thread creation failed");

       exit(EXIT_FAILURE);

      }

     }

     printf("Waiting for threads to finish...\n");

     for (lots_of_threads = NUM_THREADS - 1; lots_of_threads >= 0;

      lots of threads--) {

      res = pthread_join(a_thread[lots_of_threads], &thread_result);

      if (res == 0) {

       printf("Picked up a thread\n");

      } else {

       perror("pthread_join failed");

      }

     }

     printf("All done\n");

     exit(EXIT_SUCCESS);

    }


    void* thread_function(void* arg) {

     int my_number = (int)arg;

     int rand_num;

     printf("thread_function is running. Argument was %d\n", my_number);

     rand_num = 1+(int)(9.0*rand()/(RAND_MAX+1.0));

     sleep(rand_num);

     printf("Bye from %d\n", my_number);

     pthread_exit(NULL);

    }

    Резюме

    В этой главе вы узнали, как создать несколько потоков исполнения внутри процесса, которые совместно используют глобальные переменные. Вы рассмотрели два способа управления — семафоры и мьютексы, применяемые потоками для доступа к важным фрагментам кода и данным. Далее вы увидели, как управлять атрибутами потоков и, в особенности, как можно отсоединить потоки от основного, не заставляя его ждать завершения созданных им потоков. После краткого обзора способов формирования в одном потоке запросов на отмену других потоков и вариантов управления такими запросами в потоке, получившем их, мы представили программу с множественными одновременно выполняющимися потоками.

    Объем книги не позволяет обсудить все до единой функции и тонкости, связанные с потоками, но теперь у вас достаточно знаний для того, чтобы начать писать собственные программы, применяющие потоки, и изучать глубоко скрытые свойства потоков, читая страницы интерактивного справочного руководства.








    Главная | В избранное | Наш E-MAIL | Прислать материал | Нашёл ошибку | Наверх