Async procedures
Async procedures are those that interact with chronos
to cooperatively
suspend and resume their execution depending on the completion of other
async procedures, timers, tasks on other threads or asynchronous I/O scheduled
with the operating system.
Async procedures are marked with the {.async.}
pragma and return a Future
indicating the state of the operation.
The async
pragma
The {.async.}
pragma will transform a procedure (or a method) returning a
Future
into a closure iterator. If there is no return type specified,
Future[void]
is returned.
proc p() {.async.} =
await sleepAsync(100.milliseconds)
echo p().type # prints "Future[system.void]"
await
keyword
The await
keyword operates on Future
instances typically returned from an
async
procedure.
Whenever await
is encountered inside an async procedure, control is given
back to the dispatcher for as many steps as it's necessary for the awaited
future to complete, fail or be cancelled. await
calls the
equivalent of Future.read()
on the completed future to return the
encapsulated value when the operation finishes.
proc p1() {.async.} =
await sleepAsync(1.seconds)
proc p2() {.async.} =
await sleepAsync(1.seconds)
proc p3() {.async.} =
let
fut1 = p1()
fut2 = p2()
# Just by executing the async procs, both resulting futures entered the
# dispatcher queue and their "clocks" started ticking.
await fut1
await fut2
# Only one second passed while awaiting them both, not two.
waitFor p3()
Because `async` procedures are executed concurrently, they are subject to many
of the same risks that typically accompany multithreaded programming.
In particular, if two `async` procedures have access to the same mutable state,
the value before and after `await` might not be the same as the order of execution is not guaranteed!
Raw async procedures
Raw async procedures are those that interact with chronos
via the Future
type but whose body does not go through the async transformation.
Such functions are created by adding raw: true
to the async
parameters:
proc rawAsync(): Future[void] {.async: (raw: true).} =
let fut = newFuture[void]("rawAsync")
fut.complete()
fut
Raw functions must not raise exceptions directly - they are implicitly declared
as raises: []
- instead they should store exceptions in the returned Future
:
proc rawFailure(): Future[void] {.async: (raw: true).} =
let fut = newFuture[void]("rawAsync")
fut.fail((ref ValueError)(msg: "Oh no!"))
fut
Raw procedures can also use checked exceptions:
proc rawAsyncRaises(): Future[void] {.async: (raw: true, raises: [IOError]).} =
let fut = newFuture[void]()
assert not (compiles do: fut.fail((ref ValueError)(msg: "uh-uh")))
fut.fail((ref IOError)(msg: "IO"))
fut
Callbacks and closures
Callback/closure types are declared using the async
annotation as usual:
type MyCallback = proc(): Future[void] {.async.}
proc runCallback(cb: MyCallback) {.async: (raises: []).} =
try:
await cb()
except CatchableError:
discard # handle errors as usual
When calling a callback, it is important to remember that it may raise exceptions that need to be handled.
Checked exceptions can be used to limit the exceptions that a callback can raise:
type MyEasyCallback = proc(): Future[void] {.async: (raises: []).}
proc runCallback(cb: MyEasyCallback) {.async: (raises: [])} =
await cb()