How to use background builders to avoid caller context
Use a background* builder when library or internal async work should not stay tied to the caller's current synchronization context or scheduler.
This is similar in intent to ConfigureAwait(false): the code is saying it does not need to resume on the caller's context.
A UI thread is a single thread that owns an application's visual controls and event loop.
Frameworks like WinForms, WPF, and MAUI generally require UI updates to happen on that thread.
To make await convenient, those frameworks install a SynchronizationContext so async continuations can resume on the UI thread after an await.
Microsoft's ExecutionContext and SynchronizationContext article is a good deeper reference for how this interacts with async/await and ConfigureAwait(false).
That is useful for UI code that needs to update controls after awaiting. It is usually not useful for library code that reads files, calls services, parses data, or performs other work that does not touch UI controls.
ASP.NET Core usually does not have a UI-style SynchronizationContext, so backgroundTask is less commonly needed there. The same idea still applies to any host that installs a custom synchronization context or task scheduler.
Use backgroundTask for context-independent library work
Use task when the work should follow the caller's normal context behavior.
Use backgroundTask when the work does not need that context.
let parseProfile (text: string) = text.Trim()
let loadProfileText () =
backgroundTask {
do! Task.Delay 1
return parseProfile " Ada "
}
See the difference with a fake synchronization context
The example below installs a small SynchronizationContext that records whether an async continuation posted back through it.
task posts back through the context after Task.Yield().
type RecordingSynchronizationContext() =
inherit SynchronizationContext()
let mutable postCount = 0
member _.PostCount = postCount
override _.Post(callback, state) =
postCount <-
postCount
+ 1
callback.Invoke state
let runWithSynchronizationContext (context: SynchronizationContext) (work: unit -> Task<'T>) =
let previous = SynchronizationContext.Current
SynchronizationContext.SetSynchronizationContext context
try
work().GetAwaiter().GetResult()
finally
SynchronizationContext.SetSynchronizationContext previous
let normalTaskPostCount =
let context = RecordingSynchronizationContext()
runWithSynchronizationContext
context
(fun () ->
task {
do! Task.Yield()
return context.PostCount
}
)
backgroundTask escapes to the thread pool when a synchronization context or non-default scheduler is present, so it does not post through that caller context.
If it is already running on the thread pool with the default scheduler, it avoids adding an extra hop.
let backgroundTaskPostCount =
let context = RecordingSynchronizationContext()
runWithSynchronizationContext
context
(fun () ->
backgroundTask {
do! Task.Yield()
return context.PostCount
}
)
Use the matching background shape
Pick the task shape first, then use the background variant only when you want to avoid the caller context.
Normal builder |
Background builder |
Result shape |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
let saveProfileAudit () = backgroundTaskUnit { do! Task.Delay 1 }
let loadProfileLater () =
backgroundColdTask {
let! text = loadProfileText ()
return text
}
let loadProfileForRequest () =
backgroundCancellableTask {
let! cancellationToken = CancellableTask.getCancellationToken ()
do! Task.Delay(1, cancellationToken)
return "Ada"
}
Run the examples
These values make the context behavior visible:
normalTaskPostCountis greater than zero becausetaskposted through the installed context.backgroundTaskPostCountis zero becausebackgroundTaskavoided that context.
let profileText = loadProfileText().GetAwaiter().GetResult()
let auditResult = saveProfileAudit().GetAwaiter().GetResult()
let laterProfile =
let operation = loadProfileLater ()
operation().GetAwaiter().GetResult()
let requestProfile =
(loadProfileForRequest ()) CancellationToken.None
|> Async.AwaitTask
|> Async.RunSynchronously
When not to use a background builder
Do not use a background builder when the continuation must run on the caller's context. For example, UI code that updates controls after an await normally needs to resume on the UI thread.
For ASP.NET Core request handlers, start with the non-background builder unless you have a specific custom scheduler or context concern.
val string: value: 'T -> string
--------------------
type string = String
String.Trim([<ParamArray>] trimChars: char array) : string
String.Trim(trimChar: char) : string
type Task = interface IAsyncResult interface IDisposable new: action: Action -> unit + 7 overloads member ConfigureAwait: continueOnCapturedContext: bool -> ConfiguredTaskAwaitable + 1 overload member ContinueWith: continuationAction: Action<Task,obj> * state: obj -> Task + 19 overloads member Dispose: unit -> unit member GetAwaiter: unit -> TaskAwaiter member RunSynchronously: unit -> unit + 1 overload member Start: unit -> unit + 1 overload member Wait: unit -> unit + 5 overloads ...
<summary>Represents an asynchronous operation.</summary>
--------------------
type Task<'TResult> = inherit Task new: ``function`` : Func<obj,'TResult> * state: obj -> unit + 7 overloads member ConfigureAwait: continueOnCapturedContext: bool -> ConfiguredTaskAwaitable<'TResult> + 1 overload member ContinueWith: continuationAction: Action<Task<'TResult>,obj> * state: obj -> Task + 19 overloads member GetAwaiter: unit -> TaskAwaiter<'TResult> member WaitAsync: cancellationToken: CancellationToken -> Task<'TResult> + 4 overloads member Result: 'TResult static member Factory: TaskFactory<'TResult>
<summary>Represents an asynchronous operation that can return a value.</summary>
<typeparam name="TResult">The type of the result produced by this <see cref="T:System.Threading.Tasks.Task`1" />.</typeparam>
--------------------
Task(action: Action) : Task
Task(action: Action, cancellationToken: CancellationToken) : Task
Task(action: Action, creationOptions: TaskCreationOptions) : Task
Task(action: Action<obj>, state: obj) : Task
Task(action: Action, cancellationToken: CancellationToken, creationOptions: TaskCreationOptions) : Task
Task(action: Action<obj>, state: obj, cancellationToken: CancellationToken) : Task
Task(action: Action<obj>, state: obj, creationOptions: TaskCreationOptions) : Task
Task(action: Action<obj>, state: obj, cancellationToken: CancellationToken, creationOptions: TaskCreationOptions) : Task
--------------------
Task(``function`` : Func<'TResult>) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj) : Task<'TResult>
Task(``function`` : Func<'TResult>, cancellationToken: CancellationToken) : Task<'TResult>
Task(``function`` : Func<'TResult>, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj, cancellationToken: CancellationToken) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : Func<'TResult>, cancellationToken: CancellationToken, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj, cancellationToken: CancellationToken, creationOptions: TaskCreationOptions) : Task<'TResult>
Task.Delay(millisecondsDelay: int) : Task
Task.Delay(delay: TimeSpan, timeProvider: TimeProvider) : Task
Task.Delay(delay: TimeSpan, cancellationToken: CancellationToken) : Task
Task.Delay(millisecondsDelay: int, cancellationToken: CancellationToken) : Task
Task.Delay(delay: TimeSpan, timeProvider: TimeProvider, cancellationToken: CancellationToken) : Task
type RecordingSynchronizationContext = inherit SynchronizationContext new: unit -> RecordingSynchronizationContext override Post: callback: SendOrPostCallback * state: obj -> unit member PostCount: int
--------------------
new: unit -> RecordingSynchronizationContext
type SynchronizationContext = new: unit -> unit member CreateCopy: unit -> SynchronizationContext member IsWaitNotificationRequired: unit -> bool member OperationCompleted: unit -> unit member OperationStarted: unit -> unit member Post: d: SendOrPostCallback * state: obj -> unit member Send: d: SendOrPostCallback * state: obj -> unit member Wait: waitHandles: nativeint array * waitAll: bool * millisecondsTimeout: int -> int static member SetSynchronizationContext: syncContext: SynchronizationContext -> unit static member Current: SynchronizationContext
<summary>Provides the basic functionality for propagating a synchronization context in various synchronization models.</summary>
--------------------
SynchronizationContext() : SynchronizationContext
<summary>Gets the synchronization context for the current thread.</summary>
<returns>A <see cref="T:System.Threading.SynchronizationContext" /> object representing the current synchronization context.</returns>
<summary> Builds a taskUnit using computation expression syntax which switches to execute on a background thread if not already doing so. </summary>
<summary> Builds a coldTask using computation expression syntax which switches to execute on a background thread if not already doing so. </summary>
<summary> Builds a cancellableTask using computation expression syntax which switches to execute on a background thread if not already doing so. </summary>
module CancellableTask from IcedTasks.CancellableTasks.CancellableTasks
<summary> Contains functional helper functions for composing and converting CancellableTask values. </summary>
--------------------
type CancellableTask = CancellationToken -> Task
<summary> CancellationToken -> Task </summary>
--------------------
type CancellableTask<'T> = CancellationToken -> Task<'T>
<summary> CancellationToken -> Task<'T> </summary>
<summary>Gets the default cancellation token for executing computations.</summary>
<returns>The default CancellationToken.</returns>
<category index="3">Cancellation and Exceptions</category>
<example id="default-cancellation-token-1"><code lang="F#"> use tokenSource = new CancellationTokenSource() let primes = [ 2; 3; 5; 7; 11 ] for i in primes do let computation = cancellableTask { let! cancellationToken = CancellableTask.getCancellationToken() do! Task.Delay(i * 1000, cancellationToken) printfn $"{i}" } computation tokenSource.Token |> ignore Thread.Sleep(6000) tokenSource.Cancel() printfn "Tasks Finished" </code> This will print "2" 2 seconds from start, "3" 3 seconds from start, "5" 5 seconds from start, cease computation and then followed by "Tasks Finished". </example>
[<Struct>] type CancellationToken = new: canceled: bool -> unit member Equals: other: obj -> bool + 1 overload member GetHashCode: unit -> int member Register: callback: Action -> CancellationTokenRegistration + 4 overloads member ThrowIfCancellationRequested: unit -> unit member UnsafeRegister: callback: Action<obj,CancellationToken> * state: obj -> CancellationTokenRegistration + 1 overload static member (<>) : left: CancellationToken * right: CancellationToken -> bool static member (=) : left: CancellationToken * right: CancellationToken -> bool member CanBeCanceled: bool member IsCancellationRequested: bool ...
<summary>Propagates notification that operations should be canceled.</summary>
--------------------
CancellationToken ()
CancellationToken(canceled: bool) : CancellationToken
<summary>Returns an empty <see cref="T:System.Threading.CancellationToken" /> value.</summary>
<returns>An empty cancellation token.</returns>
type Async = static member AsBeginEnd: computation: ('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit) static member AwaitEvent: event: IEvent<'Del,'T> * ?cancelAction: (unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate) static member AwaitIAsyncResult: iar: IAsyncResult * ?millisecondsTimeout: int -> Async<bool> static member AwaitTask: task: Task<'T> -> Async<'T> + 1 overload static member AwaitWaitHandle: waitHandle: WaitHandle * ?millisecondsTimeout: int -> Async<bool> static member CancelDefaultToken: unit -> unit static member Catch: computation: Async<'T> -> Async<Choice<'T,exn>> static member Choice: computations: Async<'T option> seq -> Async<'T option> static member FromBeginEnd: beginAction: (AsyncCallback * obj -> IAsyncResult) * endAction: (IAsyncResult -> 'T) * ?cancelAction: (unit -> unit) -> Async<'T> + 3 overloads static member FromContinuations: callback: (('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T> ...
--------------------
type Async<'T>
static member Async.AwaitTask: task: Task<'T> -> Async<'T>
IcedTasks