std :: atomic_bool для флага отмены: является ли std :: memory_order_relaxed правильным порядком памяти? - PullRequest
0 голосов
/ 06 декабря 2018

У меня есть поток, который читает из сокета и генерирует данные.После каждой операции поток проверяет флаг std::atomic_bool, чтобы увидеть, должен ли он выйти досрочно.

Чтобы отменить операцию, я устанавливаю флаг отмены на true, затем вызываю join() для объекта рабочего потока.

Код потока и функции отмены выглядит примерно так:

std::thread work_thread;
std::atomic_bool cancel_requested{false};

void thread_func()
{
   while(! cancel_requested.load(std::memory_order_relaxed))
      process_next_element();

}

void cancel()
{
    cancel_requested.store(true, std::memory_order_relaxed);
    work_thread.join();
}

Является ли std::memory_order_relaxed правильным порядком памяти для этого использования атомарной переменной?

Ответы [ 2 ]

0 голосов
/ 06 декабря 2018

Пока нет никакой зависимости между cancel_requested флагом и чем-либо еще , вы должны быть в безопасности.

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

Это означает, что выкод на самом деле выглядит следующим образом:

std::thread work_thread;
std::atomic_bool cancel_requested{false};
std::mutex work_queue_mutex;
std::condition_variable work_queue_filled_cond;
std::queue work_queue;

void thread_func()
{
    while(! cancel_requested.load(std::memory_order_relaxed))
    {
        std::unique_lock<std::mutex> lock(work_queue_mutex);
        work_queue_filled_cond.wait(lock, []{ return !work_queue.empty(); });
        auto element = work_queue.front();
        work_queue.pop();
        lock.unlock();
        if (element == exit_sentinel)
            break;
        process_next_element(element);
    }
}

void cancel()
{
    std::unique_lock<std::mutex> lock(work_queue_mutex);
    work_queue.push_back(exit_sentinel);
    work_queue_filled_cond.notify_one();
    lock.unlock();
    cancel_requested.store(true, std::memory_order_relaxed);
    work_thread.join();
}

И если мы так далеко, то cancel_requested может стать обычной переменной, код даже станет проще.

std::thread work_thread;
bool cancel_requested = false;
std::mutex work_queue_mutex;
std::condition_variable work_queue_filled_cond;
std::queue work_queue;

void thread_func()
{
    while(true)
    {
        std::unique_lock<std::mutex> lock(work_queue_mutex);
        work_queue_filled_cond.wait(lock, []{ return cancel_requested || !work_queue.empty(); });
        if (cancel_requested)
            break;
        auto element = work_queue.front();
        work_queue.pop();
        lock.unlock();
        process_next_element(element);
    }
}

void cancel()
{
    std::unique_lock<std::mutex> lock(work_queue_mutex);
    cancel_requested = true;
    work_queue_filled_cond.notify_one();
    lock.unlock();
    work_thread.join();
}

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

Примечание std::thread::join() само по себе действует как барьер памяти между двумя потоками.

0 голосов
/ 06 декабря 2018

Правильность этого кода зависит от многих вещей.Больше всего это зависит от того, что именно вы подразумеваете под «правильным».Насколько я могу судить, биты кода, которые вы показываете, не вызывают неопределенного поведения (при условии, что ваши work_thread и cancel_requested на самом деле не инициализируются в порядке, предложенном вашим фрагментом выше, так как вы могли бы получить поток, потенциально читающийнеинициализированное значение атома).Если все, что вам нужно сделать, это изменить значение этого флага и заставить поток в конечном итоге увидеть новое значение в какой-то момент независимо от того, что еще может происходить, тогда достаточно std::memory_order_relaxed.

Однако яубедитесь, что ваш рабочий поток вызывает функцию process_next_element().Это говорит о том, что существует некоторый механизм, посредством которого рабочий поток получает элементы для обработки.Я не вижу выхода из потока, когда все элементы были обработаны.Что делает process_next_element(), когда рядом нет следующего доступного элемента?Он просто сразу возвращается?В этом случае вы ждете больше ввода или отмены, что сработает, но, вероятно, не идеально.Или process_next_element() внутренне вызывает некоторую функцию, которая блокирует, пока элемент не станет доступным !?Если это так, то для отмены потока потребуется сначала установить флаг отмены, а затем сделать все необходимое, чтобы убедиться, что следующий вызов элемента ваш поток потенциально блокирует при возврате.В этом случае потенциально важно, чтобы поток никогда не видел флаг отмены после возврата блокирующего вызова.В противном случае вы могли бы получить возврат вызова, вернуться в цикл, все еще прочитать старый флаг отмены и затем снова вызвать process_next_element().Если process_next_element() гарантированно просто вернется снова, то все в порядке.Если это не так, у вас тупик.Поэтому я считаю, что технически это зависит от того, что именно делает process_next_element().Один может представить себе реализацию process_next_element(), где вам потенциально потребуется больше, чем просто порядок памяти.Однако, если у вас уже есть механизм извлечения новых элементов для обработки, зачем вообще использовать отдельный флаг отмены?Вы можете просто обработать отмену с помощью того же механизма, например, заставить его вернуть следующий элемент со специальным значением или вообще не возвращать элемент, чтобы сигнализировать об отмене обработки и заставить поток вернуться вместо использования отдельного флага ...

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