Threads posix win32 что это
Перейти к содержимому

Threads posix win32 что это

  • автор:

POSIX Threads

POSIX Threads — стандарт POSIX реализации потоков (нитей) выполнения, определяющий API для создания и управления ими.

Библиотеки, реализующие этот стандарт (и функции этого стандарта), обычно называются Pthreads (функции имеют приставку «pthread_»). Хотя наиболее известны варианты для Unix-подобных операционных систем, таких как Linux или Solaris, но существует и реализация для Microsoft Windows (Pthreads-w32)

Основные функции стандарта

Pthreads определяет набор типов и функций на языке программирования Си. Заголовочный файл — pthread.h.

  • Типы данных:
    • pthread_t: дескриптор потока
    • pthread_attr_t: перечень атрибутов потока
    • pthread_create(): создание потока
    • pthread_exit(): завершение потока (должна вызываться функцией потока при завершении)
    • pthread_cancel(): отмена потока
    • pthread_join(): подключиться к другому потоку и ожидать его завершения; поток, к которому необходимо подключиться, должен быть создан с возможностью подключения (PTHREAD_CREATE_JOINABLE)
    • pthread_detach(): отключиться от потока, сделав его при этом отдельным (PTHREAD_CREATE_DETACHED)
    • pthread_attr_init(): инициализировать структуру атрибутов потока
    • pthread_attr_setdetachstate(): указывает параметр «отделимости» потока (detach state), который говорит о возможности подключения к нему (при помощи pthread_join) других потоков (значение PTHREAD_CREATE_JOINABLE) для ожидания окончания или о запрете подключения (значение PTHREAD_CREATE_DETACHED); ресурсы отдельного потока (PTHREAD_CREATE_DETACHED) при завершении автоматически освобождаются и возвращаются системе
    • pthread_attr_destroy(): освободить память от структуры атрибутов потока (уничтожить дескриптор)
    • pthread_mutex_init(), pthread_mutex_destroy(), pthread_mutex_lock(), pthread_mutex_trylock(), pthread_mutex_unlock(): с помощью мьютексов
    • pthread_cond_init(), pthread_cond_signal(), pthread_cond_wait(): с помощью условных переменных

    Пример

    Пример использования потоков на языке C:

    #include #include #include #include static void wait_thread(void)  time_t start_time = time(NULL); while (time(NULL) == start_time)  /* do nothing except chew CPU slices for up to one second. */ > > static void *thread_func(void *vptr_args)  int i; for (i = 0; i  20; i++)  fputs(" b\n", stderr); wait_thread(); > return NULL; > int main(void)  int i; pthread_t thread; if (pthread_create(&thread, NULL, thread_func, NULL) != 0)  return EXIT_FAILURE; > for (i = 0; i  20; i++)  puts("a"); wait_thread(); > if (pthread_join(thread, NULL) != 0)  return EXIT_FAILURE; > return EXIT_SUCCESS; > 

    Пример использования потоков на языке C++:

    #include #include #include #include class Thread  private: pthread_t thread; Thread(const Thread& copy); // copy constructor denied static void *thread_func(void *d)  ((Thread *)d)->run(); > public: Thread() > virtual ~Thread() > virtual void run() = 0; int start()  return pthread_create(&thread, NULL, Thread::thread_func, (void*)this); > int wait ()  return pthread_join (thread, NULL); > >; typedef std::auto_ptrThread> ThreadPtr; int main(void)  class Thread_a:public Thread  public: void run()  for (int i=0; i20; i++, sleep(1)) std::cout  <"a "  ::endl; > >; class Thread_b:public Thread  public: void run()  for(int i=0; i20; i++, sleep(1)) std::cout  <" b"  ::endl; > >; ThreadPtr a( new Thread_a() ); ThreadPtr b( new Thread_b() ); if (a->start() != 0 || b->start() != 0) return EXIT_FAILURE; if (a->wait() != 0 || b->wait() != 0) return EXIT_FAILURE; return EXIT_SUCCESS; > 

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

    Отличие состоит в том, что программа на C создает один новый поток для печати ‘b’, а основной поток печатает ‘a’. Основной поток (после печати ‘aaaaa….’) ждёт завершения дочернего потока.

    Программа на C++ создает два новых потока, один печатает ‘a’, второй, соответственно, — ‘b’. Основной поток ждёт завершения обоих дочерних потоков.

    См. также

    • Native POSIX Thread Library (NPTL)
    • GNU Portable Threads
    • Список многопоточных библиотек C++

    Ссылки

    • Спецкурс «Многонитевое программирование» ВМиК МГУ (рус.)
    • Многопоточное программирование (Учебник Pthreads) (англ.)
    • Примеры использования Pthreads (англ.)
    • Примеры использования Pthreads в C/C++ (англ.)
    • Статья «Объясняя потоки POSIX», Даниэля Роббинса (основателя проекта Gentoo) (англ.)
    • Интервью «10 вопросов Девиду Бутенхофу о параллельном программировании и потоках POSIX» с Майклом Суиссом (англ.)
    • The Open Group Base Specifications Issue 6, IEEE Std 1003.1 (англ.)
    • Pthread Win-32, Basic Programming (англ.)
    • Pthreads Tutorial (англ.)
    • C/C++ Tutorial: using Pthreads (англ.)
    • Article «POSIX threads explained» by Daniel Robbins (Gentoo Linux founder) (англ.)
    • Interview «Ten Questions with David Butenhof about Parallel Programming and POSIX Threads» by Michael Suess (англ.)
    • Open Source POSIX Threads for Win32 (англ.)
    • The Open Group Base Specifications Issue 6, IEEE Std 1003.1 (англ.)
    • GNU Portable threads (англ.)
    • Flash Presentation on pThread (англ.)
    • Pthreads Presentation at 2007 OSCON (O’Reilly Open Source Convention) by Adrien Lamothe. An overview of Pthreads with current trends. (англ.)
    • Стандарты POSIX
    • Потоки выполнения
    • Библиотеки параллельного программирования
    • C POSIX library

    Wikimedia Foundation . 2010 .

    Лекция 2. Реализации POSIX Threads API и сборка многопоточных программ

    Стандарт POSIX допускает различные подходы к реализации многопоточности в рамках одного процесса. Возможны три основных подхода:

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

    1.1. Пользовательские нити

    Реализация планировщика в пользовательском адресном пространстве не представляет больших сложностей; наброски реализаций таких планировщиков приводятся во многих учебниках по операционным системам, в том числе в Иртегов 2002. Учебная многозадачная ОС Minix может быть собрана и запущена в виде задачи под обычной Unix-системой; при этом процессы Minix будут с точки зрения ядра системы-хозяина пользовательскими нитями. Главным достоинством пользовательского планировщика считается тот факт, что он может быть реализован без изменений ядра системы.
    При практическом применении такого планировщика, однако, возникает серьезная проблема. Если какая-то из нитей процесса исполняет блокирующийся системный вызов, блокируется весь процесс. Устранение этой проблемы требует серьезных изменений в механизме взаимодействия диспетчера системных вызовов с планировщиком операционной системы. То есть главное достоинство пользовательского планировщика при этом будет утеряно.
    Другим недостатком пользовательских нитей является то, что они не могут воспользоваться несколькими процессорами на однопроцессорной машине – ведь процесс всегда планируется только на одном процессоре!
    Наиболее известная реализация пользовательских нитей – это волокна (fibers) в Win32. Считается, что волокна дешевле системных нитей Win32, хотя данных, подтверждающих это утверждение практическими измерениями, у меня нет. Волокна могут использоваться совместно с системными нитями Win32, но при этом волокна привязаны к определенной нити и исполняются только в контексте этой нити. Волокна не должны исполнять блокирующиеся системные вызовы; попытка сделать это приведет к блокировке нити. Некоторые системные вызовы Win32 имеют неблокирующиеся аналоги, предназначенные для использования в волокнах, но далеко не все. Это резко ограничивает применение волокон в реальных приложениях.

    1.2. Системные нити

    Ядро типичной современной ОС уже имеет планировщик, способный переключать процессы. Переделка этого планировщика для того, чтобы он мог переключать несколько нитей в пределах одного процесса, также не представляет больших сложностей. При этом возможны два подхода к такой переделке.
    В рамках первого подхода, системные нити выступают как подчиненная по отношению к процессу сущность. Идентификатор нити состоит из идентификатора родительского процесса и собственного идентификатора нити. Идентификатор нити локален по отношению к процессу, т.е. нити разных процессов могут иметь одинаковые идентификаторы. Такой подход реализует большинство систем, реализующих системные нити – IBM MVS-OS/390-zOS, DEC VAX/VMS-HP OpenVMS, OS/2, Win32, многие Unix-системы, в том числе и Solaris. В Solaris и других Unix-системах (IBM AIX, HP/UX) системные нити называются LWP (Light Weight Process, «легкие процессы»).
    Solaris 10 использует системные нити, так что каждой нити POSIX Threads API соответствует собственный LWP. Старые версии Solaris использовали гибридный подход, который рассматривается в следующем разделе.
    В рамках другого подхода, системные нити являются сущностями того же уровня, что процесс. Иногда все, что объединяет нити одного процесса – это общее адресное пространство. Наиболее известная ОС, использующая такой подход – Linux. В Linux, нити выглядят как отдельные записи в таблице процессов и отдельные строки в выводе команд top(1) и ps(1), имеют собственный идентификатор процесса.
    В старых версиях Linux это приводило к своеобразным проблемам при реализации POSIX Threads API; так, в большинстве Unix-систем завершение процесса системным вызовом exit(2) приводит к немедленному завершению всех его нитей; в Linux вплоть до 2.4 завершалась только текущая нить. В Linux 2.6 был внесен ряд изменений в ядро, приблизивших семантику многопоточности к стандарту POSIX. Эти изменения и соответствующая функциональность в libpthread.so/libc.so известны как NPTL (Native POSIX Threads Library).
    Наш курс рассчитан на стандартную семантику POSIX Threads API. При программировании для старых (2.4 и младше) версий ядра Linux необходимо изучить особенности поведения этих систем по документации, поставляющейся с системой, или по другим источникам.

    1.3. Гибридная реализация

    В гибридной реализации многопоточный процесс имеет несколько LWP и планировщик в пользовательском адресном пространстве. Этот планировщик переключает пользовательские нити между свободными LWP, подобно тому, как системный планировщик в многопроцессорной системе переключает процессы и системные нити между свободными процессами. При этом, как правило, процесс имеет больше пользовательских нитей, чем у него есть LWP.
    Причина, по которой этот подход нашел практическое применение – это убеждение разработчиков первых многопоточных версий Unix, что пользовательские нити дешевле системных, требуют меньше ресурсов для своего исполнения.
    При планировании пользовательских нитей возникает проблема блокирующихся системных вызовов. Когда какая-то нить вызывает блокирующийся системный вызов, соответствующий LWP блокируется и на некоторое время выпадает из работы. В старых версиях Solaris эта проблема решалась следующим образом: многопоточная библиотека всегда имела выделенную нить, которая не вызывала блокирующихся системных вызовов никогда. Когда ядро системы обнаруживало, что все LWP процесса заблокированы, оно посылало процессу сигнал SIGWAITING. Библиотечная нить перехватывала этот сигнал и, если это допускалось настройками библиотеки, создавала новый LWP.
    Таким образом, если все пользовательские нити исполняли блокирующиеся системные вызовы, то количество LWP могло сравняться с количеством пользовательских нитей. Можно предположить, что от компания Sun отказалась от гибридной реализации многопоточности именно потому, что обнаружилось, что такое происходит со многими реальными прикладными программами.
    В старых версиях Solaris поддерживался довольно сложный API, позволявший управлять количеством LWP и политикой планирования нитей между ними. Так, можно было привязать нить к определенному LWP. Этот API был частью Solaris Native Threads и нестандартным расширением POSIX Threads API. В рамках данного курса этот API не изучается.
    Многие современные Unix-системы, в том числе SCO UnixWare, IBM AIX, HP/UX используют гибридную реализацию POSIX Thread API.

    2. Сборка приложений с POSIX Threads

    Большинство систем, реализующих POSIX Threads, требуют сборки многопоточной программы с библиотекой libpthread.so или libpthread.a. Как правило, это достигается запуском компилятора с ключом -lpthread.

    Примечание

    Большинство C и C++ компиляторов интерпретируют ключ -l следующим образом. К параметру ключа (у ключа -lpthread параметром является строка pthread) спереди добавляется строка lib, а сзади – строка .so или .a, в зависимости от того, какой режим сборки задан другими ключами – статический или динамический. Таким образом получается строка libpthread.so. Затем файл с таким именем ищется в каталогах, перечисленных в переменной среды LIBPATH. Если эта переменная не установлена, используются каталоги /lib, /usr/lib, и, возможно, некоторые другие каталоги, зашитые в компилятор.
    Так, компилятор Sun Studio 11 при установке по умолчанию ищет дополнительные библиотеки в каталоге /opt/SUNWspro/lib.
    Компилятор GCC, входящий в поставку Solaris 10, ищет дополнительные библиотеки в каталоге /usr/sfw/lib; в действительности, при сборке GCC ему можно указать пути к дополнительным библиотекам; при сборке GCC из исходных текстов по умолчанию он настраивается на размещение драйвера компилятора (команд gcc и g++) в /usr/local/bin, а библиотек – в каталоге /usr/local/lib. Эти каталоги можно изменять параметрами скрипта configure, который необходимо запустить перед началом сборки компилятора.
    В Solaris 10 ключ -lpthread использовать не обязательно – все функции POSIX Thread API включены в стандартную библиотеку языка C libc.so, которая подключается по умолчанию. Для совместимости со старыми сборочными скриптами в поставку Solaris 10 также включена пустая библиотека libpthread.so, содержащая ссылки на соответствующие функции в libc.so.

    Внимание
    В некоторых дистрибутивах Linux, библиотека libstdc.so (стандартная библиотека языка С) содержит ссылки на функции libpthread.so. Чтобы облегчить сборку однопоточных программ с такой библиотекой, в библиотеку libstdc++.so были включены пустые функции, одноименные функциям POSIX Thread API. При сборке многопоточных программ с такой библиотекой необходимо обязательно указывать ключ -lpthread. Без этого ключа программа соберется (редактор связей не выдаст сообщений о неопределенных символах), но работать не будет (вызов функций POSIX Thread API приведет к ошибке сегментации).

    Кроме того, многие компиляторы – в том числе и Sun Studio 11 – рекомендуют компилировать все модули, входящих в многопоточную программу, с ключом -mt. В старых системах это могло быть жизненно необходимо, так как в зависимости от наличия или отсутствия этого ключа компилятор мог подключать разные версии стандартной библиотеки времени исполнения. В Solaris 10 этот ключ отвечает только за определение некоторых препроцессорных символов. Впрочем, в следующем разделе мы увидим, что некоторые эти символы также важны при сборке некоторых программ.
    Кроме того, ключ -mt может выключать некоторые оптимизации, опасные при многопоточном исполнении. Поэтому если компилятор поддерживает ключ -mt, рекомендуется его использовать как при компиляции, так и при сборке многопоточных программ.
    Компилятор GCC не поддерживает ключ -mt, вместо этого рекомендуется использовать ключ -threads или -pthread на тех платформах, где эти ключи поддерживаются. GCC, входящий в поставку Solaris 10, поддерживает ключи -threads и –pthread; GCC из поставки Debian Sarge поддерживает только ключ -pthread.
    Ниже приводится пример кода, который позволяет протестировать ваш компилятор и проверить наличие типичных препроцессорных символов, используемых в include-файлах стандартной библиотеки языка С для проверки того, однопоточная или многопоточная программа сейчас компилируется. _LIBC_REENTRANT используется в Linux, _REENTRANT в Solaris. На других платформах могут использоваться другие символы. Попробуйте собрать эту программу с разными ключами компилятора и проверить результат. Полезно также поискать соответствующие символы в файлах каталога /usr/include и посмотреть, какие именно конструкции они контролируют.
    Пример 1

    3. Программы для экспериментов

    Получите вывод препроцессора для программы примера 2 с различными ключами компиляции. Вывод препроцессора у большинства компиляторов С (в том числе у компиляторов Sun Studio 11 и GCC) генерируется ключом -E и выдается в stdout. Для того, чтобы перенаправить вывод препроцессора в файл, используйте переназначение ввода-вывода. Можно также использовать для просмотра вывода компилятора фильтры more(1) или less(1).
    Напоминаю, что переменная errno хранит код ошибки последнего неудачно завершенного системного вызова. Код ошибки EMFILE означает исчерпание лимита дескрипторов файлов на процесс. Таким образом, функция open_with_wait пытается открыть файл и, если лимит дескрипторов исчерпан, блокирует нить в надежде, что какая-то другая нить освободит дескриптор. Функции pthread_cond_wait(3C), pthread_cond_signal(3C), pthread_mutex_lock(3C), pthread_mutex_unlock(3C) и используемые ими типы данных изучаются далее в нашем курсе.
    Посмотрите, в какой код превращается обращение к переменной errno. Посмотрите, как этот код зависит от используемых ключей компиляции. Найдите в файле /usr/include/errno.h макроопределения, ответственные за эту замену. От каких предопределенных символов препроцессора они зависят?
    Подумайте, к чему привело бы в многопоточной программе обращение к переменной errno как к обычной переменной. Можно ли, как это рекомендуется в некоторых старых руководствах по Unix, описывать переменную errno как extern int errno, или следует обязательно использовать определение из файла /usr/include/errno.h?
    Пример 2

    2007-07-04 15:57:58 (55 Кб) lecture_2_ptreads_implementation.ppt

    Потоки POSIX — POSIX Threads

    Потоки POSIX , обычно называемые pthreads , представляют собой модель выполнения который существует независимо от языка, а также от модели параллельного выполнения. Это позволяет программе контролировать несколько различных потоков работы, которые перекрываются во времени. Каждый рабочий поток называется потоком , и создание и контроль над этими потоками достигается путем выполнения вызовов API потоков POSIX. POSIX Threads — это API , определенный стандартом POSIX.1c, расширениями потоков (IEEE Std 1003.1c-1995).

    Реализации API доступны во многих Unix-подобных POSIX-совместимых операционных системах, таких как FreeBSD , NetBSD , OpenBSD , Linux , macOS , Android , Solaris , Redox и AUTOSAR Адаптивный, обычно в комплекте как библиотека libpthread . Также существуют реализации DR-DOS и Microsoft Windows : в подсистеме SFU / SUA , которая обеспечивает встроенную реализацию ряда API-интерфейсов POSIX, а также в рамках сторонние пакеты, такие как pthreads-w32, который реализует pthreads поверх существующего Windows API .

    • 1 Содержание
    • 2 Пример
    • 3 POSIX Threads для Windows
    • 4 См. Также
    • 5 Ссылки
    • 6 Дополнительная литература
    • 7 Внешние ссылки

    pthreads определяет набор C язык программирования типы , функции и константы. Он реализован с заголовком pthread.h и библиотекой потока .

    Существует около 100 процедур потоков, все с префиксом pthread_ , и их можно разделить на четыре группы:

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

    POSIX семафор API работает с потоками POSIX, но не является частью стандарта потоков, который был определен в стандарте POSIX.1b, Расширения реального времени (IEEE Std 1003.1b-1993). Следовательно, процедуры семафоров имеют префикс sem_ вместо pthread_ .

    Пример

    Пример, иллюстрирующий использование pthreads в C:

    #include #include #include #include #include #define NUM_THREADS 5 void * perform_work (void * arguments) int main (void) printf("IN MAIN: All threads are created.\n"); //wait for each thread to complete for (i = 0; i < NUM_THREADS; i++) < result_code = pthread_join(threads[i], NULL); assert(!result_code); printf("IN MAIN: Thread %d has ended.\n", i); >printf("MAIN program has ended.\n"); return 0; >

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

    gcc pthreads_demo.c -lpthread -o pthreads_demo

    Вот один из многих возможных результатов выполнения этой программы.

    В ГЛАВНО: Создание потока 0. В ГЛАВНО: Создание потока 1. В ГЛАВНОМ: создание потока 2. В ГЛАВНОМ: создание потока 3. НИТИ 0: начато. В ГЛАВНУЮ: создание потока 4. НИТИ 3: начато. НИТЬ 2: начато. НИТЬ 0: будет спать в течение 3 секунд. НИТИ 1 : Запущено. НИТЬ 1: будет спать в течение 5 секунд. НИТЬ 2: будет спать в течение 4 секунд. НИТЬ 4: запущена. НИТЬ 4: будет спать в течение 1. секунд. ГЛАВНАЯ: Все темы созданы. НИТЬ 3: будет спать 4 секунды. НИТЬ 4: завершена. РЕЗЬБА 0: завершена. ГЛАВНАЯ: Тема 0 завершена. НИТЬ 2: Закончена. НИТЬ 3: Закончена. НИТЬ 1: Закончена. ГЛАВНАЯ: Тема 1 завершена. ГЛАВНАЯ: Тема 2 завершена. ГЛАВНАЯ: Тема 3 завершена. ГЛАВНАЯ: Тема 4 завершена. ГЛАВНАЯ программа закончилась.

    POSIX Threads для Windows

    Windows не поддерживает стандарт pthreads изначально, поэтому проект Pthreads-w32 стремится предоставить переносимую оболочку с открытым исходным кодом реализация. Его также можно использовать для переноса программного обеспечения Unix (которое использует pthreads ) с небольшими изменениями или без изменений для платформы Windows. Последняя версия 2.8.0 с некоторыми дополнительными патчами совместима с 64-битными системами Windows. 2.9.0 также считается 64-битной совместимой.

    Проект mingw-w64 также содержит реализацию оболочки pthreads , winpthreads , которая пытается использовать больше собственных системных вызовов, чем проект Pthreads-w32.

    Подсистема среды Interix , доступная в пакете Службы Windows для UNIX / Подсистема для приложений на основе UNIX , предоставляет собственный порт pthreads API, т.е. не сопоставлен с Win32 / Win64 API, а построен непосредственно на интерфейсе syscallоперационной системы .

    См. также

    • Система времени выполнения
    • OpenMP
    • Cilk / Cilk Plus
    • Строительные блоки потоков (TBB)
    • Собственная библиотека потоков POSIX (NPTL)
    • DCEThreads
    • clone (системный вызов Linux)
    • Ложное пробуждение
    • Локальное хранилище потока
    • Переносимые потоки GNU
    • Grand Central Dispatch (библиотека потоков Apple)
    • Beginthread (подпрограмма в Windows для создания нового потока и потока unix )
    • State Threads , управляемый событиями подход к потокам

    Ссылки

    Дополнительная литература

    • Дэвид Р. Бутенхоф (1997). Программирование с использованием потоков POSIX. Эддисон-Уэсли. ISBN978-0-201-63392-4 .
    • Брэдфорд Николс; Дик Батлар; Жаклин Пру Фарелл (сентябрь 1996 г.). Программирование потоков Pthreads . O’Reilly & Associates. ISBN978-1-56592-115-3 .
    • Чарльз Дж. Нортрап (1996-01-25). Программирование с использованием потоков UNIX . Джон Вили и сыновья. ISBN978-0-471-13751-1 .
    • Кей А. Роббинс и Стивен Роббинс (2003). Системное программирование UNIX . Прентис-Холл. ISBN978-0-13-042411-2 .

    Внешние ссылки

    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. Далее, вызывающая сторона продолжает выполнять какие-то свои действия параллельно потоковой функции.

    #include int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start)(void *), void *arg);

    При удачном завершении pthread_create() возвращает код 0, ненулевое значение сигнализирует об ошибке.

    • Первый параметр вызова pthread_create() является адресом для хранения идентификатора создаваемого потока типа pthread_t .
    • Аргумент start является указателем на потоковую void * функцию, принимающей бестиповый указатель в качестве единственной переменной.
    • Аргумент arg — это бестиповый указатель, содержащий аргументы потока. Чаще всего arg указывает на глобальную или динамическую переменную, но если вызываемая функция не требует наличия аргументов, то в качестве arg можно указать NULL .
    • Аргумент attr также является бестиповым указателем атрибутов потока pthread_attr_t . Если этот аргумент равен NULL , то поток создается с атрибутами по умолчанию.

    Рассмотрим теперь пример многопоточной программы.

    #include #include int count; /* общие данные для потоков */ int atoi(const char *nptr); void *potok(void *param); /* потоковая функция */ int main(int argc, char *argv[]) < pthread_t tid; /* идентификатор потока */ pthread_attr_t attr; /* отрибуты потока */ if (argc != 2) < fprintf(stderr,"usage: progtest \n"); return -1; > if (atoi(argv[1]) < 0) < fprintf(stderr,"Аргумент %d не может быть отрицательным числом\n",atoi(argv[1])); return -1; >/* получаем дефолтные значения атрибутов */ pthread_attr_init(&attr); /* создаем новый поток */ pthread_create(&tid,&attr,potok,argv[1]); /* ждем завершения исполнения потока */ pthread_join(tid,NULL); printf("count = %d\n",count); > /* Контроль переходит потоковой функции */ void *potok(void *param) < int i, upper = atoi(param); count = 0; if (upper >0) < for (i = 1; i pthread_exit(0); >

    Чтобы подключить библиотеку Pthread к программе, нужно передать компоновщику опцию -lpthread .

    gcc -o progtest -std=c99 -lpthread progtest.c

    О присоединении потока pthread_join расскажу чуть позже. Строка pthread_t tid задает идентификатор потока. Атрибуты функции задает pthread_attr_init(&attr) . Так как мы не задавали их явно, будут использованы значения по умолчанию.

    Завершение потока

    Поток завершает выполнение задачи когда:

    • потоковая функция выполняет return и возвращает результат произведенных вычислений;
    • в результате вызова завершения исполнения потока pthread_exit() ;
    • в результате вызова отмены потока pthread_cancel() ;
    • одна из нитей совершает вызов exit()
    • основная нить в функции main() выполняет return , и в таком случае все нити процесса резко сворачиваются.

    Синтаксис проще, чем при создании потока.

    #include void pthread_exit(void *retval);

    Если в последнем варианте старшая нить из функции main() выполнит pthread_exit() вместо просто exit() или return , то тогда остальные нити продолжат исполняться, как ни в чем не бывало.

    Ожидание потока

    Функция pthread_join() ожидает завершения потока обозначенного THREAD_ID . Если этот поток к тому времени был уже завершен, то функция немедленно возвращает значение. Смысл функции в том, чтобы синхронизировать потоки. Она объявлена в pthread.h следующим образом:

    #include int pthread_join (pthread_t THREAD_ID, void ** DATA);

    При удачном завершении pthread_join() возвращает код 0, ненулевое значение сигнализирует об ошибке.

    Если указатель DATA отличается от NULL , то туда помещаются данные, возвращаемые потоком через функцию pthread_exit() или через инструкцию return потоковой функции. Несколько потоков не могут ждать завершения одного. Если они пытаются выполнить это, один поток завершается успешно, а все остальные — с ошибкой ESRCH. После завершения pthread_join() , пространство стека связанное с потоком, может быть использовано приложением.

    В каком-то смысле pthread_joini() похожа на вызов waitpid() , ожидающую завершения исполнения процесса, но с некоторыми отличиями. Во-первых, все потоки одноранговые, среди них отсутствует иерархический порядок, в то время как процессы образуют дерево и подчинены иерархии родитель — потомок. Поэтому возможно ситуация, когда поток А, породил поток Б, тот в свою очередь заделал В, но затем после вызова функции pthread_join() А будет ожидать завершения В или же наоборот. Во-вторых, нельзя дать указание одному ожидай завершение любого потока, как это возможно с вызовом waitpid(-1, &status, options) . Также невозможно осуществить неблокирующий вызов pthread_join() .

    Досрочное завершение потока

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

    int pthread_cancel (pthread_t THREAD_ID);

    При удачном завершении pthread_cancel() возвращает код 0, ненулевое значение сигнализирует об ошибке.

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

    Небольшая иллюстрация создания и отмены потока.

    pthread_t tid; /* создание потока */ pthread_create(&tid, 0, worker, NULL); … /* досрочное завершение потока */ pthread_cancel(tid);

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

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

    while (1) < /* чего-то там делаем */ /* пам-парам-пам-пам */ /* не пора-ли сворачиваться? */ pthread_testcancel(); >
    Отсоединение потока

    Любому потоку по умолчанию можно присоединиться вызовом pthread_join() и ожидать его завершения. Однако в некоторых случаях статус завершения потока и возврат значения нам не интересны. Все, что нам надо, это завершить поток и автоматически выгрузить ресурсы обратно в распоряжение ОС. В таких случаях мы обозначаем поток отсоединившимся и используем вызов pthread_detach() .

    #include int pthread_detach(pthread_t thread);

    При удачном завершении pthread_detach() возвращает код 0, ненулевое значение сигнализирует об ошибке.

    Отсоединенный поток — это приговор. Его уже не перехватить с помощью вызова pthread_join() , чтобы получить статус завершения и прочие плюшки. Также нельзя отменить его отсоединенное состояние. Вопрос на засыпку. Что будет, если завершение потока не перехватить вызовом pthread_join() и чем это отлично от сценария, при котором завершился отсоединенный поток? В первом случае мы получим зомбо-поток, а во втором — все будет норм.

    Потоки versus процессы

    Напоследок предлагаю рассмотреть несколько соображений на тему, следует ли проектировать приложение многопоточным или запускать его в несколько процессов с одним потоком? Сперва выгоды параллельных множественных потоков.

    В начальной части статьи мы уже указывали на эти преимущество, поэтому вкратце их просто перечислим.

    • Потоки довольно просто обмениваются данными по сравнению с процессами.
    • Создавать потоки для ОС проще и быстрее, чем создавать процессы.

    Теперь немного о недостатках.

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

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

    Использованные материалы и дополнительная информация
    1. Michael Kerrisk The Linux Programming Interface.
    2. Abraham Silberschatz, Peter B. Galvin Greg Gagne, Operating System Concepts 9-th ed.
    3. Николай Иванов Самоучитель программирования в Linux 2-е издание.
    4. Эндрю Таненбаум Архитектура компьютера.

Добавить комментарий

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