Summary#
async, await and Task<T> mean what they mean in C#. Mark a method async, give it a Task<T> return, and
its body returns the T. Call it and await the call, and you get the T back — that is an ordinary call.
Call it without await and you get a Task<T>: the method starts running beside you, and you carry on.
await the task later to get its result. That is how two outbound calls overlap instead of waiting for each other:
var rates = FetchRates(day); // starts — and carries on
var stock = FetchStock(sku); // starts too; both are in flight now
var total = await rates + await stock; // wait for bothEverything await waits for is real: the two calls above are in flight at the same time, on the server.
Signature#
async Task<T> Name(params) { … return value; } // the body returns the T
async Task Name(params) { … } // returns nothing
async ValueTask<T> Name(params) { … } // the same as Task<T> here
var t = Name(args); // START it — a Task<T>, running beside you
T v = await t; // join it: its result, or its failure rethrown
T w = await Name(args); // await where you call it: the plain call, nothing startedasync stands with a function's other modifiers in any order, as in C#: public async Task<T> Name(),
async public …, internal async …, public static async …. On a top-level function static is accepted and adds
nothing — a top-level function belongs to no object already.
| you write | what happens |
|---|---|
await Fetch(x) | the call, exactly as without async — nothing runs beside you |
var t = Fetch(x); | Fetch starts and runs beside the caller; t is its Task<T> |
await t | wait for t; answer its result, or rethrow what it threw |
t.Result · t.Wait() · t.GetAwaiter().GetResult() | the same join, written C#'s blocking way — nothing here holds a thread, so it cannot deadlock. As in C#, .Result and .Wait() raise an AggregateException over a failure, where await and GetResult() raise the failure itself |
Task.Run(() => …) · Task.Factory.StartNew(() => …) · Task.Run(() => …, token) | the lambda runs as a task beside you — a block body (() => { …; return x; }) too, in a component as well as on the server |
t.ContinueWith(prev => …) | a task that waits for t (succeeded or not) and then runs the lambda with it |
_ = Fetch(x); | started on purpose with nobody waiting; if it fails, the failure is logged |
Fetch(x); on its own | refused — nothing would ever wait for it (C#'s CS4014, an error here); write await or _ = |
t.IsCompleted · IsFaulted · IsCanceled · Status · Exception | the task's state, read now — journaled in a durable run |
await foreach (var x in Items()) | read an IAsyncEnumerable<T> / stream<T> |
Description#
What does a task run, and where?#
A task started on the server is a leg: the method runs on its own beside the caller, with the arguments the caller
evaluated. The caller does not wait until it says await. A run does not finish while a task it started is still
running — it waits for every one — so nothing you start is ever left running after its request has ended.
Use it for work that waits on someone else: outbound HTTP calls, a model completion, a slow read. Two of those started together take as long as the slower one, not as long as both.
Can a task write data?#
Not to its caller's unit of work. A task runs at the same time as its caller, and the two would race over the same pending changes. So the compiler refuses a started task whose method creates, changes or deletes a row — directly or through a function it calls — and says what to do:
`Save` runs as a task here — it is not awaited where it is called — and it creates a Note. A task runs beside its
caller, so it may not write to the caller's unit of work: the two would race. Await it where it is called
(`await Save(…)`) so its writes join the caller's, or return what should be written and write it after the `await`.A write the compiler cannot see — one behind an interface, or in a package — is refused when the task finishes, with
the same sentence, and nothing it staged is kept. UnitOfWork.Commit() inside a task is refused at once: it would
commit the caller's half-finished work.
Reading is fine. A task reads what its caller can read, through the caller's own permissions.
A task may still write in two ways, each in a unit of work that is not the caller's: call a function marked
[[function-commits-alone|[CommitsAlone]]], and run a block as a declared actor — runas (Robot) { … } — whose
writes are the actor's and are committed as the block ends ([[security-declared-actor#beside]]). The compiler does
not count either as a write of the task's.
What happens if the process dies?#
Inside a durable run (a workflow body, and every function it calls) each task is journaled. When a task finishes,
its result is recorded on its own transaction, straight away. If the run is killed and resumed, a task that had
finished is not run again — its recorded result is the task. A task that had not finished runs again, exactly as a
Workflow.Once step would.
Before a run suspends — a hand-off to the browser, a park on a workflow — it waits for every running task, so what is carried across the suspension is only ever a finished task.
And in the browser?#
A page's methods can be async too, and async void is accepted on a component method — it is C#'s event-handler
case, and a component method bound to a click is exactly that. In the browser a task's call runs where the task is
started and the task is finished from then on: the case C# calls a task that completes synchronously. Every value an
await answers, where a failure surfaces, and what Task.WhenAll and Task.WhenAny answer are the same as on the
server. What the browser does not do is overlap the calls; an outbound call is the server's anyway.
What does a task's state answer?#
t.IsCompleted, t.IsCompletedSuccessfully, t.IsFaulted, t.IsCanceled, t.Status (a TaskStatus, C#'s members
and numbers) and t.Exception (an AggregateException whose message names the failure, or null) answer what the
task is doing at the moment you ask. That is a race — the task may finish a moment later — so inside a durable run the
first reading of each is written to the run's journal, and a run that is resumed reads the same answer back.
And await foreach?#
async IAsyncEnumerable<T> Items() { … yield return x; … } is an async stream — the same thing as a stream<T>
producer — and await foreach (var x in Items()) reads it. A plain foreach is the same loop
([[function-foreach#stream]]). The loop body runs on each item as it arrives, as in C#, and leaving the loop stops
the reading. Only if the body has to wait on something outside the run — the browser, a person, a durable delay — is
the rest of the stream read to its end at that moment, because a run that is put away cannot hold a half-read stream.
What is refused, and what to write instead?#
| you write | why | write instead |
|---|---|---|
async void F() outside a component | nothing can await it or see it fail | async Task F() |
async string F() | an async method returns a task (C#'s CS1983) | async Task<string> F() |
t.Wait(timeout) | a join with a time limit | await Task.WhenAny(t, Task.Delay(timeout)) |
Thread, SynchronizationContext, TaskScheduler, AsyncLocal<T> | a body never owns a thread, and nothing flows invisibly across a pause | Task.Run, Task.Delay, a SemaphoreSlim, a parameter |
await 5 | a value is not a task | drop await |
t.ConfigureAwait(false) is accepted and changes nothing — there is no synchronization context to return to.
t.AsTask() answers t. using System.Threading.Tasks; is accepted and imports nothing: Task is always in scope.
An app that declares its own entity Task keeps it: Task alone means the entity, while Task<T>, async Task and
Task.WhenAll(…) mean the built-in.
Examples#
app Rates {
model "model/**/*.osy";
use Osysharp.Http;
}
async Task<decimal> Rate(string currency) {
var r = Http.Get("https://rates.example/" + currency);
return decimal.Parse(r.Body);
}
async Task<decimal> Basket(decimal eur, decimal usd) {
var euro = Rate("EUR");
var dollar = Rate("USD");
return eur * await euro + usd * await dollar;
}Task<decimal> Cached(decimal known) {
return Task.FromResult(known);
}
async Task<decimal> Either(bool haveIt, decimal known) {
return haveIt ? await Cached(known) : await Rate("EUR");
}entity Quote { [MaxLength(10)] string Currency; decimal Value; }
async Task<decimal> Remember(string currency) {
var rate = Rate(currency); // reads and an outbound call: fine as a task
var value = await rate;
new Quote { Currency = currency, Value = value }; // the write, after the await
return value;
}async Task<int> Scores() {
var total = Task.Run(() => {
var sum = 0;
for (var i = 1; i <= 4; i++) { sum += i * i; }
return sum;
});
var doubled = total.ContinueWith(done => done.Result * 2);
return await doubled;
}async Task Ping(string where) { Http.Get("https://status.example/" + where); }
async Task<string> Report() {
_ = Ping("report"); // fire and forget — a failure is logged, not thrown here
return "sent";
}See also#
- Wait for several tasks (Task.WhenAll, Task.WhenAny) — waiting for several tasks, or for the first
- Stop a task (CancellationToken) and complete one by hand (TaskCompletionSource) — stopping a task with a CancellationToken; TaskCompletionSource
- Do the same work for many items at once (Parallel, SemaphoreSlim) —
Parallel.For/ForEach/ForEachAsync, andSemaphoreSlim - Wait in place (Task.Delay) —
await Task.Delay(…), and a delay as a task - Parallel legs (start several, then wait for them) — parallel child workflows inside a saga, each with its own compensation
- Http.* —
Http.Get/Http.Post, the calls worth overlapping - UnitOfWork — the unit of work a task may not write to