Подход к использованию std::atomic по сравнению с std::condition_variable относительно приостановки и возобновления std::thread в C++

Это отдельный вопрос, но связанный с предыдущим вопросом, который я задал здесь

Я использую std::thread в своем коде C++ для постоянного опроса некоторых данных и добавления их в буфер. Я использую C++ lambda для запуска потока следующим образом:

StartMyThread() {

    thread_running = true;
    the_thread = std::thread { [this] {
        while(thread_running) {
          GetData();
        }
    }};
}

thread_running — это atomic<bool>, объявленный в заголовке класса. Вот моя функция GetData:

GetData() {
    //Some heavy logic
}

Затем у меня также есть функция StopMyThread, в которой я устанавливаю thread_running в false, чтобы она вышла из цикла while в lambda block.

StopMyThread() {
  thread_running = false;
  the_thread.join();
}

Насколько я понимаю, я могу приостановить и возобновить поток, используя std::condition_variable, как указано здесь в моем предыдущем вопросе.

Но есть ли недостаток, если я просто использую std::atomic<bool> thread_running для выполнения или не выполнения логики в GetData(), как показано ниже?

GetData() {
    if (thread_running == false)
      return;
    //Some heavy logic
}

Сожжет ли это больше циклов ЦП по сравнению с подходом с использованием std::condition_variable, как описано здесь ?


person TheWaterProgrammer    schedule 11.11.2016    source источник
comment
Честно говоря, я не понимаю the_thread.join();, которое стоит сразу после thread_running=false. Я думаю, что установка thread_running на false приводит к выходу из цикла while потока, за которым также следует выход из потока. Чему служит .join в этом случае? Особенно, когда поток использует loop.   -  person Nawaz    schedule 11.11.2016
comment
@Nawaz Вызов join необходим, потому что OP не отсоединил поток. Если он не присоединит мертвый поток обратно к основному потоку, произойдет утечка ресурсов основного потока (как правило, в конечном итоге приводит к сбою приложения).   -  person Mark B    schedule 11.11.2016
comment
@Nawaz the_thread.join() требуется от основного потока, чтобы дождаться остановки рабочего потока, что является выходом из цикла GetData(). Если я не сделаю .join, то столкнусь с неопределенными сбоями приложения. следовательно, всегда хорошо иметь, даже если это не обязательно. Но мой вопрос в другом, как вы видите. Меня не беспокоит .join   -  person TheWaterProgrammer    schedule 11.11.2016
comment
@MarkB: я не знал о таком поведении .join. Я думал, что он только блокирует вызов, пока поток не завершится. Даже cppreference не упоминает то, что вы сказали об утечке ресурсов и сбое!   -  person Nawaz    schedule 11.11.2016
comment
@Nawaz позвольте мне немного вернуться к моему заявлению: это работает таким образом на различных системах Solaris / Linux, с которыми я работал, но Windows может справиться с этим более изящно.   -  person Mark B    schedule 11.11.2016
comment
@Nawaz: Даже cppreference не упоминает о том, что вы сказали Конечно! Это базовое стандартное поведение. Присоединяйтесь к своим темам!   -  person Lightness Races in Orbit    schedule 11.11.2016
comment
При использовании атомарной стратегии ваш поток вообще не останавливается. Он будет сжигать ядро ​​​​ЦП, постоянно проверяя, является ли это логическое значение истинным или ложным, снова и снова, снова и снова на скорости 4 ГГц. Вам нужна переменная condition_variable, без вопросов.   -  person screwnut    schedule 12.11.2016


Ответы (3)


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

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

Как правило, ваш поток ответа пользовательского интерфейса никогда не должен блокироваться в неготовом состоянии от рабочих потоков.

struct worker_thread {
  worker_thread( std::function<void()> t, bool play = true ):
    task(std::move(t)),
    execute(play)
  {
    thread = std::async( std::launch::async, [this]{
      work();
    });
  }
  // move is not safe.  If you need this movable,
  // use unique_ptr<worker_thread>.
  worker_thread(worker_thread&& )=delete;
  ~worker_thread() {
    if (!exit) finalize();
    wait();
  }
  void finalize() {
    auto l = lock();
    exit = true;
    cv.notify_one();
  }
  void pause() {
    auto l = lock();
    execute = false;
  }
  void play() {
    auto l = lock();
    execute = true;
    cv.notify_one();
  }
  void wait() {
    Assert(exit);
    if (thread)
      thread.get();
  }
private:
  void work() {
    while(true) {
      bool done = false;
      {
        auto l = lock();
        cv.wait( l, [&]{
          return exit || execute;
        });
        done = exit; // have lock here
      }
      if (done) break;
      task();
    }
  }
  std::unique_lock<std::mutex> lock() {
     return std::unique_lock<std::mutex>(m);
  }
  std::mutex m;
  std::condition_variable cv;
  bool exit = false;
  bool execute = true;
  std::function<void()> task;
  std::future<void> thread;
};

или что-то в этом роде.

Это владеет нитью. Поток повторно запускает задачу, пока он находится в режиме play(). Если вы pause() в следующий раз, когда task() завершится, рабочий поток остановится. Если вы play() до завершения вызова task(), он не заметит pause().

Единственное ожидание — это уничтожение worker_thread, когда он автоматически информирует рабочий поток о том, что он должен выйти, и ждет его завершения.

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

.finalize() нельзя отменить.

Код не тестировался.

person Yakk - Adam Nevraumont    schedule 11.11.2016
comment
Спасибо за такой яркий ответ. Следовательно, принимая этот ответ. - person TheWaterProgrammer; 12.11.2016

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

person Mark B    schedule 11.11.2016
comment
В моем первоначальном вопросе завершение остановки потока и запуск нового действительно проблема. Я даже мог видеть отставание, когда много раз нажимал кнопку пользовательского интерфейса, чтобы воспроизвести/приостановить рабочий поток. Это основная причина, по которой я выступаю за альтернативы тому, чтобы не останавливать поток. Кроме того, я читал, что для очистки потока и запуска нового требуется, чтобы ОС выделяла ресурсы для потока, что является тяжелым делом. Верно ? - person TheWaterProgrammer; 11.11.2016
comment
Вы видели задержку из-за объединения всех порожденных тем. Порождение одного потока не будет заметно - person user1095108; 11.11.2016
comment
@user1095108 user1095108 Согласен с настольными компьютерами. Мой код также используется на мобильных устройствах и оборудовании с низким энергопотреблением. И на Android-телефоне среднего уровня, если я специально нажимаю кнопку паузы воспроизведения много раз, вызывая запуск и остановку этого потока, то небольшое отставание появляется примерно раз в 15 раз. - person TheWaterProgrammer; 11.11.2016

Решаются две разные проблемы, и это может зависеть от того, что вы на самом деле делаете. Одна проблема: «Я хочу, чтобы мой поток работал, пока я не скажу ему остановиться». Другой, кажется, случай «у меня есть пара производитель/потребитель, и я хочу иметь возможность уведомить потребителя, когда данные будут готовы». Метод thread_running и join хорошо работает для первого из них. Во-вторых, вы можете захотеть использовать мьютекс и условие, потому что вы делаете больше, чем просто используете состояние для запуска работы. Предположим, у вас есть vector<Work>. Вы охраняете это с помощью мьютекса, поэтому условие становится [&work] (){ return !work.empty(); } или чем-то подобным. Когда ожидание возвращается, вы держите мьютекс, чтобы вы могли взять вещи из работы и сделать их. Когда вы закончите, вы возвращаетесь к ожиданию, освобождая мьютекс, чтобы производитель мог добавить что-то в очередь.

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

person Charlie    schedule 11.11.2016
comment
Моя потребность не так сложна, как описанный вами сценарий. Мне нужно только воспроизвести или приостановить поток. Вот и все. Следовательно, я думаю, что только std::atomic переменной thread_running будет достаточно, чтобы выполнить сложную логику в GetData() или не выполнить логику в GetData(). - person TheWaterProgrammer; 11.11.2016
comment
Моя проблема в том, что рука не является проблемой производителя и потребителя. Это просто автономный worker_thread, который нужно часто приостанавливать и возобновлять. Как вы думаете, мне все еще нужно использовать std::condition_variable для достижения этой цели? - person TheWaterProgrammer; 11.11.2016
comment
Вам нужно ждать (), или поток будет без необходимости сжигать циклы процессора - person user1095108; 11.11.2016
comment
Ни один из них не является лучшим выбором для возобновления паузы. Состояние, вероятно, менее ресурсоемкое в целом. Многие реализации мьютекса имеют своего рода очередь ожидания, поэтому, когда вы говорите, подождите, пока это станет правдой, а затем повторно захватите мьютекс, вы на самом деле говорите, что каждый раз, когда кто-то освобождает мьютекс, получает его, проверяет мое состояние и возвращает true, если условие выполнено, в противном случае освободите мьютекс и повторите попытку. Вам нужно убедиться, что каждый раз, когда вы меняете флаг, вы делаете это, удерживая мьютекс, но... - person Charlie; 11.11.2016
comment
@user1095108 user1095108 Это то, что я хотел подтвердить. Если я сжигаю циклы процессора, то да, я буду использовать std::condition_variable и wait(). - person TheWaterProgrammer; 11.11.2016
comment
@Charlie Чарли, я должен сказать, что std::atomic C ++ довольно солидный. Если я использую атомарную переменную для проверки состояния thread_running, мне не нужно защищать ее с помощью std::mutex. Я тестировал эту штуку на osx, ios и android, дико нажимая кнопку воспроизведения/паузы в пользовательском интерфейсе моего приложения. при использовании std::atomic вам не нужна mutex защита. Помимо этого, как вы говорите, condition_variable менее ресурсоемкий. Итак, я буду использовать то же самое для своего кода. спасибо за это разъяснение. - person TheWaterProgrammer; 11.11.2016
comment
@NelsonP Дело не в том, что атомарность будет неправильной или даже в том, что она не будет работать. Проверка работоспособности будет довольно быстрой каждый раз в цикле, но наличие потока, который завершается и нуждается в повторном порождении, по сравнению с потоком, ожидающим уведомления, вероятно, важнее для рассмотрения. while (true) { if (some_atomic_bool) { ... do work ...}} в основном сжигает ядро ​​​​в этом потоке, когда условие ложно. while (true) { some_condition.wait(...); ... do work ...}} в хорошо реализованной системе должен переводить поток в спящий режим и пробуждать его только тогда, когда условие могло измениться. - person Charlie; 11.11.2016
comment
@Чарли Согласен. Спасибо за объяснение. я принял твой ответ - person TheWaterProgrammer; 11.11.2016