Posix win threads for windows что это

Обновлено: 05.07.2024

Я устанавливаю mingw-w64 на Windows, и есть два варианта: win32 threads и posix threads. Я знаю, в чем разница между потоками win32 и pthreads, но я не понимаю, в чем разница между этими двумя вариантами. Я сомневаюсь, что если я выберу потоки posix, это помешает мне вызывать функции WinAPI, такие как CreateThread.

кажется, что эта опция указывает, какой API потоков будет использоваться некоторой программой или библиотекой, но чем? По НКУ, libstdc++ или что-то еще?

Я нашел это: в чем разница между thread_posixs и thread_win32 в порту gcc windows?

короче говоря, для этой версии mingw выпуск threads-posix будет использовать API posix и разрешать использование std::thread, а threads-win32 будет использовать API win32 и отключит часть std::thread стандарта.

Ok, если я выберу win32 threads, то std:: thread будет недоступно, но потоки win32 по-прежнему будут использоваться. Но используется кем?

GCC поставляется с библиотекой времени выполнения компилятора (libgcc), которую он использует (среди прочего), обеспечивая низкоуровневую абстракцию ОС для многопоточности связанных функций на языках, которые он поддерживает. Наиболее релевантным примером является libstdc++ ' S C++11 <thread> , <mutex> и <future> , которые не имеют полной реализации, когда GCC построен с его внутренней моделью потоков Win32. MinGW-w64 предоставляет winpthreads (реализация pthreads поверх многопоточного API Win32), который Затем GCC может подключиться, чтобы включить все причудливые функции.

Я должен подчеркнуть, что эта опция не запрещает вам писать любой код, который вы хотите (он имеет абсолютно нет влияние на то, какой API вы можете вызвать в своем коде). Он отражает только то, что библиотеки времени выполнения GCC (libgcc / libstdc++/. ) использовать для их функциональности. Оговорка, цитируемая @James, не имеет ничего общего с внутренней моделью потоковой передачи GCC, а скорее с реализацией CRT Microsoft.

для подведем итог:

  • posix : включить функции многопоточности C++11/C11. Делает libgcc зависеть от libwinpthreads, так что даже если вы непосредственно не вызываете pthreads API, вы будете распространять WINPTHREADS DLL. Нет ничего плохого в распространении еще одной DLL с вашим приложением.
  • win32 : нет функций многопоточности C++11.

ни имеют влияние на любой пользовательский код, вызывающий Win32 APIs или Pthreads APIs. Вы всегда можете использовать оба.

части среды выполнения GCC (в частности, обработка исключений) зависят от используемой модели потоков. Таким образом, если вы используете версию среды выполнения, которая была построена с потоками POSIX, но решили создать потоки в своем собственном коде с API Win32, в какой-то момент у вас могут возникнуть проблемы.

даже если вы используете потоковую версию Win32 среды выполнения, вы, вероятно, не должны вызывать API Win32 напрямую. Цитата из компилятор MinGW FAQ:

поскольку MinGW использует стандартную библиотеку времени выполнения Microsoft C, которая поставляется с Windows, вы должны быть осторожны и использовать правильную функцию для создания нового потока. В частности, CreateThread функция не настроит стек правильно для библиотеки времени выполнения C. Вы должны использовать _beginthreadex вместо этого, который (почти) полностью совместимы с CreateThread .

из истории ревизий похоже, что есть какая-то недавняя попытка сделать это частью среды выполнения mingw64.

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


В этой статье мы познакомимся с POSIX Threads для того, чтобы затем узнать как это все работает в Linux. Не заходя в дебри синхронизации и сигналов, рассмотрим основные элементы Pthreads. Итак, под капотом потоки.

Общие сведения

Множественные нити исполнения в одном процессе называют потоками и это базовая единица загрузки ЦПУ, состоящая из идентификатора потока, счетчика, регистров и стека. Потоки внутри одного процесса делят секции кода, данных, а также различные ресурсы: описатели открытых файлов, учетные данные процесса сигналы, значения umask , nice , таймеры и прочее.


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


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

Таблицы страниц до и после изменения общей страницы памяти во время копирования при записи.


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

Закон Амдаля для распараллеливания процессов.


Используя уравнение, показанное на рисунке, можно вычислить максимальное улучшение производительности системы, использующей N процессоров и фактор F, который указывает, какая часть системы не может быть распараллелена. Например 75% кода запускается параллельно, а 25% — последовательно. В таком случае на двухядерном процессоре будет достигнуто 1.6 кратное ускорение программы, на четырехядерном процессоре — 2.28571 кратное, а предельное значение ускорения при N стремящемся к бесконечности равно 4.

Отображение потоков в режим ядра

Практически все современные ОС — включая Windows, Linux, Mac OS X, и Solaris — поддерживают управление потоками в режиме ядра. Однако потоки могут быть созданы не только в режиме ядра, но и в режиме пользователя. При использовании этого уровня ядро не знает о существовании потоков — все управление потоками реализуется приложением с помощью специальных библиотек. Пользовательские потоки по разному отображаются на потоки в режиме ядра. Всего существует три модели, из которых 1:1 является наиболее часто используемой.

Отображение N:1

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


Отображение 1:1

Это самая проста модель, в которой каждый поток созданный в каком-нибудь процессе непосредственно управляется планировщиком ядра ОС и отображается на один единственный поток в режиме ядра. Чтобы приложение не плодило бесконтрольно потоки, перегружая ОС, вводят ограничение на максимальное количество потоков поддерживаемых в ОС. Данный способ отображения потоков поддерживают ОС Linux и Windows.


Отображение M:N

При таком подходе M пользовательских потоков мультиплексируются в такое же или меньшее N количество потоков ядра. Преодолеваются негативные эффекты двух других моделей: нити по-настоящему исполняются параллельно и нет необходимости в ОС вводить ограничения на их общее количество. Вместе с тем данную модель довольно трудно реализовать с точки зрения программирования.


Потоки POSIX

В конце 1980-х и начале 1990-х было несколько разных API, но в 1995 г. POSIX.1c стандартизовал потоки POSIX, позже это стало частью спецификаций SUSv3. В наше время многоядерные процессоры проникли даже в настольные ПК и смартфоны, так что у большинства машин есть низкоуровневая аппаратная поддержка, позволяющая им одновременно выполнять несколько потоков. В былые времена одновременное исполнение потоков на одноядерных ЦПУ было лишь впечатляюще изобретательной, но очень эффективной иллюзией.

Pthreads определяет набор типов и функций на Си.

  • pthread_t — идентификатор потока;
  • pthread_mutex_t — мютекс;
  • pthread_mutexattr_t — объект атрибутов мютекса
  • pthread_cond_t — условная переменная
  • pthread_condattr_t — объект атрибута условной переменной;
  • pthread_key_t — данные, специфичные для потока;
  • pthread_once_t — контекст контроля динамической инициализации;
  • pthread_attr_t — перечень атрибутов потока.

В традиционном Unix API код последней ошибки errno является глобальной int переменной. Это однако не годится для программ с множественными нитями исполнения. В ситуации, когда вызов функции в одном из исполняемых потоков завершился ошибкой в глобальной переменной errno , может возникнуть состояние гонки из-за того, что и остальные потоки могут в данный момент проверять код ошибки и оконфузиться. В Unix и Linux эту проблему обошли тем, что errno определяется как макрос, задающий для каждой нити собственное изменяемое lvalue .

Из man errno
Переменная errno определена в стандарте ISO C как изменяемое lvalue int и не объявляемая явно; errno может быть и макросом. Переменная errno является локальным значением нити; её изменение в одной нити не влияет на её значение в другой нити.

Создание потока

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

Потоки 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 .

Содержание

Содержание

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

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

  • Управление потоками - создание, объединение потоков и т. Д. между потоками с использованием блокировок чтения / записи и барьеров

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

Пример

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

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

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

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операционной системы .

POSIX Threads - стандарт POSIX реализации потоков выполнения. Для предоставления возможности создания переносимых многопоточных программ, институтом IEEE был определен стандарт - IEEE standard 1003.1c. Сегодня используется расширенная редакция стандарта — POSIX 1003.1-2001. Определенный в нем пакет, касающийся потоков, называется Pthreads. Создание и контролирование потоков достигается путём вызова POSIX Threads API. Реализации API доступны на многих UNIX-подобных операционных системах, таких как NetBSD, OpenBSD, Linux и другие. DR-DOS и Microsoft Windows реализации также существуют, в рамках SFU/SUA подсистем, которые обеспечивают нативную реализацию ряда POSIX APIs.

Содержание

Причины использования потоков

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

Основная причина использования потоков заключается в том, что во многих приложениях одновременно происходит несколько действий, часть которых может периодически быть заблокированной. Модель программирования упрощается за счет разделения такого приложения на несколько последовательных потоков, выполняемых в квазипараллельном режиме. Рассматривая потоки, мы добавляем новый элемент: возможность использования параллельными процессами единого адресного пространства и всех имеющихся данных. Эта возможность играет весьма важную роль для тех приложений, которым не подходит использование нескольких процессов (с их раздельными адресными пространствами).

Вторым аргументом в пользу потоков является легкость (то есть быстрота) их создания и ликвидации по сравнению с более «тяжеловесными» процессами. Во многих системах создание потоков осуществляется в 10–100 раз быстрее, чем создание процессов. Это свойство особенно пригодится, когда потребуется быстро и динамично изменять количество потоков. [Источник 1]

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

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

Основные функции

В стандарте определено более 60 вызовов функций. Они могут быть разделены на 4 категории:

  1. Потоковое управление - creating, joining thread
  2. Мьютексы
  3. Условные переменные
  4. Синхронизация между потоками используя read/write блокировки и барьеры.
  5. Спинлок

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

выполняться другому потоку

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

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

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

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

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

Мьютексы в пакете Pthreads

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

Основные вызовы, связанные с мьютексами, показаны в таблице ниже. Как и ожидалось, мьютексы могут создаваться и уничтожаться. Вызовы, осуществляющие эти операции, называются, соответственно, pthread_mutex_init и pthread_mutex_destroy . Мьютексы также могут быть заблокированы вызовом pthread_mutex_lock , который пытается завладеть блокировкой и блокирует выполнение потока, если мьютекс уже заблокирован. Есть также вызов, используемый для попытки заблокировать мьютекс и безуспешного выхода с кодом ошибки, если мьютекс уже был заблокирован. Этот вызов называется pthread_mutex_trylock . Он позволяет потоку организовать эффективное активное ожидание, если в таковом возникнет необходимость. И наконец, вызов pthread_mutex_unlock разблокирует мьютекс и возобновляет работу только одного потока, если имеется один или более потоков, ожидающих разблокирования. Мьютексы могут иметь также атрибуты, но они используются только для решения специализированных задач. Ряд вызовов пакета Pthreads, имеющих отношения к мьютексам.

<ocde>pthread_mutex_init</code> Создание мьютекса
pthread_mutex_destroy Уничтожение существующего мьютекса
pthread_mutex_lock Овладение блокировкой или блокирование потока
pthread_mutex_trylock Овладение блокировкой или выход с ошибкой
pthread_mutex_unlock Разблокирование

Условные переменные

В дополнение к мьютексам пакет Pthreads предлагает второй механизм синхронизации - условные переменные. Мьютексы хороши для разрешения или блокирования доступа к критической области. Условные переменные позволяют потокам блокироваться до выполнения конкретных условий. Эти два метода практически всегда используются вместе.

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

Вызов из потока Описание
pthread_cond_init Создание условной переменной
pthread_cond_destroy Уничтожение условной переменной
pthread_cond_wait Блокировка в ожидании сигнала
pthread_cond_signal Сигнализирование другому потоку и его активизация
pthread_cond_broadcast Сигнализирование нескольким потокам и активизация всех этих потоков

У них могут быть атрибуты, для управления которыми существует ряд других (не показанных в таблице) вызовов. Первичные операции над условными переменными осуществляются с помощью вызовов pthread_cond_wait и pthread_cond_signal . Первый из них блокирует вызывающий поток до тех пор, пока не будет получен сигнал от другого потока (использующего второй вызов). Разумеется, основания для блокирования и ожидания не являются частью протокола ожиданий и отправки сигналов. Заблокированный поток зачастую ожидает, пока сигнализирующий поток не совершит определенную работу, не освободит какие-то ресурсы или не выполнит какие-нибудь другие действия. Только после этого заблокированный поток продолжает свою работу. Условные переменные позволяют осуществлять это ожидание и блокирование как неделимые операции. Вызов pthread_cond_broadcast используется в том случае, если есть потенциальная возможность находиться в заблокированном состоянии и ожидании одного и того же сигнала сразу нескольким потокам.

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

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

Блокировка чтения-записи

Первый механизм, который мы рассмотрим это read/write lock (блокировка чтения/записи). Фактически он, как и мьютекс позволяет организовывать взаимоисключение при доступе к общим данным, однако он в отличие от мьютекса позволяет учитывать то, модифицируем ли мы данные или же только читаем их. Действительно, одновременный доступ по чтению к общим данным из разных потоков выполнения не вызывает никаких проблем, и следовательно вполне логично было бы допускать до одновременной работы с общими данными несколько нитей, которые только читают, или же одну нить, которая модифицирует общие данные [Источник 2] . Именно такую логику и реализует блокировку чтения/записи, он позволяет нескольким нитям одновременно читать общие данные, при условии, что нет нити, которая модифицирует их, и допускает только одну нить до модификации данных. При захвате блокировки чтения/записи нить обязана указать, что она будет только читать данные или желает модифицировать их. То есть здесь в отличие от мьютекса появляются два типа захватов – захват на чтение и захват на запись. Часто соответствующие два типа доступа называют также разделяемый доступ (по чтению) и эксклюзивный (исключительный) доступ (по записи). При этом если нить нарушит свои обещания, то общие данные могут испортиться, например, может нарушиться какой либо из инвариантов, что естественно может привести к ошибке. Блокировки чтения/записи целесообразно использовать в тех ситуациях, когда мы часто читаем общие данные и сравнительно редко модифицируем их. Типичным примером такого рода данных может служить кэш, из которого в основном читают данные, а обновляют его только если какие-то данные отсутствуют в нем.

Согласно стандарту POSIX блокировки чтения/записи будут представляться в виде объектов типа pthread_rwlock_t . Как и в случае мьютексов так и в случае условных переменных этот тип непрозрачен, то есть объекты этого типа не подлежат копированию и нельзя сравнивать. Перед использованием по назначению объект этого типа должен быть проинициализирован при помощи функции:

При этом после инициализации блокировка оказывается в свободном состоянии. Установить разделяемую блокировку чтения можно при помощи функции:

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

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

Барьеры

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

Стандарт POSIX поддерживает барьеры в качестве опции. В программе они представляются при помощи непрозрачного типа pthread_barrier_t , объекты которого можно инициализировать при помощи вызова функции:

Где последний параметр count задает число нитей, которые должны дойти до барьера (вызвать функцию pthread_barrier_wait ) прежде чем они смогут продолжить выполнение. Раз существует функция инициализирующая барьер, то существует и комплиментарная функция которая уничтожает его:

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

Как сказано в стандарте она синхронизирует выполнение нитей участников барьера. Нить вызвавшая ее блокируется до тех пор пока count нитей не вызовут ее для данного барьера. При возврате из pthread_barrier_wait в одной из нитей преодолевших барьер будет возвращено отличное от нуля значение PTHREAD_BARRIER_SERIAL , что позволяет выделить единственную нить в которой выполнять последовательный код (смотри наш пример с суммированием). После возврата всех нитей из pthread_barrier_wait барьер приходит в исходное состояние, то есть оказывается снова готов к использованию.

Спинлок

Третий примитив синхронизации, про который хотелось бы поговорить это spin lock(спинлок). С точки зрения функциональности этот примитив ничем не отличается от мьютекса. Основная стоящая за ними идея состоит в том, чтобы вместо того чтобы заблокировать нить в случае если блокировка уже захвачена, крутиться, проверяя не освободилась ли блокировка (отсюда и слово spin в названии). Фактически спинлок это мьютекс основанный на использовании активного ожидания. Какой тогда смысл в таком примитиве? В случае если мы имеем мультипроцессор (так как на однопроцессорной машине спинлок будет бесполезно съедать циклы процессора до тех пор, пока не произойдет переключение на другую владеющую блокировкой нить) и крайне малый размер критической секции защищаемой этим примитивом (чтобы опять же не ждать долго и снизить опасность переключения на другую нить) спинлок может быть использован эффективнее чем мьютекс. Это обусловлено тем что он не вызывает долгой операции перевода нити в сон, вообще считается что spin lock реализуется на основе самых быстрых механизмов синхронизации доступных в системе. В случае если какое-либо из двух вышеприведенных условий нарушается, спинлок не даст выигрыша в производительности и выгоднее воспользоваться мьютексом. Согласно стандарту данный примитив представляется при помощи типа pthread_spinlock_t и набора операций над ним, которые практически совпадают с соответствующими операциями для мьютекса:

Заключение

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

Читайте также: