С. Мейерс - Эффективный и современный C++ (1114942), страница 65
Текст из файла (страница 65)
Какой в этом случае способ межпоточного сообщения является наилучшим?Очевидный подход заключается в применении переменной условия. Если назвать задачу, которая обнаруживает условие, задачей обнаружения, а задачу, которая на него реагирует, задачей реакции, то выразить стратегию просто: задача реакции ожидает переменную условия, а поток задачи обнаружения выполняет ее уведомление при наступлении события. При-std : : condit ion variaЫe cv; / / Переменная условия событияstd: :mutex m;11 Мьютекс для использования с cvкод задачи обнаружения прост настолько, насколько это возможно:cv . notify_one ( ) ;11 Обнаружение события11 Уведомление задачи реакцииЕсли требуется уведомить несколько задач реакции, можно заменить not i f у_oneна not i f у_а 1 1 , но пока что будем считать, что у нас только одна задача реакции.Код задачи реакции немного сложнее, поскольку перед вызовом wa i t для переменнойусловия он должен блокировать мьютекс с помощью объекта std : : unique l ock.
(Блокировка мьютекса перед ожиданием переменной условия типична для многопоточныхбиблиотек. Необходимость блокировки мьютекса с помощью объекта std : : unique_ lockявляется частью API С++ 1 1 .) Вот как выглядит концептуальный подход:_1111std: : unique lock<s td: : mutex>lk (m) ;11cv . wait (lk) ;1111111111Подготовка к реакцииОткрытие критического разделаБлокировка мьютексаОжидание уведомления;неверно !Реакция н а событие(m заблокирован)Закрытие критического раздела ;7.5. П рименяйте ф ьючерсы void дnя однора э овых сообщений о событиях26511111111разблокирование m спомощью деструктора l kПродолжение реакции(m разблокирован)Первой проблемой при таком подходе является то, что часто называют кодом с душком (code smell): даже если команда работает, что-то выглядит не совсем верным. В нашемслучае запах исходит от необходимости применения мьютексов. Мьютексы используютсядля управления доступом к совместно используемым данным, но вполне возможно, чтодля задач обнаружения и реакции такой посредник не требуется.
Например, задача обнаружения может отвечать за инициализацию глобальной структуры данных, которая затем передается для использования задаче реакции. Если задача обнаружения никогда необращается к структуре данных после ее инициализации и если задача реакции никогдане обращается к ней до того, как задача обнаружения укажет, что структура данных готова к использованию, эти две задачи оказываются не связанными логикой программыодна с другой. При этом нет никакой необходимости в мьютексе.
Тот факт, что подходс использованием переменной условия требует применения мьютексов, оставляет тревожащий запашок подозрительного дизайна.Даже если пропустить этот вопрос, все равно остаются две проблемы, которым, определенно, следует уделить внимание.•Если задача обнаружения уведомляет переменную условия до вызова wai t задаЧтобы уведомление переменной условияактивизировало другую задачу, эта другая задача должна находиться в состоянииожидания переменной условия. Если вдруг задача обнаружения выполняет уведомление до того, как задача реакции выполняет wai t, эта задача реакции пропуституведомление и будет ждать его вечно.чей реакции, задача реакции "зависнет''.•Вызов wait приводит к ложным пробуждениям. В потоковых API (во многих языках программирования, не только в С++) не редкость ситуация, когда код, ожидающий переменную условия, может быть пробужден, даже если переменная условияне была уведомлена.
Такое пробуждение называется ложным пробуждением (spuriouswakeup). Корректный код обрабатывает такую ситуацию, проверяя, что ожидаемоеусловие в действительности выполнено, и это делается первым, немедленно послепробуждения. API переменных условия С++ делает это исключительно простым,поскольку допускает применение лямбда-выражений (или иных функциональныхобъектов), которые проверяют условие, переданное в wa it. Таким образом, вызовwai t в задаче реакции может быть записан следующим образом:cv . wait ( l k,[ ] { return Црохэошло.1111собы!1'хе;});Применение этой возможности требует, чтобы задача реакции могла выяснить,истинно ли ожидаемое ею условие. Но в рассматриваемом нами сценарииожидаемым условием является наступление события, за распознавание которого266Глава 7.
Параллельные вычисленияотве'lает поток обнаружения. Поток реакции может быть не в состоянии определить,имело ли место ожидаемое событие. Вот почему он ожидает переменную условия!Имеется много ситуаций, когда сообщение между задачами с помощью переменнойусловия вполне решает стоящую перед программистом проблему, но не похоже, что перед нами одна из них.Многие разработчики используют совместно используемый булев флаг. Изначальноэтот флаг имеет значение false. Когда поток обнаружения распознает ожидаемое событие, он устанавливает этот флаг:std : : atomic<bool> flag ( fa l se ) ; / /1111flagtrue;11=Совместно используемый флаг ;std : : atomic см .
в разделе 7 . 6Обнаружение событияСообщение задаче обнаруженияСо своей стороны поток реакции просто опрашивает флаг. Когда он видит, что флагустановлен, он знает, что ожидаемое событие произошло:while ( ! flag) ;/ / Подготовка к реакции11 Ожидание события11 Реакция на событиеЭтот подход не страдает ни одним из недостатков проекта на основе переменной условия. Нет необходимости в мьютексе, не проблема, если задача обнаружения устанавливает флаг до того, как задача реакции начинает опрос, и нет н ичего подобного ложнымпробуждениям.
Хорошо, просто замечательно.Куда менее замечательно выглядит стоимость опроса в задаче реакции. Во время ожидания флага задача, по сути, заблокирована, но продолжает выполняться. А раз так, оназанимает аппаратный поток, который мог бы использоваться другой зада•1ей, требуетстоимости переключения контекста при каждом начале и завершении выделенных потоку временных промежутков и заставляет работать ядро, которое в противном случаемогло бы быть отключено для экономии энергии. При настоящей блокировке задача неделает ничего из перечисленного. Это является преимуществом подхода на основе переменных условия, поскольку блокировка задачи при вызове wait является истинной.Распространено сочетание подходов на основе переменных условия и флагов.
Флагуказывает, произошло ли интересующее нас событие, но доступ к флагу синхронизированмьютексом. Поскольку мьютекс предотвращает параллельный доступ к флагу, не требуется,как поясняется в разделе 7.6, чтобы флаг был объявлен как std : : atomic; вполне достаточно простого bool. Задача обнаружения в этом случае может имеет следующий вид:std : : coпdition variaЫe cv;std : : mutex m;Ьоо l flag ( fa l se ) ;11 Как и ранее11 Не std : : atomic11 Обнаружение событияstd: : lock guard<std : : mutex> g (m) ; 11 Блокировка m_11 конструктором g7.5. П рименяйте фьючерсы void для одноразовых сообщений о событиях267flagtrue;cv . notify_one ( ) ;111111111111Сообщаем задаче реакции( часть 1 )Снятие блокировки mдеструктором gСообщаем задаче реакции( часть 2 )А вот задача реакции:1 1 Подготовка к реакции/ / Как и ранееstd: : unique_lock<std : : mutex> lk (m) ; / / Как и ранееcv .
wait(lk , [ ] ( return flag; } ) ;/ / Применение лямбда/ / выражения во избежание1 1 ложных пробуждений/ / Реакция на событие1 1 (m заблокирован)11 Продолжение реа кции1 1 (m разблокирован)Этот подход позволяет избежать проблем, которые мы обсуждали. Он работает независимо от того, вызывает ли задача реакции wai t до уведомления задачей обнаружения,он работает при наличии ложных пробуждений и не требует опроса флага. Тем не менее душок остается, потому что задача обнаружения взаимодействует с задачей реакцииочень любопытным способом. Уведомляемая переменная условия говорит задаче реакции о том, что, вероятно, произошло ожидаемое событие, но задаче реакции необходимопроверить флаг, чтобы быть в этом уверенной.
Установка флага говорит задаче реакции,что событие, определенно, произошло, но задача обнаружения по-прежнему обязана уведомить переменную условия о том, чтобы задача реакции активизировалась и проверилафлаг. Этот подход работает, но не кажется очень чистым.Альтернативный вариант заключается в том, чтобы избежать переменных условия,мьютексов и флагов с помощью вызова wai t задачей реакции для фьючерса, установленного задачей обнаружения. Это может показаться странной идеей.
В конце концов,в разделе 7.4 поясняется, что фьючерс представляет принимающий конец канала связиот вызываемой функции к (обычно асинхронной) вызывающей функции, а между задачами обнаружения и реакции нет отношений "вызываемая-вызывающая': Однако в разделе 7.4 также отмечается, что канал связи, передающий конец которого представляетсобой s t d : : promise, а принимающий - фьючерс, может использоваться для большего,чем простой обмен информацией между вызываемой и вызывающей функциями. Такойканал связи может быть использован в любой ситуации, в которой необходима передачаинформации из одного места вашей программы в другое.
В нашем случае мы воспользуемся им для передачи информации от задачи обнаружения задаче реакции, и информация, которую мы будем передавать, - о том, что произошло интересующее нас событие.268Глава 7. Параллельные вычисленияПроект прост. Задача обнаружения имеет объект s t d : : promise (т.е. передающийконец канала связи), а задача реакции имеет соответствующий фьючерс. Когда задачаобнаружения видит, что произошло ожидаемое событие, она устанавливает объектs t d : : promise (т.е. выполняет запись в канал связи).
Тем временем задача реакции выполняет вызов wait своего фьючерса. Этот вызов wait блокирует задачу реакции до техпор, пока не будет установлен объект std : : promise.И std : : promise, и фьючерсы (т.е. std : : future и std : : shared_ future ) являются шаблонами, требующими параметр типа. Этот параметр указывает тип данных, передаваемый по каналу связи. Однако в нашем случае никакие данные не передаются. Единственное, что представляет интерес для задачи реакции, - что ее фьючерс установлен.