Osy#betaRun it on Osyrin: osyrin.com

Reference / Function

async / await and Task

async Task<T> Name(params) { … return value; } · var t = Name(args); · T v = await t; · T v = await Name(args);

C#'s async and await, with C#'s meaning. A call to a Task-returning method that you do not await where you make it starts a task that runs beside the caller; await joins it. Awaiting the call where you make it is just the call. Inside a durable run every task is journaled, so a run that is resumed does not do finished work again.

stable5 examples compiled by CIfunctionconcurrencydurability

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 both

Everything 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 started

async 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 writewhat 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 twait 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 ownrefused — nothing would ever wait for it (C#'s CS4014, an error here); write await or _ =
t.IsCompleted · IsFaulted · IsCanceled · Status · Exceptionthe 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 writewhywrite instead
async void F() outside a componentnothing can await it or see it failasync 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 limitawait Task.WhenAny(t, Task.Delay(timeout))
Thread, SynchronizationContext, TaskScheduler, AsyncLocal<T>a body never owns a thread, and nothing flows invisibly across a pauseTask.Run, Task.Delay, a SemaphoreSlim, a parameter
await 5a value is not a taskdrop 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#

Related

Wait for several tasks (Task.WhenAll, Task.WhenAny)

Task.WhenAll waits for every task and answers their results in order; Task.WhenAny answers the first task to finish…

Wait in place (Task.Delay)

Wait before carrying on — between checks on a job another system is still running, before trying a call again, or for a…

Stop a task (CancellationToken) and complete one by hand (TaskCompletionSource)

C#'s cancellation, with C#'s meaning. A CancellationTokenSource hands out a Token; cancel the source and every task…

Do the same work for many items at once (Parallel, SemaphoreSlim)

Parallel.For, Parallel.ForEach and Parallel.ForEachAsync run one body per item, each as a task beside the others, and…

function

A function is a top-level unit of work, written like a C# method — a return type, a name, typed parameters, a body. It…

Http.*

Make an outbound HTTP call to a URL you build at runtime — a webhook, a third-party API, a discovered endpoint…

Parallel legs (start several, then wait for them)

Start several pieces of work at once, each with its own compensation, and wait for them together. Writing…

UnitOfWork

Every body accumulates its writes in a unit of work rather than sending them one at a time. UnitOfWork.Commit() makes…