Потоки, блокировки и условные переменные в C++11 [Часть 1]
В C++11, работа с потокам осуществляется по средствам класса std::thread (доступного из заголовочного файла <thread> ), который может работать с регулярными функциями, лямбдами и функторами. Кроме того, он позволяет вам передавать любое число параметров в функцию потока.
В этом примере, thr — это объект, представляющий поток, в котором будет выполняться функция threadFunction() . Вызов join блокирует вызывающий поток (в нашем случае — поток main) до тех пор, пока thr (а точнее threadFunction() ) не выполнит свою работу. Если функция потока возвращает значение — оно будет проигнорировано. Однако принять функция может любое количество параметров.
Несмотря на то, что передавать можно любое число параметров, все они были переданы по значению Если в функцию необходимо передать параметры по ссылке, они должны быть обернуты в std::ref или std::cref , как в примере:
Программа напечатает в консоль 2. Если не использовать std::ref , то результатом работы программы будет 1.
Помимо метода join , следует рассмотреть еще один, похожий метод — detach . detach позволяет отсоединить поток от объекта, иными словами, сделать его фоновым. К отсоединенным потокам больше нельзя применять join .
Также следует отметить, что если функция потока кидает исключение, то оно не будет поймано try-catch блоком. Т.е. следующий код не будет работать (точнее работать то будет, но не так как было задумано: без перехвата исключений):
Для передачи исключений между потоками, необходимо ловить их в функции потока и хранить их где-то, чтобы, в дальнейшем, получить к ним доступ.
-
: возвращает id текущего потока : говорит планировщику выполнять другие потоки, может использоваться при активном ожидании : блокирует выполнение текущего потока в течение установленного периода : блокирует выполнение текущего потока, пока не будет достигнут указанный момент времени
-
: обеспечивает базовые функции lock() и unlock() и не блокируемый метод try_lock() : может войти «сам в себя» : в отличие от обычного мьютекса, имеет еще два метода: try_lock_for() и try_lock_until() : это комбинация timed_mutex и recursive_mutex
Программа должна выдавать примерно следующее:
Перед обращением к общим данным, мьютекс должен быть заблокирован методом lock , а после окончания работы с общими данными — разблокирован методом unlock .
Следующий пример показывает простой потокобезопасный контейнер (реализованный на базе std::vector ), имеющий методы add() для добавления одного элемента и addrange() для добавления нескольких элементов. Примечание: и всё же этот контейнер не является полностью потокобезопасным по нескольким причинам, включая использование va_args . Также, метод dump() не должен принадлежать контейнеру, а должен быть автономной функцией. Цель этого примера в том, что показать основные концепции использования мьютексов, а не не сделать полноценный, безошибочный, потокобезопасный контейнер.
При выполнении этой программы произойдет deadlock (взаимоблокировка, т.е. заблокированный поток так и останется ждать). Причиной является то, что контейнер пытается получить мьютекс несколько раз до его освобождения (вызова unlock ), что невозможно. Здесь и выходит на сцену std::recursive_mutex , который позволяет получать тот же мьютекс несколько раз. Максимальное количество получения мьютекса не определено, но если это количество будет достигно, то lock бросит исключение std::system_error. Поэтому, решение проблемы в коде выше (кроме изменения реализации addrange() , чтобы не вызывались lock и unlock ), заключается в замене мьютекса на std::recursive_mutex .
Теперь, результат работы программы будет следующего вида:
-
: когда объект создан, он пытается получить мьютекс (вызывая lock() ), а когда объект уничтожен, он автоматически освобождает мьютекс (вызывая unlock() ) : в отличие от lock_guard , также поддерживает отложенную блокировку, временную блокировку, рекурсивную блокировку и использование условных переменных
Можно поспорить насчет того, что метод dump() должен быть константным, ибо не изменяет состояние контейнера. Попробуйте сделать его таковым и получите ошибку при компиляции:
Мьютекс (не зависимо от формы реализации), должен быть получен и освобожден, а это подразумевает использование не константных методов lock() и unlock() . Таким образом, аргумент lock_guard не может быть константой. Решение этой проблемы заключается в том, чтобы сделать мьютекс mutable , тогда спецификатор const будет игнорироваться и это позволит изменять состояние из константных функций.
- defer_lock типа defer_lock_t : не получать мьютекс
- try_to_lock типа try_to_lock_t : попытаться получить мьютекс без блокировки
- adopt_lock типа adopt_lock_t : предполагается, что у вызывающего потока уже есть мьютекс
-
: блокирует мьютекс, используя алгоритм избегания deadlock'ов (используя lock() , try_lock() и unlock() ) : пытается блокировать мьютексы в порядке, в котором они были указаны
Предположим, что эта функция вызвана из двух разных потоков, из первого потока: элемент удаляется из 1 контейнера и добавляется во 2, из второго потока, наоборот, элемент удаляется из 2 контейнера и добавляется в 1. Это может вызвать deadlock (если контекст потока переключается от одного потока к другому, сразу после первой блокировки).
Для решения этой проблемы можно использовать std::lock , который гарантирует блокировку безопасным (с точки зрения взаимоблокировки) способом: