Продвинутое программирование в среде Unix: главы 11–12 и интерфейс программирования Linux: главы 29–33

Преимущества многопоточных программ:

  • Несколько процессов имеют сложные механизмы для совместного использования памяти и файловых дескрипторов, потоки автоматически получают доступ к одному и тому же адресному пространству и файловым дескрипторам.
  • Некоторые проблемы можно разделить, чтобы задачи можно было выполнять более эффективно.
  • Повысьте скорость отклика программ ввода-вывода или пользовательского интерфейса.
  • Создание потока может быть примерно в 10 раз быстрее, чем fork().

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

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

Интерфейс потоков — это потоки posix или pthreads, тест функций — _POSIX_THREADS для тестов времени компиляции или sysconf и _SC_THREADS для тестов времени выполнения.

Идентификатор темы

Идентификатор потока, представленный типом данных pthread_t, может сравнивать два потока pthread с помощью pthread_equal(), может получить собственный идентификатор с помощью pthread_self().

Создание темы

Модель процесса Unix поддерживает только один поток управления для каждого процесса, за ним следует pthread, пока новые потоки не будут созданы:

интервал pthread_create(

  • pthread_t *restrict tidp, : сохраняет идентификатор нового потока
  • const pthread_attr_t *restrict attr, : установить атрибуты потока
  • void *(*start_rtn)(void *), : функция для запуска
  • void *restrict arg); : аргумент передается в pthread, если нужно создать несколько структур

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

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

Вызов exit внутри pthreads завершит весь процесс. Чтобы выйти из потока:

  1. возвращение
  2. отменить другим потоком, используя _cancel
  3. void pthread_exit(void *rval_ptr :значение, доступное для передачи в phtread_join());
  4. любой поток вызывает exit() или main возвращает

Можно заблокировать другой поток до выхода из потока с помощью

int pthread_join(pthread_t thread :идентификатор потока для присоединения, void **rval_ptr: хранит возвращаемое значение start_fn ); если поток был в отсоединенном состоянии, это не удастся. Нельзя хранить выделенную стеком память потока внутри значений _join или _exit — если нужно вернуть структуру, используйте malloc или global.

Можно отменить другой поток с помощью: int pthread_cancel(pthread_t tid);

Можно настроить обработчики выхода с помощью pthread_cleanup_push, pthread_cleanup_pop, но обработчики вызываются только тогда, когда поток не завершился с возвратом. Статус завершения потока сохраняется до тех пор, пока не будет вызван pthread_join, но при отсоединении основное хранилище немедленно освобождается. После отсоединения больше не может быть _joined(). Отсоединяется вызовом pthread_detach.

Синхронизация потоков

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

Мьютексы (взаимные исключения)

Блокировки, используемые перед доступом к общему ресурсу, который предоставляет эксклюзивный доступ к потоку, пока у него разблокирован ресурс, другие ждут. Для pthreads тип данных pthread_mutex_t. Может запускаться с помощью PTHREAD_MUTEX_INITIALIZER (для статически выделенных мьютексов) или pthread_mutex_init.

pthred_mutex_(lock/trylock/timedlock/unlock). Блокировка будет блокироваться до тех пор, пока ресурс не станет доступным, тогда как _trylock завершится ошибкой и установит EBUSY, если блокировка недоступна во время вызова, _timedlock ограничивает количество времени, в течение которого поток будет ожидать получения, пока вы вызываете разблокировку, чтобы освободить блокировку.

Предотвращение взаимоблокировки

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

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

Подобно мьютексам, но допускает больший параллелизм — возможны 3 состояния:

  • заблокирован в режиме чтения
  • заблокирован на запись
  • разблокирован

Одновременно только один поток может удерживать rw-lock в режиме записи, а несколько — в режиме чтения. При блокировке записи блокируются все блокировки чтения, а любые операции чтения блокируются блокировкой записи. Гораздо лучшая производительность, когда ресурсы читаются гораздо больше, чем записываются. Может инициализироваться с помощью pthread_rwlock_init или PTHREAD_RWLOCK_INITIALIZER для статических распределений. _destroy освободит любые ресурсы, выделенные _init, если их освободить перед уничтожением, ресурсы могут быть потеряны. Может вызывать _rdlock или _wrlock, или _tryrdlock или _trywrlock, но _unlock освобождает оба. Также можно указать тайм-ауты с помощью _timerdlock или _timedwrlock. Также известна как совместно-эксклюзивная блокировка, так как она может быть заблокирована в общем (чтении) режиме или заблокирована в монопольном режиме (записи).

Переменные условия

Переменные условия позволяют любому количеству потоков ожидать, пока другой поток не просигнализирует об условии, аналогично событию, которое разбудит другой поток. Условия должны быть объединены с мьютексом, чтобы заставить работать. Инициализируйте с помощью pthread_cond_init или PTHREAD_COND_INITIALIZER, затем можно использовать с _wait или _timedwait. Можно использовать _signal для пробуждения хотя бы одного потока или _broadcast для всех потоков, ожидающих сигнала. Следует использовать цикл while для проверки условия, а не оператор if — нет гарантии, что поток проснется только при достижении условия — также известное как ложное пробуждение. Также другие потоки могут быть разбужены первыми, что исключает необходимость продолжения. Другой поток, который отправляет сигнал, должен установить какой-либо предикат в значение true и позволить потоку ожидания продолжаться.

Спин-блокировки

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

Барьеры

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

Атрибуты цепочки

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

Может создавать/удалять с помощью pthread_attr_init/destroy(pthread_attr_t *attr), _destroy заполнит атрибут недопустимыми значениями, поэтому попытки использовать его при создании pthread будут вернуть ошибку

  • detach: Отсоединенные потоки позволяют системе восстанавливать ресурсы потоков сразу после выхода из потока, не позволяя присоединяться к нему после выхода.
  • размер стека: виртуальное адресное пространство процессов фиксировано, что обычно не является проблемой только с одним стеком. С потоками возможно превышение размера, если стеки потоков слишком велики, или если так много потоков, что количество * минимальный размер стека превышает адресное пространство. Можно указать диапазон адресов для mmap для использования в качестве стека для потока или указать размер и позволить _setstacksize самому выделить пространство.
  • размер защиты: контроль размера памяти после стека потока для защиты от переполнения стека.

Атрибуты мьютекса

  • общий атрибут процесса: разрешить мьютексам, выделенным из памяти, доступной из нескольких процессов, синхронизировать доступ к памяти из указанных процессов. разрешение доступа между процессами обходится дороже, чем внутри одного процесса.
  • надежный атрибут: при совместном использовании мьютекса между процессами процесс может умереть, удерживая мьютекс. поведение по умолчанию состоит в том, чтобы позволить этому случиться, что приводит к «зависанию» и неопределенному поведению. также может быть установлен как надежный и позволить другой блокировке получить мьютекс, но с кодом EOWNERDEAD вместо 0 (указывает на необходимость восстановления). Поскольку состояние программы потенциально может быть невосстановимым, необходимо вызвать pthread_mutex_consistent перед разблокировкой, иначе блокировка других вызовов завершится с ошибкой ENOTERECOVERABLE.
  • Атрибут type: управление блокирующими характеристиками мьютекса: PTHREAD_MUTEX_NORMAL (без проверки ошибок или обнаружения взаимоблокировок), _ERRORCHECK (обеспечивает проверку ошибок), _RECURSIVE (разрешает одному и тому же потоку блокировать мьютекс несколько раз без разблокировки, создает счетчик блокировок и разблокирует, только когда count == 0. _DEFAULT предоставляет характеристики по умолчанию, зависящие от ОС

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

Атрибуты RWLock

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

Атрибуты переменной условия

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

Атрибуты барьера

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

Повторный вход

Функции, которые могут быть безопасно вызваны несколькими потоками одновременно, называются потокобезопасными, если они выполняются без использования мьютексов, известных как повторно входящие. Функции, использующие глобальные или статические переменные, не являются потокобезопасными. Библиотечные функции, которые не являются альтернативными, могут иметь альтернативы с суффиксом _r, чтобы пометить их как реентерабельные. Thread-safe отличается от async-signal-safe, что означает безопасный повторный вход из обработчика асинхронного сигнала. Может использовать f(un)lockfile для управления файловыми объектами потокобезопасным способом. Можно использовать (get/put)(c/char)_unlocked для выполнения потокового ввода-вывода. Если бы стандартные подпрограммы ввода-вывода получали блокировки, возникали бы проблемы с производительностью при выполнении ввода-вывода посимвольно. Этим разблокированным функциям должны предшествовать и следовать файлы flockfile и funlockfile.

Один раз

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

Специфичные для потока данные

Поскольку потоки могут «делиться» переменными в инициализированных, неинициализированных данных и данных сегмента кучи, нужны способы пометить переменные как локальные для потока. Можно использовать ключи pthread, чтобы пометить данные как локальные для потока. Несколько потоков могут использовать переменную, но каждый поток будет связывать адрес данных конкретного потока с ключом. Также связывает необязательный деструктор, который будет вызываться при завершении потока через return или pthread_exit или после обработчиков очистки при отмене потока — потоки обычно используют malloc для данных, специфичных для потока, и освобождают в деструкторе. Мы можем разорвать связь ключа со специфическими для потока данными, вызвав pthread_key_delete, который не вызывает деструктор. Чтобы инициализировать ключ, используйте pthread_once, чтобы убедиться, что мы установили init только один раз. После создания его можно получить и установить с помощью pthread_(get/set). Errno — это пример локальной переменной потока.

Локальное хранилище потоков

Некоторые операционные системы предоставляют ключевое слово __thread для обозначения локальных или статических переменных как специфичных для потока, например: static __thread buf[10].

Отмена

Атрибуты потока состояние отмены и тип возможности отмены изменяют ответ потока на вызов pthread_cancel. Состояние либо _ENABLE, либо _DISABLE, может измениться вызовом _setcancelstate. Вызов pthread_cancel не ждет завершения потока, он продолжит выполнение до тех пор, пока не достигнет точки отмены — определенных функций lib или _testcancel. Отключение состояния отмены приостанавливает любые полученные отмены до тех пор, пока состояние снова не будет включено. Тип отмены по умолчанию — отложенный, который ожидает точки отмены, также может быть установлен на _ASYNCHRONOUS, который не будет ждать отмены точки отмены.

Потоки и сигналы

Каждый поток имеет собственную маску сигнала, но имеет общее расположение — отдельные потоки могут блокировать сигналы, но все совместно выполняют действия в ответ. Сигналы доставляются отдельным потокам — если действия одного потока вызывают аппаратный сбой, сигнал доставляется этому сигналу, а другой сигнал отправляется произвольному потоку. Потоки используют pthread_sigmask для блокировки сигналов или могут вызывать sigwait, чтобы заставить поток ждать определенного сигнала, должны блокировать сигнал перед ожиданием его. Чтобы отправить сигнал потоку, мы используем pthread_kill.

Потоки и вилки

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

Потоки и ввод-вывод

Может использовать pread/pwrite в потоках, чтобы разрешить доступ к ресурсам ввода/вывода.