Training
Module
Implement Asynchronous Tasks - Training
Learn how to implement asynchronous tasks in C# apps using the `async` and `await` keywords and how to run asynchronous tasks in parallel.
This browser is no longer supported.
Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.
Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Asynchronous programming is a mechanism that is essential to modern applications for diverse reasons. There are two primary use cases that most developers will encounter:
Although background work often does involve the utilization of multiple threads, it's important to consider the concepts of asynchrony and multi-threading separately. In fact, they are separate concerns, and one does not imply the other. This article describes the separate concepts in more detail.
The previous point - that asynchrony is independent of the utilization of multiple threads - is worth explaining a bit further. There are three concepts that are sometimes related, but strictly independent of one another:
All three are orthogonal concepts, but can be easily conflated, especially when they are used together. For example, you may need to execute multiple asynchronous computations in parallel. This relationship does not mean that parallelism or asynchrony imply one another.
If you consider the etymology of the word "asynchronous", there are two pieces involved:
When you put these two terms together, you'll see that "asynchronous" means "not at the same time". That's it! There is no implication of concurrency or parallelism in this definition. This is also true in practice.
In practical terms, asynchronous computations in F# are scheduled to execute independently of the main program flow. This independent execution doesn't imply concurrency or parallelism, nor does it imply that a computation always happens in the background. In fact, asynchronous computations can even execute synchronously, depending on the nature of the computation and the environment the computation is executing in.
The main takeaway you should have is that asynchronous computations are independent of the main program flow. Although there are few guarantees about when or how an asynchronous computation executes, there are some approaches to orchestrating and scheduling them. The rest of this article explores core concepts for F# asynchrony and how to use the types, functions, and expressions built into F#.
In F#, asynchronous programming is centered around two core concepts: async computations and tasks.
Async<'T>
type with async { }
expressions, which represents a composable asynchronous computation that can be started to form a task.Task<'T>
type, with task { }
expressions, which represents an executing .NET task.In general, you should consider using task {…}
over async {…}
in new code if you're interoperating with .NET libraries that use tasks, and if you don't rely on asynchronous code tailcalls or implicit cancellation token propagation.
You can see the basic concepts of "async" programming in the following example:
open System
open System.IO
// Perform an asynchronous read of a file using 'async'
let printTotalFileBytesUsingAsync (path: string) =
async {
let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
printTotalFileBytesUsingAsync "path-to-file.txt"
|> Async.RunSynchronously
Console.Read() |> ignore
0
In the example, the printTotalFileBytesUsingAsync
function is of type string -> Async<unit>
. Calling the function does not actually execute the asynchronous computation. Instead, it returns an Async<unit>
that acts as a specification of the work that is to execute asynchronously. It calls Async.AwaitTask
in its body, which converts the result of ReadAllBytesAsync to an appropriate type.
Another important line is the call to Async.RunSynchronously
. This is one of the Async module starting functions that you'll need to call if you want to actually execute an F# asynchronous computation.
This is a fundamental difference with the C#/Visual Basic style of async
programming. In F#, asynchronous computations can be thought of as Cold tasks. They must be explicitly started to actually execute. This has some advantages, as it allows you to combine and sequence asynchronous work much more easily than in C# or Visual Basic.
Here is an example that builds upon the previous one by combining computations:
open System
open System.IO
let printTotalFileBytes path =
async {
let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
argv
|> Seq.map printTotalFileBytes
|> Async.Parallel
|> Async.Ignore
|> Async.RunSynchronously
0
As you can see, the main
function has quite a few more elements. Conceptually, it does the following:
Async<unit>
computations with Seq.map
.Async<'T[]>
that schedules and runs the printTotalFileBytes
computations in parallel when it runs.Async<unit>
that will run the parallel computation and ignore its result (which is a unit[]
).Async.RunSynchronously
, blocking until it completes.When this program runs, printTotalFileBytes
runs in parallel for each command-line argument. Because asynchronous computations execute independently of program flow, there is no defined order in which they print their information and finish executing. The computations will be scheduled in parallel, but their order of execution is not guaranteed.
Because Async<'T>
is a specification of work rather than an already-running task, you can perform more intricate transformations easily. Here is an example that sequences a set of Async computations so they execute one after another.
let printTotalFileBytes path =
async {
let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
argv
|> Seq.map printTotalFileBytes
|> Async.Sequential
|> Async.Ignore
|> Async.RunSynchronously
|> ignore
This will schedule printTotalFileBytes
to execute in the order of the elements of argv
rather than scheduling them in parallel. Because each successive operation will not be scheduled until after the preceding computation has finished executing, the computations are sequenced such that there is no overlap in their execution.
When you write async code in F#, you'll usually interact with a framework that handles scheduling of computations for you. However, this is not always the case, so it is good to understand the various functions that can be used to schedule asynchronous work.
Because F# asynchronous computations are a specification of work rather than a representation of work that is already executing, they must be explicitly started with a starting function. There are many Async starting methods that are helpful in different contexts. The following section describes some of the more common starting functions.
Starts a child computation within an asynchronous computation. This allows multiple asynchronous computations to be executed concurrently. The child computation shares a cancellation token with the parent computation. If the parent computation is canceled, the child computation is also canceled.
Signature:
computation: Async<'T> * ?millisecondsTimeout: int -> Async<Async<'T>>
When to use:
What to watch out for:
Async.StartChild
isn't the same as scheduling them in parallel. If you wish to schedule computations in parallel, use Async.Parallel
.Runs an asynchronous computation, starting immediately on the current operating system thread. This is helpful if you need to update something on the calling thread during the computation. For example, if an asynchronous computation must update a UI (such as updating a progress bar), then Async.StartImmediate
should be used.
Signature:
computation: Async<unit> * ?cancellationToken: CancellationToken -> unit
When to use:
What to watch out for:
Async.StartImmediate
is likely inappropriate to use.Executes a computation in the thread pool. Returns a Task<TResult> that will be completed on the corresponding state once the computation terminates (produces the result, throws exception, or gets canceled). If no cancellation token is provided, then the default cancellation token is used.
Signature:
computation: Async<'T> * ?taskCreationOptions: TaskCreationOptions * ?cancellationToken: CancellationToken -> Task<'T>
When to use:
What to watch out for:
Task
object, which can increase overhead if it is used often.Schedules a sequence of asynchronous computations to be executed in parallel, yielding an array of results in the order they were supplied. The degree of parallelism can be optionally tuned/throttled by specifying the maxDegreeOfParallelism
parameter.
Signature:
computations: seq<Async<'T>> * ?maxDegreeOfParallelism: int -> Async<'T[]>
When to use it:
What to watch out for:
Schedules a sequence of asynchronous computations to be executed in the order that they are passed. The first computation will be executed, then the next, and so on. No computations will be executed in parallel.
Signature:
computations: seq<Async<'T>> -> Async<'T[]>
When to use it:
What to watch out for:
Returns an asynchronous computation that waits for the given Task<TResult> to complete and returns its result as an Async<'T>
Signature:
task: Task<'T> -> Async<'T>
When to use:
What to watch out for:
Creates an asynchronous computation that executes a given Async<'T>
, returning an Async<Choice<'T, exn>>
. If the given Async<'T>
completes successfully, then a Choice1Of2
is returned with the resultant value. If an exception is thrown before it completes, then a Choice2of2
is returned with the raised exception. If it is used on an asynchronous computation that is itself composed of many computations, and one of those computations throws an exception, the encompassing computation will be stopped entirely.
Signature:
computation: Async<'T> -> Async<Choice<'T, exn>>
When to use:
What to watch out for:
Creates an asynchronous computation that runs the given computation but drops its result.
Signature:
computation: Async<'T> -> Async<unit>
When to use:
ignore
function for non-asynchronous code.What to watch out for:
Async.Ignore
because you wish to use Async.Start
or another function that requires Async<unit>
, consider if discarding the result is okay. Avoid discarding results just to fit a type signature.Runs an asynchronous computation and awaits its result on the calling thread. Propagates an exception should the computation yield one. This call is blocking.
Signature:
computation: Async<'T> * ?timeout: int * ?cancellationToken: CancellationToken -> 'T
When to use it:
What to watch out for:
Async.RunSynchronously
blocks the calling thread until the execution completes.Starts an asynchronous computation that returns unit
in the thread pool. Doesn't wait for its completion and/or observe an exception outcome. Nested computations started with Async.Start
are started independently of the parent computation that called them; their lifetime is not tied to any parent computation. If the parent computation is canceled, no child computations are canceled.
Signature:
computation: Async<unit> * ?cancellationToken: CancellationToken -> unit
Use only when:
What to watch out for:
Async.Start
aren't propagated to the caller. The call stack will be completely unwound.printfn
) started with Async.Start
won't cause the effect to happen on the main thread of a program's execution.If using async { }
programming, you may need to interoperate with a .NET library or C# codebase that uses async/await-style asynchronous programming. Because C# and the majority of .NET libraries use the Task<TResult> and Task types as their core abstractions this may change how you write your F# asynchronous code.
One option is to switch to writing .NET tasks directly using task { }
. Alternatively, you can use the Async.AwaitTask
function to await a .NET asynchronous computation:
let getValueFromLibrary param =
async {
let! value = DotNetLibrary.GetValueAsync param |> Async.AwaitTask
return value
}
You can use the Async.StartAsTask
function to pass an asynchronous computation to a .NET caller:
let computationForCaller param =
async {
let! result = getAsyncResult param
return result
} |> Async.StartAsTask
To work with APIs that use Task (that is, .NET async computations that do not return a value), you may need to add an additional function that will convert an Async<'T>
to a Task:
module Async =
// Async<unit> -> Task
let startTaskFromAsyncUnit (comp: Async<unit>) =
Async.StartAsTask comp :> Task
There is already an Async.AwaitTask
that accepts a Task as input. With this and the previously defined startTaskFromAsyncUnit
function, you can start and await Task types from an F# async computation.
In F#, you can write tasks directly using task { }
, for example:
open System
open System.IO
/// Perform an asynchronous read of a file using 'task'
let printTotalFileBytesUsingTasks (path: string) =
task {
let! bytes = File.ReadAllBytesAsync(path)
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
let task = printTotalFileBytesUsingTasks "path-to-file.txt"
task.Wait()
Console.Read() |> ignore
0
In the example, the printTotalFileBytesUsingTasks
function is of type string -> Task<unit>
. Calling the function starts to execute the task.
The call to task.Wait()
waits for the task to complete.
Although threading is mentioned throughout this article, there are two important things to remember:
For example, a computation may actually run on its caller's thread, depending on the nature of the work. A computation could also "jump" between threads, borrowing them for a small amount of time to do useful work in between periods of "waiting" (such as when a network call is in transit).
Although F# provides some abilities to start an asynchronous computation on the current thread (or explicitly not on the current thread), asynchrony generally is not associated with a particular threading strategy.
.NET feedback
.NET is an open source project. Select a link to provide feedback:
Training
Module
Implement Asynchronous Tasks - Training
Learn how to implement asynchronous tasks in C# apps using the `async` and `await` keywords and how to run asynchronous tasks in parallel.