Programming model: asynchronous tasks
The asynchronous tasks programming model of Coyote is based on the task-based asynchronous
pattern.
Coyote provides a drop in replacement for the native .NET Task
type that works with the async
and await
C# keywords . To avoid confusion we will use the following terminology:
Short Name | Fully Qualified Name |
---|---|
native Task |
System.Threading.Tasks.Task |
controlled Task |
Microsoft.Coyote.Tasks.Task |
A controlled Task
is similar to a native Task
type and can be used alongside the native Task
type to easily invoke external asynchronous APIs from your code. We will discuss this in more detail
below.
The benefit of using the controlled Task is that the Coyote runtime takes control of its execution
and scheduling during systematic testing, enabling coyote test
to explore various interleavings
between controlled Task
objects. In production, a controlled Task
executes efficiently, as it a
simple wrapper over a native Task
, with operations being pass-through (Coyote takes control only
during testing).
Overview
The core of the Coyote asynchronous tasks programming model is the controlled Task
and Task<T>
objects, which model asynchronous operations. They are supported by the async
and await
keywords.
This programming model is fairly simple in most cases:
- For I/O-bound code, you await
an operation which returns a controlled Task
or Task<T>
inside of an async
method.
- For CPU-bound code, you await
an operation which is started on a background thread with the
Task.Run
method.
In more detail, a controlled Task
is a construct used to implement what is known as the promise
model of concurrency. A controlled Task
basically offers you a promise that work will be completed at a later point, letting you
coordinate with this promise using async
and await
. A controlled Task
represents a single
operation which does not return a value. A controlled Task<T>
represents a single operation which
returns a value of type T
. It is important to reason about tasks as abstractions of work happening
asynchronously, and not an abstraction over threading. By default, a controlled Task
executes
(using a Task
) on the current thread and delegates work to the operating system, as appropriate.
Optionally, a controlled Task
can be explicitly requested to run on a separate thread via the
Task.Run
API.
The await
keyword is where the magic happens. Using await
yields control to the caller of the
method that performed await
, allowing your program to be responsive or a service to be elastic,
since it can now perform useful work while a controlled Task
is running on the background. Your
code does not need to rely on callbacks or events to continue execution after the task has been
completed. The C# language does that for you. If you’re using controlled Task<T>
, the await
keyword will additionally unwrap the value returned when the controlled Task
is complete. The
details of how await
works are further explained in the C#
docs.
During testing, using await
allows the Coyote runtime to automatically inject scheduling points
and thoroughly explore asynchronous interleavings to find concurrency bugs.
You can choose to use the Microsoft.Coyote.Tasks.Task
directly in your programs or you can use
the automatic rewriting feature which will rewrite your compiled binaries that use
System.Threading.Tasks.Task
and inject the required Coyote controls so you can run coyote test
on the rewritten binaries.
What happens under the covers
The C# compiler transforms an async
method into a state machine (literally called
IAsyncStateMachine) which keeps track of things like yielding execution when an await
is reached
and resuming execution when a background job has finished.
The controlled Task
type uses a C# 7 feature known as async task types
(see
here) that allows
framework developers to create custom task types that can be used with async
and await
. This is
where the magic happens. In production, controlled Task
enables C# to build a custom asynchronous
state machine that uses regular Task
objects. However, during testing, Coyote uses dependency
injection to supply a custom asynchronous state machine that allows controlling the scheduling of
controlled Task
objects, and thus systematically exploring their interleavings.
How to use asynchronous tasks
We will now show how to write a program using the Coyote asynchronous task programming model. As
mentioned before, the controlled Task
type is a drop-in replacement for the Task
type, and thus
any prior experience writing asynchronous code using async
and await
is useful and relevant. If
you are not already familiar with async
and await
, you can learn more in the C#
docs.
Say that you have the following simple C# program:
// Use the Coyote controlled task type.
using Microsoft.Coyote.Tasks;
using Microsoft.Coyote.Specifications;
public class Program
{
public int Value = 0;
public async Task WriteWithDelayAsync(int value)
{
await Task.Delay(100);
this.Value = value;
}
public async Task RunAsync()
{
Task task1 = WriteWithDelayAsync(3);
Task task2 = WriteWithDelayAsync(5);
await Task.WhenAll(task1, task2);
Specification.Assert(this.Value == 5, "Value is '{0}' instead of 5.", this.Value);
}
}
The above program contains a int Value
that is updated by the WriteWithDelayAsync
method. This
is a C# async
method that asynchronously waits for a controlled Task
to complete after 100
ms
(created via the Task.Delay(100)
call), and then modifies the Value field.
The asynchronous RunAsync
method twice invokes the WriteWithDelayAsync
method by passing the
values 3
and 5
respectively. Each method call returns a controlled Task
object, which can be
awaited using await
. The RunAsync
method first invokes the two asynchronous method calls and
then calls Task.WhenAll(...)
to await
on the completion of both tasks.
Because WriteWithDelayAsync
method awaits a Task.Delay
to complete, it will yield control to
the caller of the method, which is the RunAsync
method. However, the RunAsync
method is not
awaiting immediately upon invoking the WriteWithDelayAsync
method calls. This means that the two
calls can happen asynchronously, and thus the resulting Value can be either 3
or 5
after
Task.WhenAll(...)
completes.
Using Specification.Assert
, Coyote allows you to write assertions that check these kinds of safety
properties. In this case, the assertion will check if the Value is 5
or not, and if not it will
throw an exception, or report an error together with a reproducible trace during testing.
What about System.Collections.Concurrent?
Yes, you can use the .NET thread safe collections to share information across tasks, but not the
BlockingCollection
as this can block and Coyote will not know about that which will lead to
deadlocks during testing. The other thread safe collections do not have uncontrolled
non-determinism, either from Task.Run, or from retry loops, timers or waits.
The caveat is that Coyote has not instrumented the .NET concurrent collections, and so coyote does
not systematically explore thread switching in the middle of these operations, therefore Coyote
will not always find all data race conditions related to concurrent access on these collections.
For example, two tasks calling TryAdd
with the same key, one task will succeed the other will
not, but Coyote will not systematically explore all possible orderings around this operation. You
can help Coyote do better by using the
SchedulingPoint class.
Samples
To try out more samples that show how to use Coyote to systematically test task-based programs see the following: