Является ли условная переменная .wait_for () эффективным способом выполнения фоновой задачи через определенный интервал? - PullRequest
0 голосов
/ 28 сентября 2018

Мне нужно запускать действие время от времени, пока работает моя программа.В рабочем коде это настраивается по умолчанию 30 минут, но в примере ниже я использовал 5 секунд.Ранее у меня был std::thread, который проверял бы один раз в секунду, проверяя, не пришло ли время выполнить действие ИЛИ, если программа была закрыта.Это позволило мне закрыть программу в любое время, не имея .join() в потоке действия, блокирующего выход моего приложения в ожидании следующей итерации.В любой момент было меньше, чем за секунду до проверки того, должно ли оно закрыться или выполнить действие.

Мне не нравится идея тратить время на проверку каждую секунду активности, которая может происходить только каждые 30 минут.минут, пока программа запущена, поэтому я попытался переключить ее на условную переменную.Я включил небольшой пример моей реализации ниже.Я хочу быть уверен, что использую правильные инструменты для этого.Проблема, с которой я сталкиваюсь в своем коде, заключается в ненужных вызовах лямбда-выражения, которые я объясню ниже.

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

bool asking_thread_to_quit;
std::mutex cv_mutex;
std::condition_variable cv;

void RunThread()
{
    {
        std::lock_guard<std::mutex> lock(cv_mutex);
        asking_thread_to_quit = false;
    }

    std::cout << "Started RunThread." << std::endl;
    while(true)
    {
        {
            std::unique_lock<std::mutex> lock(cv_mutex);
            std::chrono::seconds delay(5);
            if(cv.wait_for(lock, delay, [] { std::cout << "WAKEUP" << std::endl; return asking_thread_to_quit; })) // timed out
            {
                std::cout << "Breaking RunThread Loop." << std::endl;
                break;
            }
        }

        std::cout << "TIMER CODE!" << std::endl;
    }
}

int main(int argc, char *argv[])
{
    std::cout << "Program Started" << std::endl;
    std::thread run_thread(RunThread);

    // This is where the rest of the program would be implemented, but for the sake of this example, simply wait for user input to allow the thread to run in the background:
    char test;
    std::cin >> test;

    {
        std::lock_guard<std::mutex> lock(cv_mutex);
        asking_thread_to_quit = true;
    }
    cv.notify_all();

    std::cout << "Joining RunThread..." << std::endl;
    run_thread.join();
    std::cout << "RunThread Joined." << std::endl;

    return 0;
}

Если вы запустите программу и разрешите пройти одну 5-секундную итерацию, она даст следующеевывод:

Program Started
Started RunThread.
WAKEUP
WAKEUP
TIMER CODE!
WAKEUP
q    <-- I typed this to quit.
Joining RunThread...
WAKEUP
Breaking RunThread Loop.
RunThread Joined.

Вы можете видеть, что он делает следующее:

  1. (WAKEUP) Выполняет проверку перед ожиданием

  2. Подождите пять секунд

  3. (WAKEUP) Выполняет проверку

  4. (TIMER CODE!) Выполняет действие
  5. (WAKEUP) Выполняет проверку еще раз , прежде чем вернуться к ожиданию

Шаг 5 кажется ненужным, поскольку я только что выполнил его несколько секунд назад, но я считаю, что этонеобходимо, поскольку .wait_for() не знает, что я использую его внутри цикла while(true).Это то, с чем я застрял, или есть способ убрать начальную проверку в вызове .wait_for()?Я предполагаю, что нет, поскольку это позволило бы системе .wait_for() что-то, что ей не нужно ждать.Это то, что заставляет меня задуматься, использую ли я правильные языковые функции для начала.Есть ли лучший способ?

Ответ

Ответ, приведенный ниже, подробно описывает другие вопросы, связанные с моим кодом, а также вызвал информативную беседу.Я приму этот ответ, так как он мне больше всего помог, но быстрый ответ на этот вопрос, по-видимому, следующий:

asking_thread_to_quit мог быть установлен в значение true во время TIMER CODE!раздел, требующий еще одной проверки перед повторным ожиданием условной переменной.

1 Ответ

0 голосов
/ 28 сентября 2018

Ваш код имеет несколько проблем с ним.

void RunThread()
{
  asking_thread_to_quit = false;

Это условие гонки.Не синхронизируйте неатомарную переменную общего доступа в двух разных потоках без синхронизации.

  std::cout << "Started RunThread." << std::endl;
  while(true)
  {
    std::unique_lock<std::mutex> lock(cv_mutex);
    std::chrono::seconds delay(5);

Первая using namespace std::literals::chrono_literals;.Затем используйте 5s.

    if(cv.wait_for(lock, delay, [] { std::cout << "WAKEUP" << std::endl; return asking_thread_to_quit; })) // timed out
    {
      std::cout << "Breaking RunThread Loop." << std::endl;
      break;
    }
    else
    {
      std::cout << "TIMER CODE!" << std::endl;
    }

, который TIMER CODE обычно не должен запускаться в пределах блокировки std::mutex, поскольку это означает, что любой отправляющий сообщение блокируется до тех пор, пока код таймера не будет завершен.

  }
}

Наконец, WAKEUP s являются ложными подробностями.Вы могли бы WAKEUP 50 раз за эти 5 секунд;переменные условия не гарантируют ограниченное количество проверок.

asking_thread_to_quit = true;
cv.notify_all();

это снова приводит к состоянию гонки;Ваша программа теперь выполняет неопределенное поведение дважды.

Изменение asking_thread_to_quit на std::atomic<bool> избавит от формального состояния гонки и UB.Однако он позволит вашему коду пропустить запрос на выход и ошибочно выполнить еще 5 секунд ожидания, после чего будет выполнено задание.

Это потому, что может быть вычислено возвращаемое значение вашей лямбды, тогда asking_thread_to_quit=true иnotify_all вычисляет ничего, ожидая переменную условия (таким образом, ничего не просыпается), затем переменная условия блокируется, проходит 5 секунд, она возвращается, возвращая false, затем повторяет цикл while.

Смьютекс, удерживаемый во всех записях в bool, запись не может происходить до тех пор, пока не вернется лямбда, и мы ожидаем состояние с разблокированным мьютексом.Это предотвращает пропуск .notify_all().

Решающим для этого культом груза является всегда охранник все , считывающие и записывающие asking_thread_to_quit с помощью cv_mutex.Затем избегайте удерживать cv_mutex в течение любого промежутка времени, в том числе при обработке включения таймера.

std::unique_lock<std::mutex> lock_cv() {
  return std::unique_lock<std::mutex>(cv_mutex);
}
void RunThread()
{
  {
    auto lock = lock_cv();
    asking_thread_to_quit = false;
  }

  std::cout << "Started RunThread." << std::endl;
  while(true)
  {
    {
      auto lock = lock_cv();
      using namespace std::literals::chrono_literals;
      if(cv.wait_for(lock, 5s, [] { std::cout << "WAKEUP" << std::endl; return asking_thread_to_quit; })) // timed out
      {
        std::cout << "Breaking RunThread Loop." << std::endl;
        break;
      }
    }         
    std::cout << "TIMER CODE!" << std::endl;
  }
}

и в основном:

{
  auto lock = lock_cv();
  asking_thread_to_quit = true;
}
cv.notify_all();

И да, я предназначил для cv.notify_all() быть вне мьютекса.Оно работает;понимание того, что выходит за рамки решения "Cargo-Cult", которое я здесь предоставляю.

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

Паразитные WAKEUP s все еще могут возникать;они будут отображаться как * WAKEUP с, чем вы ожидаете.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...