Deadlock с использованием std :: mutex для защиты cout в нескольких потоках

Использование cout в нескольких потоках может привести к перемежению вывода.
Поэтому я попытался защитить cout с помощью мьютекса.

Следующий код запускает 10 фоновых потоков с std :: async. Когда начинается поток, он печатает "Начальный поток...". Основной поток выполняет итерацию фьючерсов фоновых потоков в том порядке, в котором они были созданы, и распечатывает "Done thread...", когда соответствующий поток завершен.

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

В чем причина тупика?

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

Почему все нити остались голодными?

Код

#include <future>
#include <iostream>
#include <vector>

using namespace std;
std::mutex mtx;           // mutex for critical section

int print_start(int i) {
   lock_guard<mutex> g(mtx);
   cout << "Started thread" << i << "(" << this_thread::get_id() << ") " << endl;
   return i;
}

int main() {
   vector<future<int>> futures;

   for (int i = 0; i < 10; ++i) {
      futures.push_back(async(print_start, i));
   }

   //retrieve and print the value stored in the future
   for (auto &f : futures) {
      lock_guard<mutex> g(mtx);
      cout << "Done thread" << f.get() << "(" << this_thread::get_id() << ")" << endl;
   }
   cin.get();
   return 0;
}

Выход

Started thread0(352)
Started thread1(14944)
Started thread2(6404)
Started thread3(16884)
Done thread0(16024)
Done thread1(16024)
Done thread2(16024)
Done thread3(16024)

Ответы

Ответ 1

Ваша проблема заключается в использовании future::get:

Возвращает значение, сохраненное в общем состоянии (или генерирует его исключение), когда общее состояние готово.

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

http://www.cplusplus.com/reference/future/future/get/

Поэтому, если поток за будущим еще не успел запустить, функция блокирует, пока этот поток не завершится. Тем не менее, вы берете на себя ответственность за мьютексы, прежде чем вызывать future::get, поэтому какая бы ни была ваша нить, вы не сможете достичь мьютекса для себя.

Это должно устранить вашу проблему взаимоблокировки:

int value = f.get();
lock_guard<mutex> g(mtx);
cout << "Done thread" << value << "(" << this_thread::get_id() << ")" << endl;

Ответ 2

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

BTW: Блокировка выходных потоков не очень эффективна, потому что ее можно легко обойти кодом, который вы даже не контролируете. Вместо того, чтобы использовать эти глобальные переменные, дайте поток для кода, который должен что-то выводить (инъекция зависимостей), а затем собирать данные из этого потока потокобезопасным способом. Или используйте библиотеку протоколирования, потому что это, вероятно, то, что вы хотели сделать в любом случае.

Ответ 3

Хорошо, что причина была обнаружена у источника. Однако довольно часто ошибка, как это бывает, может быть не так-то просто найти. И причина может отличаться. К счастью, в случае тупика вы можете использовать отладчик, чтобы исследовать его.

Я скомпилировал и выполнил ваш пример, а затем, после присоединения к нему с gdb (gcc 4.9.2/Linux), есть обратная трассировка (данные о шумной реализации пропущены):

#0  __lll_lock_wait ()
...
#5  0x0000000000403140 in std::lock_guard<std::mutex>::lock_guard (
    this=0x7ffe74903320, __m=...) at /usr/include/c++/4.9/mutex:377
#6  0x0000000000402147 in print_start (i=0) at so_deadlock.cc:9
...
#23 0x0000000000409e69 in ....::_M_complete_async() (this=0xdd4020)
    at /usr/include/c++/4.9/future:1498
#24 0x0000000000402af2 in std::__future_base::_State_baseV2::wait (
    this=0xdd4020) at /usr/include/c++/4.9/future:321
#25 0x0000000000404713 in std::__basic_future<int>::_M_get_result (
    this=0xdd47e0) at /usr/include/c++/4.9/future:621
#26 0x0000000000403c48 in std::future<int>::get (this=0xdd47e0)
    at /usr/include/c++/4.9/future:700
#27 0x000000000040229b in main () at so_deadlock.cc:24

Это объясняется тем, что объясняется в других ответах - код в заблокированном разделе (so_deadlock.cc24) вызывает future :: get(), который, в свою очередь, (заставляя результат) пытается снова получить блокировку.

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