Как сделать однопоточную программу многопоточной

Добавил пользователь Евгений Кузнецов
Обновлено: 04.10.2024

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

Как работает многопоточность

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

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

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

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

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

Можно ли считать threading многопоточным?

В Python используется GIL (Global Interpreter Lock), который однопоточный. Все потоки, которые создаются с помощью threading будут работать внутри потока GIL. В связи с этим они будут обрабатываться только одним ядром. Ни о какой работе одновременно на нескольких физических ядрах процессора не может быть и речи.

А так как threading будет выполняться только на одном ядре процессора, то нету преимущества по скорости, только наоборот — threading замедлит работу.

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

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

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

В чём преимущества тогда модуля Threading по сравнению с Multiprocessing? Рассмотрим их:

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

Подключение библиотеки threading

Threading – это стандартный модуль, который поставляется вместе с интерпретатором. Программисту не нужно устанавливать его, достаточно просто подключить модуль с помощью команды:

Работать с потоками можно, создавая экземпляры класса Thread. Чтобы создать отдельный, поток нужно создать экземпляр класса и применить к нему метод start() . Вот пример:

Здесь мы функцию mydef запустили в отдельном потоке. В качестве аргументов функции передали числа 1 и 2.

threading.Thread()

Эта конструкция позволяет создать новый поток, создав экземпляр класса Thread. Вот как выглядят её аргументы:

Она принимает аргументы:

Рассмотрим их подробнее:

Демоны

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

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

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

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

Методы для работы с потоками

Для создания и управления потоками используются различные методы класса Thread. С их помощью можно легко манипулировать сразу несколькими потоками и определять их поведение.

start()

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

Здесь пока мы не вызвали метод start , функция myfunc не будет запущена.

Этот метод блокирует выполнение потока, который его вызвал, до тех пор пока не завершится поток, метод которого был вызван. То есть если в потоке thread1 был вызван метод потока thread2: thread2.join(), то поток thread1 будет приостановлен до тех пор, пока выполнение thread2 не завершится.

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

У метода join() есть аргумент timeout . По умолчанию он имеет значение None, но программист может передать в него число с плавающей точкой.

Если аргумент имеет значение по умолчанию, то выполнение потока приостанавливается, пока выполняется поток метода.

Если передать в качестве аргумента число, то для метода join() установится время ожидания, когда оно истечёт, поток продолжит свою работу.

Например, thr1.join(100) означает, что будет ожидаться завершение выполнения потока thr1 не более 100 секунд.

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

Здесь мы делаем поток демоническим, чтобы программа не дожидалась окончания выполнения функции. Подключаем модуль time, для того, чтобы сделать задержку в функции на 2.5 секунд. После старта потока, мы приостанавливаем основной поток на 0.125 секунд. Потом выполняем проверку is_alive(). Если выведет True, значит поток не закончил выполнение за 0.125 секунды.

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

Остановка потока

Бывают ситуации, когда требуется остановить поток, который работает в фоне. Допустим у нас поток у которого в функции run бесконечный цикл. В основной программе нам нужно его остановить. Тут самое простое — это создать некую переменную stop:

  • В бесконечном цикле делать постоянно её проверку и если она True, то завершать его.
  • Не использовать функции, которые могут блокировать выполнение на длительное время. Всегда использовать timeout.

Вот пример такой программы:

Здесь используем глобальную переменную stop. Когда нам нужно остановить поток, мы ей присваиваем значение True, а дальше просто ждём его завершения.

Состояние гонки

Состояние гонки или race condition – это ошибка, возникающая при неправильном проектировании многопоточной программы. Она возникает тогда, когда несколько потоков обращаются к одним и тем же данным. Например, переменная хранит число, которое пытаются одновременно изменить потоки thread1 и thread2, что приводит к непредсказуемым результатам или ошибке.

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

deadlock

При использовании Lock возникает серьезная проблема, которая приводит к полной остановки работы программы. Если вызвать метод acquire(), а объект Lock уже заблокирован, то вызвавший acquire() поток будет ждать, пока заблокировавший объект поток не вызовет release().

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

Самоблокировку можно предотвратить, если удалить лишний вызов acquire(), но это не всегда возможно. Самоблокировка может происходить из-за следующий вещей:

  • Возникновение ошибок, когда Lock остаётся заблокированным.
  • Неправильное проектирование программы, когда одна функция вызывается другой функцией, у которой отсутствует блокировка.

В случае возникновения ошибок достаточно воспользоваться конструкцией try-finally или оператором with.

Вот пример с with:

Конструкция try-finally позволяет удалять блокировку даже в случае возникновения ошибок, что позволяет избежать deadblock. Пример:

Конструкция try-finally гарантирует, что код в finally будет исполнен всегда, независимо от ошибок и результатов блока try.

Однако это не работает в случае самоблокировки из-за неправильного проектирования программы. Для этого был создан объект RLock.

RLock

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

Данный код будет работать, но его проблема заключается в том, что при вызове функции both_parts , в ней вызываются функции part1 и part2 . Между вызовами этих функций может получить доступ к данным какой-нибудь другой поток и их поменять. А что делать, если нужно избежать изменения другим потоком?

Чтобы решить проблему, нужно заблокировать lock1 и в both_parts , перепишем её:

Идея проста: внешняя both_parts блокирует поток на время выполнения функций part1 и part1 . Каждая из функций также блокирует поток для суммирования своей части объекта. Однако объект Lock не позволит этого сделать, этот код приведет к полному зависанию программы, потому что для Lock нет разницы, где в потоке был вызван acquire().

RLock блокирует поток только в том случае, если объект заблокирован другим потоком. Используя RLock, поток никогда не сможет заблокировать сам себя.

Также следует помнить, что, хотя и можно вызывать acquire() несколько раз, метод release() нужно вызвать столько же раз. При каждом вызове acquire() уровень рекурсии увеличивается на единицу, соответственно при каждом вызове release() он уменьшается на единицу.

Передача данных с помощью очередей (Queue)

Библиотеке queue содержит все необходимые инструменты для передачи данных между потоками и реализует нужные механизмы блокировки.

Класс Queue реализует очередь FIFO, который работает так: первый элемент, который пошел в очередь, первым и выйдет из неё. Эту очередь можно сравнить с вертикальной полой трубой, в которую сверху бросают элементы.

Queue имеет параметр maxsize, принимающий только целочисленные значения. Он указывает максимальное количество элементов, которое можно поместить в очередь. Когда максимум достигается, добавление в очередь элементов блокируется, пока в ней не освободится место. Если maxsize принимает значение myfunc выполнится через 4 секунды после вызова метода start().

Barrier

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

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

Так выглядят его аргументы:

Рассмотрим пример использования:

Здесь выставляю barrier на 2 вызова wait. То есть, для того, чтобы выполнился код после wait, wait должен быть вызван в 2 потоках. В данном случае функция myfunc сразу запускается в потоке, но она сразу не выведет 'отработал barrier' в консоль, а дождётся когда в основном потоке будет вызван wait тоже.

Event

Event представляет собой простой механизм реализации связи между потоками: один поток даёт сигнал о событии, другие ожидают этого сигнала.

Объект события управляет внутренним флагом, который может быть установлен в True или False с помощью методов set() и clear(). Также есть методы is_set(), которым можно проверить состояние внутреннего флага. С помощью метода wait(timeout=None) можно ждать пока не выставлен флаг в True. Так же при необходимости можно задать время ожидания.

Вот пример:

Возможность управления потоками в Python – это мощный инструмент в разработке больших программ. Для работы с ними используется модуль Threading и библиотека queue в связке с ним.

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

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


Вступление

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

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

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


Много потоков vs. много процессов

Многопоточность, к сожалению, вовсе не гарантирует вам хорошей работы приложения при зависании какого-то одного из потоков. Почему? В общем-то, если говорить о чистой теории, то всё хорошо. В реальности же в каждом приложении есть такие узкие места, где потоки сходятся. И когда потоки работают в общем адресном пространстве, то никто не мешает одному из потоков влезть на "чужую территорию", после чего всё уж точно встанет колом.

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

Что ж, надо сказать, что эту идею вполне можно признать удачной, однако лишь для относительно ограниченного числа приложений. Во-первых, всё-таки это довольно расточительно - создавать и убивать процессы дольше, чем потоки, да и всяческих ресурсов (хотя бы тех же дескрипторов) требуется больше. Во-вторых, создание приложения, работающего одновременно с несколькими процессами, технически сложнее для большинства современных средств программирования, чем создание просто многопоточного приложения. Почему так? Да потому, что, например, в той же Java есть встроенные средства создания многопоточных приложений, а вот средств создания приложений, "расползшихся" на несколько процессов, в языке до сих пор нет. И вряд ли это минус самого языка - всё-таки, многопоточность для подавляющего большинства приложений очень хорошее решение, и, что самое главное, оно поддерживается как языками программирования, так и разработчиками операционных систем, которые, в общем-то, задумывали процесс как единицу приложения. Так что в вопросе, что лучше, многопоточное приложение или же приложение многопроцессное, сложно дать однозначный ответ.

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


Проблема многопоточных приложений

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

Большая проблема, в общем-то, всего одна - одновременный доступ к некоторым ресурсам, которые используются ими в приложении совместно. Решается эта проблема путём введения специального вида объектов, которые управляют взаимодействием потоков. Я говорю об объектах, потому что львиная доля всех многопоточных приложений сегодня пишется на объектно-ориентированных языках программирования, хотя, конечно, смысл объектов от того, что они превратятся в обычные флаги в процедурно-ориентированных языках (том же Си, к примеру) не слишком, скажем прямо, изменится.

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

Первый вариант синхронизации (ну, или не вариант, а разновидность - в принципе, не так уж и принципиально, как тут сказать) - это мьютексы, или, если говорить по-русски, взаимные исключения. Конечно, в рамках борьбы за чистоту русского языка было бы лучше использовать именно второй термин, однако на деле обычно говорят именно о мьютексах. В общем-то, работу мьютекса мы уже разобрали выше. Если в деталях, то мьютекс работает так: как только какой-либо поток добирается в рамках своего выполнения до области действия мьютекса, тот сразу устанавливается в закрытое состояние (его ещё называют неотмеченным), после чего остальным потокам вход в зону действия этого мьютекса становится запрещён. Если поток выходит из мьютекса, то, соответственно, меняется и состояние этого объекта синхронизации, которое становится неотмеченным, то есть, по-русски говоря, открытым для всех остальных потоков. При этом, правда, есть вероятность того, что потоки начнут соревноваться за доступ к ресурсам, а то и вовсе уйдут в дедлок (то есть, станут заниматься ожиданием освобождения ресурсов, ими же самими и занятыми). Но, несмотря на эти неприятности, которые решаются грамотным проектированием многопоточных приложений и применением специальных защитных алгоритмов, мьютексы применяются очень и очень широко в силу своей концептуальной простоты и следующей из неё напрямую простоты реализации. В некоторых источниках мьютекс называется критической секцией. Это терминологическое различие очень важно в программировании под Windows, однако когда вы будете писать многопоточные приложения с использованием Windows API, вы сами сможете узнать, в чём именно заключается различие между данными объектами. Что касается не Windows, а UNIX (сиречь POSIX-систем), то в них мьютексы называются фьютексами, хотя там тоже есть свои тонкости, о которых, опять-таки, лучше разузнать перед тем, как программировать многопоточные приложения на POSIX API.

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


Резюме

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

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

Как известно, Visual Basic (до версии 6.0 включительно) никогда ранее не позволял создавать многопоточные программные компоненты (EXE, ActiveX DLL и OCX). Тут нужно вспомнить, что архитектура COM включает три разные потоковые модели: однопоточную (Single Thread), совместную (Single Threaded Apartment, STA) и свободную (Multi-Threaded Apartment). VB 6.0 позволяет создавать программы первых двух типов. Вариант STA предусматривает псевдомногопоточный режим - несколько потоков действительно работают параллельно, но при этом программный код каждого из них защищен от доступа к нему извне (в частности, потоки не могут использовать общие ресурсы).

В свое время сообщество VB-разработчиков, выражая недовольство многими будущими новшествами этого языка, с большим одобрением отнеслось к известию о том, что с помощью новой версии инструмента можно будет создавать многопоточные программы (см. "В ожидании Visual Studio .NET", "BYTE/Россия" № 1/2001). Однако многие эксперты высказывали более сдержанные оценки по поводу этого новшества. Вот, например, мнение Дана Эпплмана (Dan Appleman), известного разработчика и автора многочисленных книг для VB-программистов: "Многопоточность в VB.NET страшит меня больше, чем все остальные новшества, причем, как и во многих новых технологиях .NET, это объясняется скорее человеческими, нежели технологическими факторами. Я боюсь многопоточности в VB.NET, потому что VB-программисты обычно не обладают опытом проектирования и отладки многопоточных приложений" [1].

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

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

Параллельная обработка в VB6

Конечно, организовать псевдопараллельную обработку данных можно было и с помощью VB6, но возможности эти были весьма ограниченными. Например, мне несколько лет назад понадобилось написать процедуру, которая приостанавливает выполнение программы на указанное число секунд (соответствующий оператор SLEEP в готовом виде присутствовал в Microsoft Basic/DOS). Ее нетрудно реализовать самостоятельно в виде следующей простой подпрограммы:

В ее работоспособности можно легко убедиться, например, с помощью такого кода обработки щелчка кнопки на форме:

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

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

Чтобы увидеть ограниченность поддержки параллельных вычислений в VB6, замените обращение к функции DoEvents на вывод метки:

В этом случае не только не будет срабатывать кнопка Command2, но даже в течение 5 с не будет изменяться содержание метки.

Для проведения еще одного эксперимента добавьте вызов ожидания в код для Command2 (это можно сделать, так как процедура SleepVB реентерабельна):

Для первого знакомства с созданием параллельных потоков создадим Windows-приложение с формой, на которой разместим кнопки ButtonStart и ButtonAbort и напишем следующий код:

Сразу же хотелось бы обратить внимание на три момента. Во-первых, ключевые слова Imports используются для обращения к сокращенным именам классов, описанных здесь пространством имен. Я специально привел еще один вариант применения Imports для описания сокращенного эквивалента длинного названия пространства имен (VB = Microsoft.VisualBasic), который можно применить к тексту программы. В этом случае сразу видно, к какому пространству имен относится объект Timer.

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

Запустите приложение и щелкните кнопку ButtonStart. Запустился процесс ожидания в цикле заданного интервала времени, причем в данном случае (в отличие от примера с VB6) - в независимом потоке. В этом легко убедиться - все визуальные элементы формы являются доступными. Например, нажав кнопку ButtonAbort, можно аварийно завершить процесс с помощью метода Abort (но закрытие формы с помощью системной кнопки Close не прервет выполнение процедуры!). Для наглядности динамики процесса вы можете разместить на форме метку, а в цикл ожидания процедуры SleepVBNET добавить вывод текущего времени:


Рис. 2. Параллельное выполнение потоков в консольном приложении.

Теперь окно появляется почти сразу. Как видим, создавать экземпляры объекта Thread можно двумя способами. Сначала мы применяли первый из них - создали новый объект (поток) Thread1 и работали с ним. Второй вариант - получить объект Thread для выполняемого в данный момент потока с помощью статического метода CurrentThread. Именно таким образом процедура Main сама для себя установила более высокий приоритет, но могла она это сделать и для любого другого потока, например:

Чтобы показать возможности управления запущенным процессом, добавим в конце процедуры Main такие строчки кода:

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

В этом фрагменте мы использовали метод Sleep для приостановки текущего процесса. Заметьте: Sleep является статическим методом и может применяться только к текущему процессу, но не к какому-то экземпляру объекта Thread. Синтаксис языка позволяет написать Thread1.Sleep или Thread.Sleep, но все равно в этом случае используется объект CurrentThread.

Метод Sleep может также использовать аргумент 0. В этом случае текущий поток освободит неиспользованный остаток кванта выделенного для него времени.

Еще один интересный вариант использования Sleep - со значением Timeout.Infinite. В этом случае поток будет приостановлен на неопределенный срок, пока это состояние не будет прервано другим потоком с помощью метода Thread.Interrupt.

Чтобы приостановить внешний поток из другого потока без остановки последнего, нужно использовать вызов метода Thread.Suspend. Тогда продолжить его выполнение можно будет методом Thread.Resume, что мы и сделали в приведенном выше коде.

Немного о синхронизации потоков

Синхронизация потоков - это одна из главных задач при написании многопоточных приложений, и в пространстве System.Threading имеется большой набор средств для ее решения. Но сейчас мы познакомимся только с методом Thread.Join, который позволяет отлеживать окончание выполнение потока. Чтобы увидеть, как он работает, замените последние строки процедуры Main на такой код:

Управление приоритетами процессов

Распределение квантов времени процессора между потоками выполняется с помощью приоритетов, которые задаются в виде свойства Thread.Priority. Для потоков, создаваемых в период выполнения, можно устанавливать пять значений: Highest, AboveNormal, Normal (используется по умолчанию), BelowNormal и Lowest. Чтобы посмотреть, как влияют приоритеты на скорость выполнения потоков, напишем такой код для процедуры Main:

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


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

Теперь перед запуском первого потока установим для него приоритет на один уровень ниже:

Картина резко поменялась: второй поток практически полностью отнял все время у первого (рис. 4).

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


Рис. 4. Поток 2 был запущен позднее, но с более высоким приоритетом.

Заключение

Более подробное описание этой технологии приведено в отдельных главах книг [1] и [2], из которых мне хотелось бы привести несколько цитат (с которыми я полностью согласен) в качестве очень краткого подведения итогов по теме "Многопоточность в .NET".

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

"Вы должны тщательно подходить к проектированию многопоточности и жестко управлять доступом к общим объектам и переменным" [1].

"Не следует рассматривать применение многопоточности как подход по умолчанию" [2].

"Я спросил аудиторию, состоящую из опытных VB-программистов, хотя ли они получить свободную многопоточность будущей версии VB. Практически все подняли руки. Затем я спросил, кто знает, на что он идет при этом. На этот раз руки подняли всего несколько человек, и на их лицах были понимающие улыбки" [1].

"Если вас не устрашили трудности, связанные с проектированием многопоточных приложений, при правильном применении многопоточность способна значительно улучшить быстродействие приложения" [1].

Литература:

Многозадачность и многопоточность

Начнем с такого простого утверждения: 32-разрядные операционные системы Windows поддерживают многозадачные (многопроцессные) и многопоточные режимы обработки данных. Можно обсуждать, насколько хорошо они это делают, но это уже другой вопрос.

Многозадачность - это режим работы, когда компьютер может выполнять несколько задач одновременно, параллельно. Понятно, что если компьютер имеет один процессор, то речь идет о псевдопараллельности, когда ОС по некоторым правилам может выполнять быстрое переключение между различными задачами. Задача - это программа или часть программы (приложения), выполняющая некоторое логическое действие и являющаяся единицей, для которой ОС выделяет ресурсы. Несколько в упрощенном виде можно считать, что в Windows задачей является каждый программный компонент, реализованный в виде отдельного исполняемого модуля (EXE, DLL). Для Windows понятие "задача" имеет тот же смысл, что и "процесс", что, в частности, означает выполнение программного кода строго в отведенном для него адресном пространстве.

Имеется два основных вида многозадачности - совместная (cooperative) и вытесняющая (preemptive). Первый вариант, реализованный в ранних версиях Windows, предусматривает переключение между задачами только в момент обращения активной задачи к ОС (например, для ввода-вывода). При этом каждый поток отвечает за возврат управления ОС. Если же задача забывала делать такую операцию (например, зацикливалась), то довольно часто это приводило к зависанию всего компьютера.

Вытесняющая многозадачность - режим, когда сама ОС отвечает за выдачу каждому потоку причитающегося ему кванта времени (time-slice), по истечении которого она (при наличии запросов от других задач) автоматически прерывает этот поток и принимает решение, что запускать далее. Раньше этот режим так и назывался - "с разделением времени".

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

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

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

Другие статьи из раздела

Демонстрация Chloride Trinergy

Chloride
Демонстрация Chloride Trinergy
Впервые в России компания Chloride Rus провела демонстрацию системы бесперебойного электропитания Chloride Trinergy®, а также ИБП Chloride 80-NET™, NXC и NX для своих партнеров и заказчиков.

RAID-контроллеры Adaptec Series 5Z с безбатарейной защитой кэша

Adaptec by PMC
RAID-контроллеры Adaptec Series 5Z с безбатарейной защитой кэша
Опытные сетевые администраторы знают, что задействование в работе кэш-памяти RAID-контроллера дает серьезные преимущества в производительности …

Трехфазный ИБП Chloride от 200 до 1200 кВт: Trinergy

Chloride
Трехфазный ИБП Chloride от 200 до 1200 кВт: Trinergy
Trinergy — новое решение на рынке ИБП, впервые с динамическим режимом работы, масштабируемостью до 9.6 МВт и КПД до 99%. Уникальное сочетание …

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

Содержание

Описание

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

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

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

  • Упрощение программы в некоторых случаях за счёт использования общего адресного пространства.
  • Меньшие относительно процесса временны́е затраты на создание потока.

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

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

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

Многопотоковое программирование предложено в качестве средства разработки параллельных программ для многопроцессорных систем (систем с разделяемой памятью). При этом реальное разнесение потоков управления на разные процессоры - задача ОС. Фирма SUN Microsystems для поддержки потоков (нитей) управления реализовала легковесные процессы LWP (LightWeight Processes). Диспетчирование LWP - практически не управляемая пользователем процедура. Потоки характеризуются следующими атрибутами:

  • идентификатор потока (уникален в рамках процесса);
  • значение приоритета;
  • сигнальная маска.

Предложены 2 API потокового программирования:

  • фирмы SUN Microsystems (пионер в этом деле);
  • комитета POSIX.1C по стандартизации.

Здесь рассматривается вариант POSIX (Portable Operating System Interface for Unix). Все функции этого варианта имеют в своих именах префикс pthread_ и объявлены в заголовочном файле pthread.h.

Аппаратная реализация

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

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

  • Временная многопоточность(англ.Temporal multithreading ).
  • Одновременная многопоточность (англ.Simultaneous multithreading ).

Типы реализации потоков

Взаимодействие потоков

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

  • Взаимоисключения (mutex, мьютекс) — это объект синхронизации, который устанавливается в особое сигнальное состояние, когда не занят каким-либо потоком. Только один поток владеет этим объектом в любой момент времени, отсюда и название таких объектов (от английского mutually exclusive access — взаимно исключающий доступ) — одновременный доступ к общему ресурсу исключается. После всех необходимых действий мьютекс освобождается, предоставляя другим потокам доступ к общему ресурсу. Объект может поддерживать рекурсивный захват второй раз тем же потоком, увеличивая счётчик, не блокируя поток, и требуя потом многократного освобождения. Такова, например, критическая секция в Win32. Тем не менее, есть и такие реализации, которые не поддерживают такое и приводят к Взаимная блокировка|взаимной блокировке потока при попытке рекурсивного захвата. Например, это FAST_MUTEX в ядре Windows.
  • Семафоры представляют собой доступные ресурсы, которые могут быть приобретены несколькими потоками в одно и то же время, пока пул ресурсов не опустеет. Тогда дополнительные потоки должны ждать, пока требуемое количество ресурсов не будет снова доступно.
  • ERESOURCE. Мьютекс, поддерживающий рекурсивный захват, с семантикой разделяемого или эксклюзивного захвата. Семантика: объект может быть либо свободен, либо захвачен произвольным числом потоков разделяемым образом, либо захвачен всего одним потоком эксклюзивным образом. Любые попытки осуществить захваты, нарушающее это правило, приводят к блокировке потока до тех пор, пока объект не освободится так, чтобы сделать захват разрешённым. Также есть операции вида TryToAcquire — никогда не блокирует поток, либо захватывает, либо (если нужна блокировка) возвращает FALSE, ничего не делая. Используется в ядре Windows, особенно в файловых системах — так, например, любому кем-то открытому дисковому файлу соответствует структура FCB, в которой есть 2 таких объекта для синхронизации доступа к размеру файла. Один из них — paging IO resource — захватывается эксклюзивно только в пути обрезания файла, и гарантирует, что в момент обрезания на файле нет активного ввода-вывода от кэша и от отображения в память.

Создание потока управления

Создает новый поток для функции, заданной параметром func_p. Эта функция имеет аргументом указатель (void *) и возвращает значение того же типа. Реально же в функцию передается аргумент arg_p. Идентификатор нового потока возвращается через tid_p.

Аргумент attr_p указывает на структуру, задающую атрибуты вновь создаваемого потока. Если attr_p=NULL, то используются атрибуты "по умолчанию" (но это плохая практика, т.к. в разных ОС эти значения могут быть различными, хотя декларируется обратное). Одна структура, указываемая attr_p, может использоваться для управления несколькими потоками.

Инициализация атрибутов потока

Инициализирует структуру, указываемую attr_p, значениями "по умолчанию" (при этом распределяется кое-какая память). Атрибуты потока:

  • Область действия конкуренции (scope) [PTHREAD_SCOPE_PROCESS] - определяет связность потока с LWP.
  • Отсоединенность (detachstate) [PTHREAD_CREATE_JOINABLE] - определяет то, может или нет какой-либо другой поток ожидать окончания данного (посредством функции).
  • Адрес динамического стека потока (stackaddr) [NULL].
  • Размер динамического стека потока(stacksize) [1 Mb].
  • Приоритет потока (priority) [наследуется от потока-родителя].
  • Правила и параметры планирования. Неприятно то, что schedpolicy по умолчанию устанавливается в SCHED_OTHER, зависимую от ОС.

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

Область конкуренции

scope может принимать два значения: PTHREAD_SCOPE_PROCESS - для несвязанного потока; PTHREAD_SCOPE_SYSTEM - для связанного потока.

Состояние отсоединенности

detachstate может принимать два значения: PTHREAD_CREATE_DETACHED - для отсоединеного потока; PTHREAD_CREATE_JOINABLE - для присоединенного потока.

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

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

В потоках можно использовать стандартную функцию exit(), однако это ведет к немедленному завершению всех потоков и процесса в целом. Поток завершается вместе с вызовом return() в функции, вызванной pthread_create(). Поток заканчивает свое выполнение также с помощью функции

допустимо в качестве status использовать NULL. Поток может быть завершен другим потоком посредством функции pthread_cancel() (с этой функцией работают pthread_setcanceltype, pthread_setcancelstate и pthread_testcancel).

Ожидание завершения потока

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

Получение идентификатора потока

Передача управления другому потоку

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

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

Посылает сигнал с идентификатором signum в поток, задаваемый идентификатором tid.

Манипулирование сигнальной маской потока

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

  • SIG_BLOCK - добавить сигналы из набора, указываемого set_p, в текущую сигнальную маску, описывающую блокируемые сигналы;
  • SIG_UNBLOCK - удалить сигналы, содержащиеся в наборе, указываемом set_p, из текущей сигнальной маски;
  • SIG_SETMASK - установить сигнальную маску, указываемую set_p, в качестве текущей.

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

Объекты синхронизации потоков управления

  • взамоисключающие блокировки (mutex locks);
  • условные переменные (conditional variables);
  • семафоры (semaphores);
  • барьеры (barriers).

Указанные средства перечислены в порядке ухудшения их эффективности. Заметим, что доступ к атомарным данным (char, int, double) реализуется за один такт процессора, поэтому существуют ситуации (зависящие от логики программы), когда такие данные сами могут выступать в качестве средства синхронизации.

Взамоисключающие блокировки

инициализирует взаимоисключающую блокировку, выделяя необходимую память. Если mattrp=NULL, то создается блокировка с атрибутами "по умолчанию". В настоящее время атрибут один - область действия блокировки, его умолчательное значение - PTHREAD_PROCESS_PRIVATE (а может быть еще PTHREAD_PROCESS_SHARED).

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

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

Функция pthread_mutex_unlock() освобождает захваченную ранее блокировку. Освободить блокировку может только ее владелец.

Функция pthread_mutex_trylock() - неблокирующая версия функции pthread_mutex_lock(). Если на момент обращения к этой функции блокировка уже захвачена, то происходит немедленный возврат из функции со значением EBUSY.

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

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

инициализирует условную переменную, выделяя память.

разрушает условную переменную, освобождая память.

автоматически освобождает взаимоисключающую блокировку, указанную mp, а вызывающий поток блокируется по условной переменной, заданной cvp. Заблокированный поток разблокируется функциями pthread_cond_signal() и pthread_cond_broadcast(). Одной условной переменной могут быть заблокированы несколько потоков.

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

разблокирует ожидающий данную условную переменную поток. Если сигнала по условной переменной ожидают несколько потоков, то будет разблокирован только какой-либо один из них.

разблокирует все потоки, ожидающие данную условную переменную.

Семафоры

Семафор представляет собой целочисленную переменную. Потоки могут наращивать (post) и уменьшать (wait) ее значение на единицу. Если поток пытается уменьшить семафор так, что его значение становится отрицательным, то поток блокируется. Поток будет разблокирован, когда какой-либо другой поток не увеличит значение семафора так, что он станет неотрицательным после уменьшения его первым (заблокированным) потоком.

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

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

инициализирует семафор, указанный аргументом sp, значением value. Если pshared=0, то область действия семафора - только один процесс, иначе - несколько процессов.

увеличивает значение семафора на 1, при этом может быть разблокирован один (из, возможно, нескольких) поток (какой именно не определено).

пытается уменьшить значение семафора на 1. Если при этом значение семафора должно стать отрицательным, то поток блокируется.

неблокирующая версия функции sem_wait().

Барьеры

Барьер используется для синхронизации работы нескольких потоков управления. Барьер характеризуется натуральным числом count, задающим количество синхронизируемых потоков. Поток управления, "подошедший" к барьеру (обратившийся к функции pthread_barrier), блокируется до момента накопления перед этим барьером указанного количества потоков count.

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

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

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

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