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

Автор работы: Пользователь скрыл имя, 17 Июня 2013 в 11:58, реферат

Описание работы

Допустим, вы решили написать параллельную программу, а конкретно - использовать несколько потоков. Как в этом случае нужно поступить? Как запустить потоки, как узнать что поток завершился, и как отследить его выполнение? Средства, появившиеся в стандартной библиотеке с выходом нового стандарта С++11, позволяют относительно просто решить эти задачи. Для решения сложных задач библиотека позволяет построить то, что нужно из простейших кирпичиков.

Файлы: 1 файл

Управление потоками.docx

— 46.24 Кб (Скачать файл)

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

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

 

Взаимоблокировка: проблема и решение 

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

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

     Общая рекомендация, как избежать взаимоблокировок, заключается в том, чтобы всегда захватывать мьютексы в одном и том же порядке, - если мьютекс А всегда захватывается раньше мьютекса В, то взаимоблокировка не возникнет. Иногда это просто, потому что мьютексы служат разным целям, а иногда совсем не просто, например, если каждый мьютекс защищает отдельный объект одного и того же класса. Рассмотрим операцию сравнения двух объектов одного класса. Чтобы сравнению не мешала одновременная модификация, необходимо захватить мьютексы для обоих объектов. Однако, если выбрать какой-то определённый порядок, то легко получить результат, обратный желаемому: стоит двум потокам вызвать функцию сравнения, передав ей одни и те же объекты в разном порядке, как мы получим взаимоблокировку! 

     В стандартной библиотеке есть функция std::lock, которая умеет захватывать сразу два и более мьютексов без риска получить взаимоблокировку. Но и std::lock бессильна в случае, когда мьютексы захватываются порознь, - в таком случае остаётся полагаться только на дисциплину программирования. Существует несколько относительно простых правил, которые позволяют писать свободный от взаимоблокировок код.

 

Дополнительные  рекомендации, как избежать взаимоблокировок  

     Взаимоблокировка может возникать не только при захвате мьютекса. Нарваться на неё можно и тогда, когда есть два потока и никаких мьютексов; достаточно, чтобы каждый поток вызывал join() объекта std::thread, связанного с другим потоком. В этом случае ни один поток не сможет продолжить выполнение, потому что будет ждать завершения другого потока. Такой простой цикл может возникнуть всюду, где один поток ждёт завершения другого для продолжения работы, этот другой поток одновременно ждёт завершения первого. Общая рекомендация во всех случаях одна: не ждите завершения другого потока, если есть малейшая возможность, что он будет дожидаться вас. Остальные советы выглядят так:

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

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Синхронизация параллельных операций

 

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

 

Ожидание события  или иного условия

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

Второй вариант – заставить  ожидающий поток спать между  проверками с помощью функции std::this_thread::sleep_for():

 

bool flag;

std::mutex m;

void wait_for_flag()

{

std::unique_lock<std::mutex> lk(m); // 1

while (!flag)

{

lk.unlock(); // 2 спать 100 мс

std::this_thread::sleep_for(std::chrono::milliseconds(100));

lk.lock(); // 3

}

}

 

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

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

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

 

Ожидание условия  с помощью условных переменных

Стандартная библиотека С++ предоставлет не одну, а две реализации условных переменных: std::condition_variable и std::condition_variable_any. Оба класса объявлены в заголовке <condition_variable>. В обоих случаях для обеспечения синхронизации необходимо взаимодействие с мьютексом; первый класс может работать только с std::mutex, второй – с любым классом, который отвечает минимальным требованиям к «мьютексоподобию», отсюда и суффикс _any. Поскольку класс std::condition_variable_any более общий, то его использование может обойтись дороже с точки зрения объёма потребляемой памяти, производительности и ресурсов операционной системы.

 

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

В стандартной библиотеке С++ одноразовые события моделируются с помощью будущего результата. Если поток должен ждать некого одноразового события, то он каким-то образом получает представляющий его объект-будущее. Затем поток может периодически в течение очень короткого времени ожидать этот объект-будущее, проверяя, произошло событие, а между проверками заниматься другим делом. Можно поступить и иначе – выполнять другую работу до тех пор, пока не наступит момент, когда без наступления ожидаемого события двигаться дальше невозможно, и вот тогда ждать готовности будущего результата. С будущим результатом могут быть ассоциированы какие-то данные. После того, как событие произошло, сбросить объект-будущее в исходное состояние уже невозможно.

В стандартной библиотеке С++ есть две разновидности будущих результатов, реализованных в форме двух шаблонов классов, которые объявлены в заголовке <future>: уникальные будущие результаты (std::future<>) и разделяемые будущие результаты (std::shared_future<>). Эти классы устроены по образцу std::unique_ptr и std::shared_ptr. На одно событие может ссылаться только один экземпляр std::future, но несколько экземпляров std::shared_future. В последнем случае все экземпляры оказываются готовы одновременно и могут обращаться к ассоциированным с событием данным. Именно из-за ассоциированных данных будущие результаты представлены шаблонами, а не обычными классами; точно так же шаблоны std::unique_ptr и std::shared_ptr параметризованы типом ассоциированных данных. Если ассоциированных данных нет, то следует использовать специализации шаблонов std::future<void> и std::shared_future<void>. Хотя будущие результаты используются как механизм межпоточной коммуникации, сами по себе они не обеспечивают синхронизацию доступа. Если несколько потоков обращаются к единственному объекту-будущему, то они должны защитить доступ с помощью мьютекса или какого-либо другого механизма синхронизации. Однако, каждый из нескольких потоков может работать с собственной копией std::shared_future<> безо всякой синхронизации, даже если все они ссылаются на один и тот же асинхронно получаемый результат.

Самое простое одноразовое  событие – это результат вычисления, выполненного в фоновом режиме.


Информация о работе Управление потоками