Потоки, блокировки и условные переменные в C++11 [Часть 1]

Потоки, блокировки и условные переменные в 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 , который гарантирует блокировку безопасным (с точки зрения взаимоблокировки) способом:

📎📎📎📎📎📎📎📎📎📎