1. Чому «чекати 1000» — це початок майбутньої драми
Коли ми вперше пишемо код на кшталт «почекати секунду» або «дати операції максимум 2 секунди», рука сама тягнеться до магічних чисел: 1000, 2000, 30. Проблема не в тому, що числа самі собою погані. Проблема в тому, що в такому вигляді вони не несуть сенсу. Це мілісекунди? Секунди? Мікросекунди? І що станеться, якщо хтось прочитає «1000» як секунди? У результаті ви отримаєте програму, яка «трішки підвисає»… на 16 хвилин.
У std::chrono ідея проста: сенс часу має бути закладений у типі, а не лише в голові програміста (яка, як відомо, неідеальна й охоче перекладає відповідальність на «я потім розберуся»).
Порівняйте:
int timeout = 1000; // 1000 чого?
і
#include <chrono>
std::chrono::milliseconds timeout{1000}; // 1000 ms — уже не сплутаєш
Таймаут і дедлайн: те саме обмеження, але «з різних боків»
Зараз буде важливий поворот сюжету. Багато новачків думають, що таймаут і дедлайн — синоніми. Насправді це два способи описати те саме обмеження, але з різною «геометрією».
Таймаут — це скільки можна чекати (інтервал). Дедлайн — це до якого моменту можна чекати (момент). У термінах <chrono> це дуже добре виражається типами:
| Поняття | Сенс простими словами | Тип у <chrono> |
|---|---|---|
| Таймаут (timeout) | «не довше N» | (наприклад, ) |
| Дедлайн (deadline) | «не пізніше моменту T» | (наприклад, ) |
Ключова думка: таймаут зручно передавати як параметр, а дедлайн — зручно зберігати всередині процесу, який складається з кількох кроків.
2. Від таймауту до дедлайну: один рядок, який економить нерви
Майже в будь-якому реальному коді відбувається одна й та сама трансформація: у нас є таймаут, і ми хочемо перетворити його на конкретний момент часу, після якого — усе, час вичерпано.
З steady_clock це виглядає так:
#include <chrono>
int main() {
using clock = std::chrono::steady_clock;
auto timeout = std::chrono::milliseconds{500};
auto deadline = clock::now() + timeout;
(void)deadline;
}
Цей рядок deadline = now() + timeout — як вдалий заголовок до книги: далі, дивлячись на код, ви вже не сперечаєтеся, «скільки часу минуло?», а просто перевіряєте: «ми вже перейшли дедлайн чи ні?».
4. Навіщо потрібні wait_for і wait_until
Якщо ви колись зазирали в документацію стандартної бібліотеки, то могли помітити закономірність: операції очікування часто мають дві версії — wait_for і wait_until. Таке трапляється в різних частинах стандартної бібліотеки, наприклад у механізмах очікування результатів і в примітивах синхронізації; навіть у текстах змін до стандарту трапляються згадки wait_for/wait_until.
Логіка цієї пари дуже проста:
- wait_for(
timeout) — «чекай не більше інтервалу».duration - wait_until(
deadline) — «чекай до моменту».time_point
Чому це взагалі зручно? Тому що це два різні контракти, і кожен доречний у своїй ситуації.
Коли ви пишете просту програму й хочете «не зависнути довше секунди», таймаут природний: «секунда» — це інтервал.
Коли процес складається з кількох етапів і ви хочете обмежити загальний час, дедлайн набагато чесніший. Тому що таймаут, застосований кілька разів поспіль, може «розтягнути» сумарне очікування.
Розгляньмо це на прикладі простої життєвої математики.
Уявіть: ви робите 3 спроби й на кожну даєте timeout = 500ms. Якщо щоразу викликати «почекати 500ms», то теоретично можна чекати до 1500ms. А якщо ви хочете, щоб «увесь процес має завершитися за 500ms», то мусите працювати з дедлайном: один раз обчислити момент завершення й далі перевіряти саме його.
Саме тому дві форми очікування існують одночасно: одна простіша, інша — загальна й сувора.
5. Мініправило вибору: коли думати «for», а коли «until»
Зараз буде правило рівня «наліпіть на монітор». Воно не математичне, але дуже практичне.
Якщо ви обмежуєте один крок — думайте таймаутом (for).
Якщо ви обмежуєте увесь сценарій — думайте дедлайном (until).
Наприклад, «завантаження одного файлу не довше 2 секунд» — це wait_for(2s) (концептуально). А «вся операція синхронізації не довше 2 секунд, включно з повторами й додатковими перевірками» — це wait_until(deadline).
І так, саме тут добре видно, навіщо для вимірювань зазвичай беруть steady_clock: дедлайн, який «їде назад», швидко перетворюється на комедію. Не завжди смішну.
6. Свій маленький «таймаутний» каркас: рахуємо час, що залишився
Ми поки не вивчаємо справжні примітиви очікування (до них дістанемося пізніше), але вже зараз можемо написати дуже корисні утиліти: «скільки часу залишилося до дедлайну?» і «дедлайн уже настав?».
Це знадобиться навіть у звичайних циклах обробки даних: ви можете припиняти роботу, якщо вийшли за ліміт, і коректно показувати, скільки часу ще залишалося.
Зробімо дві функції для нашого «застосунку дня»: нехай це буде консольна утиліта MiniDeadline, яка зберігає дедлайни операцій і показує їхній статус.
Функція expired(deadline)
Тут ми акуратно використовуємо steady_clock і порівнюємо time_point.
#include <chrono>
bool expired(std::chrono::steady_clock::time_point deadline) {
return std::chrono::steady_clock::now() >= deadline;
}
Зверніть увагу: ми не викликаємо count(), не порівнюємо числа й не пишемо про «магічні секунди». Ми порівнюємо моменти часу.
Функція remaining_ms(deadline)
Тепер зробімо функцію, яка повертає «скільки мілісекунд залишилося» (якщо час уже минув — повертаємо 0). Ми свідомо обираємо milliseconds, бо це зручна одиниця для інтерфейсу й логів.
#include <chrono>
std::chrono::milliseconds remaining_ms(std::chrono::steady_clock::time_point deadline) {
auto now = std::chrono::steady_clock::now();
if (now >= deadline) {
return std::chrono::milliseconds{0};
}
return std::chrono::duration_cast<std::chrono::milliseconds>(deadline - now);
}
Тут є маленька, але важлива деталь: deadline - now — це duration. Ми явно приводимо її до мілісекунд через duration_cast, щоб не залишати двозначності.
7. Збираємо MiniDeadline: «операція + дедлайн + статус»
Зробімо крихітну модель: є «операція» з назвою й дедлайном. Жодних потоків і жодних очікувань — лише часовий контракт і зрозумілий вивід.
Модель даних
#include <chrono>
#include <string>
struct Operation {
std::string name;
std::chrono::steady_clock::time_point deadline;
};
Друк статусу
Зробімо функцію, яка показує, чи строк уже сплив, і скільки часу залишилося (у мілісекундах).
#include <chrono>
#include <iostream>
void print_status(const Operation& op) {
if (expired(op.deadline)) {
std::cout << op.name << ": час вийшов\n"; // наприклад: Download: час вийшов
return;
}
auto left = remaining_ms(op.deadline);
std::cout << op.name << ": залишилося " << left.count() << " ms\n"; // наприклад: Download: залишилося 123 ms
}
Тут left.count() — це вже «безпечне число», бо ми щойно самі нормалізували одиницю до ms і поруч чесно друкуємо ms.
8. Де тут wait_for і wait_until
Дуже доречне запитання — і саме воно добре підводить до цієї лекції.
Уявіть, що замість print_status() у вас є якийсь бібліотечний механізм на кшталт «очікувати подію». Ми поки не реалізуємо очікування, але можемо уявити такий інтерфейс.
Наприклад, ось такий «навчальний обʼєкт», який просто демонструє API:
#include <chrono>
#include <iostream>
struct FakeWaitable {
bool wait_for(std::chrono::milliseconds timeout) {
std::cout << "Імітуємо очікування протягом " << timeout.count() << " ms\n";
return true;
}
bool wait_until(std::chrono::steady_clock::time_point deadline) {
(void)deadline;
std::cout << "Імітуємо очікування до дедлайну\n";
return true;
}
};
Це не «справжня» синхронізація, а лише макет, щоб ви звикли до форми такого API.
А тепер найважливіше: якщо у вас є timeout, то викликати wait_for(timeout) легко. Але якщо всередині циклу ви хочете дотримуватися загального ліміту, то зручніше один раз обчислити дедлайн і викликати wait_until(deadline) або перераховувати «залишок» і викликати wait_for(remaining).
Це підводить нас до базової схеми.
9. Схема із загальним дедлайном
Зараз буде маленька блок-схема, яку корисно навчитися впізнавати з першого погляду. Вона дуже часто трапляється в реальних системах: у мережі, на дисках, у чергах завдань, під час очікування відповідей і повторних спроб.
flowchart TD
A[Старт операції] --> B[timeout задан як duration]
B --> C[deadline = now + timeout]
C --> D{deadline настав?}
D -- да --> E[Зупинитися: timeout/expired]
D -- ні --> F[Зробити крок роботи]
F --> D
Суть у тому, що deadline фіксується один раз. Це захищає від «розтягування» часу, коли ви випадково даєте новий повний таймаут на кожному кроці.
І саме в такій схемі wait_until(deadline) як ідея виглядає природно: «чекаємо, але не пізніше ось цього моменту».
10. Корисні нюанси під час роботи з таймаутами
Не оновлюйте дедлайн у циклі як now() + timeout
Тут розглянемо типову помилку, не перетворюючи лекцію на «страшилки».
Уявіть, що ви робите цикл із кількох кроків і щоразу пишете так:
auto timeout = std::chrono::milliseconds{500};
// десь усередині циклу:
auto local_deadline = std::chrono::steady_clock::now() + timeout;
На перший погляд здається, що ви дотримуєтеся 500ms. Але насправді ви даєте кожному кроку по 500ms. Якщо кроків 10 — вітаю, ви щойно «випадково» подарували собі 5 секунд.
Правильна логіка для загального обмеження інша: дедлайн має бути один і той самий на весь сценарій. Ви або робите deadline = start + timeout один раз, або щоразу обчислюєте «залишок» до дедлайну й використовуєте його як новий маленький таймаут.
І це знову пояснює, чому wait_until існує: він допомагає втримувати загальний ліміт.
«Залишок часу» як таймаут для наступного кроку
Припустімо, у нас є загальний дедлайн, але API, яке ми викликаємо, приймає лише wait_for(duration). Тоді ми адаптуємося: «таймаут на цей крок = скільки часу залишилося».
Ось як може виглядати утиліта, яку зручно мати у проєкті:
#include <chrono>
std::chrono::milliseconds time_left_or_zero(std::chrono::steady_clock::time_point deadline) {
auto now = std::chrono::steady_clock::now();
if (now >= deadline) {
return std::chrono::milliseconds{0};
}
return std::chrono::duration_cast<std::chrono::milliseconds>(deadline - now);
}
Сенс у тому, що тепер цей результат можна передати в будь-який інтерфейс «чекай не більше N», і водночас загальний дедлайн не порушиться. Якщо часу вже немає, ви передасте 0ms, і «очікування» (у нормальних API) завершиться відразу.
Коли дедлайн — це календарний час і потрібен system_clock
Іноді дедлайн — це не «через 500ms», а «не пізніше 12:00». Тоді це вже календарна історія, і ви використовуєте system_clock, бо користувач живе в календарному часі.
Але важливо памʼятати: календарний час може коригуватися (NTP, ручне виправлення годинника), і для вимірювання тривалостей він не годиться. Тому практичне правило таке: таймаути операцій і дедлайни «на час виконання» зазвичай будують на steady_clock, а дедлайни «на певну годину» — на system_clock.
Сьогодні ми не заглиблюємося в цю тему, але сам вибір годинника — це вже частина коректності програми.
11. Типові помилки
Помилка №1: зберігати таймаут як int без одиниці вимірювання.
Такий код швидко перетворюється на «телепатію»: ви дивитеся на timeout = 1000 і намагаєтеся згадати, про що домовлялися тиждень тому. Набагато надійніше зберігати таймаут як std::chrono::milliseconds (або іншу відповідну duration), щоб одиниця була закладена в типі й перевірялася компілятором.
Помилка №2: плутати таймаут і дедлайн у логіці.
Таймаут — це інтервал, дедлайн — момент. Якщо ви зберігаєте дедлайн як число мілісекунд «просто тому, що так простіше», ви легко почнете додавати до нього ще мілісекунди, ніби це таймаут, і в логіці швидко зʼявиться плутанина. Тримайте просте правило: «timeout = duration, deadline = time_point».
Помилка №3: оновлювати дедлайн у циклі як now() + timeout.
Це один із найнеприємніших багів: здається, що ви обмежили час, а насправді обмежили лише один крок — і кожен крок отримує новий ліміт. Виправлення зазвичай просте: обчислити дедлайн один раз до циклу й далі порівнювати тільки з ним.
Помилка №4: порівнювати count() замість порівняння chrono-типів.
Порівняння чисел без одиниці — це той самий «магічний int», тільки в новій упаковці. Якщо ви пишете if (remaining.count() > 2), то виникає запитання: «2 чого?» І де гарантія, що remaining саме в секундах? Краще порівнювати duration із duration (remaining > seconds{2}) або нормалізувати значення в явну одиницю перед друком.
Помилка №5: використовувати system_clock для таймаутів виконання.
Якщо системний час коригується, ваш «дедлайн» може раптово зсунутися, і ви отримаєте дивні ефекти: відʼємні інтервали або «вічні» очікування. Для таймаутів виконання й вимірювань обирайте steady_clock, який монотонний і не «стрибає назад».
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ