Header menu logo IcedTasks

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

task

backgroundTask

Task<'T>

taskUnit

backgroundTaskUnit

Task

coldTask

backgroundColdTask

unit -> Task<'T>

cancellableTask

backgroundCancellableTask

CancellationToken -> Task<'T>

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:

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.

namespace System
namespace System.Threading
namespace System.Threading.Tasks
namespace IcedTasks
val parseProfile: text: string -> string
val text: string
Multiple items
val string: value: 'T -> string

--------------------
type string = String
String.Trim() : string
String.Trim([<ParamArray>] trimChars: char array) : string
String.Trim(trimChar: char) : string
val loadProfileText: unit -> Task<string>
val backgroundTask: BackgroundTaskBuilder
Multiple items
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(delay: TimeSpan) : Task
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
Multiple items
type RecordingSynchronizationContext = inherit SynchronizationContext new: unit -> RecordingSynchronizationContext override Post: callback: SendOrPostCallback * state: obj -> unit member PostCount: int

--------------------
new: unit -> RecordingSynchronizationContext
Multiple items
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
val mutable postCount: int
val callback: SendOrPostCallback
val state: obj
SendOrPostCallback.Invoke(state: obj) : unit
val runWithSynchronizationContext: context: SynchronizationContext -> work: (unit -> Task<'T>) -> 'T
val context: SynchronizationContext
val work: (unit -> Task<'T>)
type unit = Unit
'T
val previous: SynchronizationContext
property SynchronizationContext.Current: SynchronizationContext with get
<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>
SynchronizationContext.SetSynchronizationContext(syncContext: SynchronizationContext) : unit
val normalTaskPostCount: int
val context: RecordingSynchronizationContext
val task: TaskBuilder
Task.Yield() : Runtime.CompilerServices.YieldAwaitable
property RecordingSynchronizationContext.PostCount: int with get
val backgroundTaskPostCount: int
val saveProfileAudit: unit -> Task
val backgroundTaskUnit: BackgroundTaskUnitBuilder
<summary> Builds a taskUnit using computation expression syntax which switches to execute on a background thread if not already doing so. </summary>
val loadProfileLater: unit -> ColdTask<string>
val backgroundColdTask: BackgroundColdTaskBuilder
<summary> Builds a coldTask using computation expression syntax which switches to execute on a background thread if not already doing so. </summary>
val loadProfileForRequest: unit -> CancellableTask<string>
val backgroundCancellableTask: BackgroundCancellableTaskBuilder
<summary> Builds a cancellableTask using computation expression syntax which switches to execute on a background thread if not already doing so. </summary>
val cancellationToken: CancellationToken
Multiple items
module CancellableTask from IcedTasks.CancellableTasks.CancellableTasks
<summary> Contains functional helper functions for composing and converting CancellableTask values. </summary>

--------------------
type CancellableTask = CancellationToken -> Task
<summary> CancellationToken -&gt; Task </summary>

--------------------
type CancellableTask<'T> = CancellationToken -> Task<'T>
<summary> CancellationToken -&gt; Task&lt;'T&gt; </summary>
val getCancellationToken: unit -> ct: CancellationToken -> ValueTask<CancellationToken>
<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 |&gt; 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>
val profileText: string
val auditResult: unit
val laterProfile: string
val operation: ColdTask<string>
val requestProfile: string
Multiple items
[<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
property CancellationToken.None: CancellationToken with get
<summary>Returns an empty <see cref="T:System.Threading.CancellationToken" /> value.</summary>
<returns>An empty cancellation token.</returns>
Multiple items
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 -> Async<unit>
static member Async.AwaitTask: task: Task<'T> -> Async<'T>
static member Async.RunSynchronously: computation: Async<'T> * ?timeout: int * ?cancellationToken: CancellationToken -> 'T

Type something to start searching.