std::condition_variable неправильно просыпается после std::condition_variable::notify_all() из другого потока

Этот код является упрощением реального кода проекта. Основной поток создает рабочий поток и ждет с std::condition_variable действительно запущенного рабочего потока. В приведенном ниже коде std::condition_variable просыпается после того, как current_thread_state становится "ThreadState::Stopping" - это второе уведомление от рабочего потока, то есть основной поток не просыпается после первого уведомления, когда current_thread_state становится "ThreadState::Starting". ". Результат был тупиковым. Почему это происходит? Почему std::condition_variable не просыпается после первого thread_event.notify_all()?

int main()
{
  std::thread thread_var;
  struct ThreadState {
    enum Type { Stopped, Started, Stopping };
  };
  ThreadState::Type current_thread_state = ThreadState::Stopped;
  std::mutex thread_mutex;
  std::condition_variable thread_event;
  while (true) {
    {
      std::unique_lock<std::mutex> lck(thread_mutex);
      thread_var = std::move(std::thread([&]() {
        {
          std::unique_lock<std::mutex> lck(thread_mutex);
          cout << "ThreadFunction() - step 1\n";
          current_thread_state = ThreadState::Started;
        }
        thread_event.notify_all();

        // This code need to disable output to console (simulate some work).
        cout.setstate(std::ios::failbit);
        cout << "ThreadFunction() - step 1 -> step 2\n";
        cout.clear();

        {
          std::unique_lock<std::mutex> lck(thread_mutex);
          cout << "ThreadFunction() - step 2\n";
          current_thread_state = ThreadState::Stopping;
        }
        thread_event.notify_all();
      }));

      while (current_thread_state != ThreadState::Started) {
        thread_event.wait(lck);
      }
    }

    if (thread_var.joinable()) {
      thread_var.join();
      current_thread_state = ThreadState::Stopped;
    }
  }
  return 0;
}

person Soler    schedule 03.08.2015    source источник


Ответы (2)


Как только вы вызываете метод notify_all, ваш основной поток и ваш рабочий поток (после выполнения своей работы) пытаются получить блокировку мьютекса thread_mutex. Если ваша рабочая нагрузка незначительна, как в вашем примере, рабочий поток, скорее всего, получит блокировку перед основным потоком и установит состояние обратно в ThreadState::Stopped до того, как основной поток когда-либо прочитает его. Это приводит к мертвой блокировке.

Попробуйте добавить значительную рабочую нагрузку, например.

std::this_thread::sleep_for( std::chrono::seconds( 1 ) );

в рабочий поток. Теперь мертвые замки гораздо менее вероятны. Конечно, это не решение вашей проблемы. Это просто для иллюстрации проблемы.

person Markus Mayr    schedule 03.08.2015
comment
Да. Ты прав. Если комментарий cout.setstate(std::ios::failbit) в рабочем потоке func, то все в порядке. Но я не совсем понимаю, почему thread_event.wait(lck) не просыпается после первого thread_event.notify_all(), потому что в документации С++ сказано: Операции ожидания атомарно освобождают мьютекс и приостанавливаются выполнение потока. Когда уведомляется переменная условия, поток пробуждается, и мьютекс повторно захватывается. - person Soler; 03.08.2015
comment
Так что самое простое решение в этом случае - изменить while (current_thread_state != ThreadState::Started) на while (current_thread_state == ThreadState::Stopped) - person stefaanv; 03.08.2015
comment
@stefaanv, мы должны найти состояние ThreadState::Started, чтобы гарантированно знать, что рабочий поток запущен. Код в рабочем потоке между первым и вторым вызовом thread_event.notify_all() может выполняться произвольное время или ждать какого-то события. - person Soler; 04.08.2015
comment
@soler: хорошо, я предлагал решение опубликованной проблемы, а не твоего требования. В любом случае, проблема в том, что вам не гарантируется, что вы сможете проверить точное состояние, поэтому вам как-то придется с этим справляться. - person stefaanv; 04.08.2015
comment
@Soler: Что касается вашего вопроса в вашем первом комментарии: да, мьютекс получен, в основном, как если бы вы вызвали для него метод lock. Но между двумя потоками, пытающимися получить блокировку мьютекса, возникает состояние гонки. Неизвестно, какой из потоков получает блокировку. Что касается вашей проблемы, вы можете использовать второй condition_variable или использовать опрос. Но может быть лучшее решение при анализе ваших фактических требований с более высокой точки зрения. - person Markus Mayr; 04.08.2015
comment
@MarkusMayr, спасибо. Ваш ответ и комментарии содержат достаточно информации для полного понимания проблемы. - person Soler; 10.08.2015

У вас есть два мчащихся потока: один дважды записывает значения current_thread_state, другой один раз считывает значение current_thread_state.

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

person Maxim Egorushkin    schedule 03.08.2015