Можете ли вы реализовать таймер без "сна" в нем, используя только стандартные 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
)