Можете ли вы реализовать таймер без "сна" в нем, используя только стандартные c++/c++ 11?

У меня есть следующий код (вручную скопированный):

// Simple stop watch class basically takes "now" as the start time and 
// returns the diff when asked for.
class stop_watch {...}

// global var
std::thread timer_thread;

void start_timer(int timeout_ms)
{
    timer_thread = std::thread([timeout_ms, this](){
        stop_watch sw;
        while (sw.get_elapsed_time() < timeout_ms)
        {
            // Here is the sleep to stop from hammering a CPU
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }

        // Do timeout things here...
        std::cout << "timed out!" << std::endl;
    })
}

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

Я просто хотел сосредоточиться на части "сна". Могу ли я реализовать что-то подобное без сна или есть лучший способ сделать это? - или сон совершенно хорош? - Я считал, что сон, как правило, является признаком плохого дизайна (я читал, что несколько мест)... но я не могу придумать способ реализовать таймер без него :(

Дополнительное примечание. Таймер должен иметь требование быть в состоянии быть остановленным/пробужденным в любое время. Просто добавив это для ясности, потому что, похоже, это влияет на решение. В моем исходном коде (не этом фрагменте) я использовал атомный флаг bool, который может выйти из цикла.

Ответ 1

С++ 11 предоставляет нам std::condition_variable. В вашем таймере вы можете подождать, пока ваше условие не будет выполнено:

// Somewhere else, e.g. in a header:
std::mutex mutex;
bool condition_to_be_met{false};
std::condition_variable cv;

// In your timer:
// ...
std::unique_lock<std::mutex> lock{mutex};
if(!cv.wait_for(lock, std::chrono::milliseconds{timeout_ms}, [this]{return condition_to_be_met;}))
std::cout << "timed out!" << std::endl;

Вы можете найти более подробную информацию здесь: https://en.cppreference.com/w/cpp/thread/condition_variable

Чтобы сигнализировать, что условие выполнено, выполните это в другом потоке:

{
    std::lock_guard<std::mutex> lock{mutex}; // Same instance as above!
    condition_to_be_met = true;
}
cv.notify_one();

Ответ 2

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

Существует std::this_thread::sleep_until который, в зависимости от реализации, возможно, просто вызывает sleep_for после выполнения какой-либо математики в любом случае, но который может использовать правильный таймер, который значительно превосходит по точности и надежности.

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

В любом случае, многократное сном для небольших сумм, как в вашем примере, является плохая идея. Это сгорит много процессорных процессов при ненужных перенастройках и бодрствованиях потоков, а на некоторых системах (в частности, Windows, хотя Windows 10 не так уж плоха в этом отношении), это может добавить значительное количество дрожания и неопределенности. Обратите внимание, что разные версии Windows округляются до гранулярности планировщика по-разному, поэтому, помимо того, что вы не слишком точны, вы даже не имеете последовательного поведения. Округление - это в значительной степени "кто заботится" за одно большое ожидание, но это серьезная проблема для серии небольших ожиданий.

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

Over-sleep - это другая проблема, если вам это нужно, потому что, даже если вы проверите и правильно определите этот случай, как только это произошло, вы ничего не можете с этим поделать (время уже прошло и никогда не возвращается). Но, увы, это просто фундаментальная слабость сна, мало что можно сделать. К счастью, большинство людей могут избавиться от этой проблемы большую часть времени.

Ответ 3

Вы можете заняться оживанием, проверяя время в цикле, пока оно не достигнет того времени, которое вы ожидаете. Это очевидно ужасно, так что не делайте этого. Сон на 10 мс несколько лучше, но определенно плохой дизайн. (У ответа @Damon есть хорошая информация.)


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

Предложение избегать sleep, вероятно, рекомендуется в течение короткого времени против общей картины сна, проверяя, есть ли что-нибудь, а затем снова спать. Вместо этого блокируйте ожидание события без тайм-аута или очень длинного таймаута. (например, GUI должен ждать нажатия клавиши/нажатия, вызывая функцию блокировки, с тайм-аутом, установленным, чтобы разбудить его, когда наступит время автосохранения или что-то в будущем. Далее вам обычно не нужен отдельный поток, спать, но вы можете, если нет смысла вставлять проверки на текущее время.)

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

Если вы знаете, что нечего делать какое-то время, просто спать так долго с одним сном. AFAIK, несколько коротких спящих не улучшит точность времени пробуждения в основных ОС, таких как Windows, Linux или OS X. Вы можете избежать кэша команд, если ваш код просыпался часто, но если это количество задержка - настоящая проблема, вам, вероятно, нужна ОС реального времени и гораздо более сложный подход к синхронизации.

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


Использование функции без sleep в имени, которое блокируется в течение 1 секунды, не лучше, чем использование sleep или this_thread::sleep_for.

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


Если вы хотите использовать чистый ISO С++ 11, то std::this_thread::sleep_for или std::this_thread::sleep_until - ваш лучший std::this_thread::sleep_until. Они определены в стандартном заголовке <thread>.

sleep(3) - это функция POSIX (например, nanosleep), а не часть ISO С++ 11. Если это не проблема для вас, тогда не стесняйтесь использовать ее, если это уместно.


Для портативного высокоточного OS-ассистированного сна для интервала, С++ 11 представил
std::this_thread::sleep_for(const std::chrono::duration<Rep, Period> &sleep_duration) (На странице cppreference есть пример использования этого кода).

Блокирует выполнение текущего потока, по крайней мере, для указанного sleep_duration.

Эта функция может блокироваться дольше, чем sleep_duration из-за планирования или задержки ресурсов.

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


Спать до тех пор, пока часы не достигнут заданного времени (возможно, учитывая изменения/исправления системного времени):

std::this_thread::sleep_until(const std::chrono::time_point<Clock,Duration>& sleep_time)

Блокирует выполнение текущего потока до тех пор, пока не будет достигнут указанный sleep_time.

Используются часы, привязанные к sleep_time, что означает, что корректировки часов учитываются. Таким образом, продолжительность блока может, но может и не быть, меньше или больше, чем sleep_time - Clock::now() во время вызова, в зависимости от направления настройки. Функция также может блокироваться дольше, чем до тех пор, пока не будет достигнуто sleep_time из-за задержек планирования или ресурсов.


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

Но sleep_until должен позволять вам просыпаться, когда системные часы достигают заданного времени, даже если это было сделано путем настройки (NTP или ручная настройка), если они используются с часами, отличными от steady_clock.


Sleep gotchas: поздний/ранний пробуждение

Оговорки о том, что возможно спящий слишком долго, также относятся к sleep и nanosleep, или к любому другому специфичному для ОС сна или тайм-ауту (включая подход с переменными состояния в ответе @Sebastian), конечно. Это неизбежно; операционная система в режиме реального времени может дать вам верхнюю границу этой дополнительной задержки.

Вы уже делаете что-то вроде этого с вашими 10-мичами:

Всегда предполагайте, что sleep или какая-либо другая функция проснулся поздно, и проверьте текущее время вместо того, чтобы использовать мертвые расчёты в любом случае, когда это имеет значение.

Вы не можете построить надежные часы из повторного sleep. например, не создавайте таймер обратного отсчета, который спит в течение 1 секунды, уменьшает и отображает счетчик, затем спит еще секунду. Если это не просто игрушка, и вам не все равно, насколько точно.

Некоторые функции сна, такие как sleep(3) POSIX sleep(3) также могут рано просыпаться по сигналу. Если пробуждение слишком рано - проблема правильности, проверьте время и, если необходимо, вернитесь к сну за рассчитанный интервал. (Или используйте sleep_until)