Структурная конкурентность: области supervised, сроки, отмена
Область supervised { } владеет каждым файбером, порождённым внутри неё:
область не завершится, пока все дети не закончили работу или не были
отменены, а spawn легален только внутри такой области (D50). Эта страница —
про управление временем жизни области: сроки (timeout: / deadline:) и
кооперативную отмену (cancel:) — и про одно правило размещения, в котором
ошибаются с первой попытки.
Про каналы и select — channels. Про освобождение ресурсов
на выходе из области — cleanup cookbook.
Сроки: timeout: / deadline:
Область может нести собственный срок — относительный (timeout: принимает
Duration) или абсолютный (deadline: принимает момент Monotonic):
supervised(timeout: 5.to_seconds()) {
spawn { work() }
}
Когда срок истекает, все дети отменяются, и область падает с типизированной
TimeoutError. Это школа «срок на области» (Kotlin/Swift/Trio), а не
Go/Rust-школа «дедлайн на дескрипторе»: срок привязан к участку программы, а
не к каждому дескрипторному вводу-выводу внутри него.
Где ставится обработчик — снаружи области
Срок принадлежит области, поэтому его истечение — событие выхода из неё:
к моменту, когда летит TimeoutError, область — вместе с любым обработчиком,
установленным внутри, — уже свёрнута. Обработчик ставится вокруг области:
// ✅ WORKING form: handler OUTSIDE supervised(timeout:)
mut timed_out = AtomicBool.new(false)
ro r = with Fail[TimeoutError] = |_e| {
timed_out.store(true)
0
} {
supervised(timeout: 50.to_millis()) {
spawn { 5000.to_millis().sleep() }
}
5 // reached only when the scope finished in time
}
Естественно выглядящая обратная форма — обработчик внутри области — компилируется, но не ловит ничего:
// ❌ NON-WORKING form: handler INSIDE the scope it is supposed to guard.
// Compiles, but on expiry the program dies with:
// nova: unhandled Fail: supervised-timeout: scope deadline exceeded
supervised(timeout: 50.to_millis()) {
with Fail[TimeoutError] = |_e| { println("never reached") } {
spawn { 5000.to_millis().sleep() }
}
}
Оба фрагмента проверены на реальном компиляторе; второй завершается кодом 127
с показанным сообщением. Если запасное значение не нужно, можно не ставить
обработчик вовсе и дать TimeoutError пролететь к вашему вызывающему.
(Флаг mut timed_out — сознательно AtomicBool: обработчик with Fail
выполняется в файбере падающей операции, а не в файбере установки — голый
mut-флаг был бы гонкой данных под M:N, D441.)
Кооперативная отмена: cancel:
Область можно завершить раньше и мимо механизма сроков — через CancelToken:
ro tok = CancelToken.new()
supervised(cancel: tok) {
spawn { 10.to_millis().sleep(); tok.cancel() }
spawn { 5000.to_millis().sleep() }
}
assert(tok.is_cancelled()) // distinguish the outcome after the scope
В отличие от срока, отмена не бросает ничего — ловить нечем, и это
сознательно: tok.cancel() — это штатное раннее завершение, а не отказ.
Область просто сворачивается раньше, и управление идёт на следующую строку.
Чтобы узнать, как завершилась область, спросите токен:
tok.is_cancelled().
cancel: и timeout: сочетаются — побеждает более раннее из двух. Если
первым сработал токен, TimeoutError не поднимается; если срок — поднимается.
Прямые блокирующие операции в теле области (починено 2026-08-07, два честных остатка).
cancel:/timeout:/deadline:теперь корректно будят прямойTime.sleepв теле самой области — область сворачивается в срок. Два узких хвоста открыты и отслеживаются: (1) прерванная прямая операция возвращается без броска, поэтому операторы между ней и концом блока могут успеть выполниться до выхода из области — внешний наблюдатель видит корректное время, но не кладите туда код, который не должен бежать после отмены; (2) прямойChannel.recv()в теле подcancel:всё ещё виснет. Обе беды обходятся одной структурой: отменяемая работа —spawn-детьми, как во фрагменте выше.
Сетевые чтения: всегда под сроком
Каждое сетевое чтение в программе должно жить под supervised(timeout:):
fn fetch_head(addr str) Net Time -> Option[str] {
with Fail[TimeoutError] = |_e| { None } {
mut out = ""
supervised(timeout: 5.to_seconds()) {
consume conn = TcpStream.connect(addr)!!
out = conn.read_text(1024)!!
conn.close()
}
Some(out)
}
}
Это не вопрос стиля. Обыденный сценарий «сервер отдал часть ответа и закрыл
соединение» иначе оставляет голый read() висеть навсегда — и есть
известный открытый дефект: второе чтение после частично полученных данных
не будится даже сроком области (в реестре проекта; корень — в слое libuv на
Windows, осознанно отложено до ближайшего тега). Срок области надёжно
прерывает первое чтение — ровно это и защищает шаблон выше. Пока дефект не
закрыт, не стройте протокольные циклы с повторными чтениями из потока, чей
пир может полузакрыться посреди ответа; предпочитайте одиночные ограниченные
чтения на область, как выше.
См. также
- channels —
select, паттерн «таймаут как ветка»,ChanReader.close_after - cleanup cookbook — exit-таймауты
consume{}при сворачивании области std/src/concurrency/supervised_deadline_test.nv— авторитетные исполняемые примеры всех сочетанийtimeout:/deadline:/cancel:- Спека: D50 (структурные области), D349 (сроки), D441 (семантика файбера
обработчика),
spec/decisions/06-concurrency.md