Table of Contents

Class JoinableTaskContext

Namespace
Microsoft.VisualStudio.Threading
Assembly
Microsoft.VisualStudio.Threading.dll

A common context within which joinable tasks may be created and interact to avoid deadlocks.

public class JoinableTaskContext : IHangReportContributor, IDisposable
Inheritance
JoinableTaskContext
Implements
Inherited Members

Remarks

There are three rules that should be strictly followed when using or interacting with JoinableTasks:

  1. If a method has certain thread apartment requirements (STA or MTA) it must either: a) Have an asynchronous signature, and asynchronously marshal to the appropriate thread if it isn't originally invoked on a compatible thread. The recommended way to switch to the main thread is:
    await JoinableTaskFactory.SwitchToMainThreadAsync();
    b) Have a synchronous signature, and throw an exception when called on the wrong thread. In particular, no method is allowed to synchronously marshal work to another thread (blocking while that work is done). Synchronous blocks in general are to be avoided whenever possible.
  2. When an implementation of an already-shipped public API must call asynchronous code and block for its completion, it must do so by following this simple pattern:
    JoinableTaskFactory.Run(async delegate {
        await SomeOperationAsync(...);
    });
  3. If ever awaiting work that was started earlier, that work must be Joined. For example, one service kicks off some asynchronous work that may later become synchronously blocking:
    JoinableTask longRunningAsyncWork = JoinableTaskFactory.RunAsync(async delegate {
        await SomeOperationAsync(...);
    });
    Then later that async work becomes blocking:
    longRunningAsyncWork.Join();
    or perhaps:
    await longRunningAsyncWork;
    Note however that this extra step is not necessary when awaiting is done immediately after kicking off an asynchronous operation.

Constructors

JoinableTaskContext()

Initializes a new instance of the JoinableTaskContext class assuming the current thread is the main thread and Current will provide the means to switch to the main thread from another thread.

public JoinableTaskContext()

Remarks

When Current is null at the time this constructor is invoked, requests to switch to the main thread using SwitchToMainThreadAsync(CancellationToken) will not result in any thread switch. This is appropriate for unit test environments where there is no main thread to switch to or processes which otherwise do not define a main thread. Thread safety concern: When configured without a synchronization context, code that requests the main thread as a means of avoiding concurrency may malfunction due to data race conditions.

JoinableTaskContext(Thread?, SynchronizationContext?)

Initializes a new instance of the JoinableTaskContext class.

public JoinableTaskContext(Thread? mainThread = null, SynchronizationContext? synchronizationContext = null)

Parameters

mainThread Thread

The thread to switch to in SwitchToMainThreadAsync(CancellationToken). If null, the current thread will be assumed to be the main thread.

synchronizationContext SynchronizationContext

The synchronization context to use to switch to the main thread.

If null is specified (or the argument is omitted), the current synchronization context will be used. If Current is also null, requests to switch to the main thread using SwitchToMainThreadAsync(CancellationToken) will not result in any thread switch. This is appropriate for unit test environments where there is no main thread to switch to or processes which otherwise do not define a main thread. Thread safety concern: When configured without a synchronization context, code that requests the main thread as a means of avoiding concurrency may malfunction due to data race conditions.

Properties

Factory

Gets the factory which creates joinable tasks that do not belong to a joinable task collection.

public JoinableTaskFactory Factory { get; }

Property Value

JoinableTaskFactory

IsNoOpContext

Gets a value indicating whether this instance is not associated with any main thread (e.g. created with CreateNoOpContext()).

public bool IsNoOpContext { get; }

Property Value

bool

Remarks

This allows library code to skip some additional work in the environments that do not have a main thread.

IsOnMainThread

Gets a value indicating whether the caller is executing on the main thread.

public bool IsOnMainThread { get; }

Property Value

bool

IsWithinJoinableTask

Gets a value indicating whether the caller is currently running within the context of a joinable task.

public bool IsWithinJoinableTask { get; }

Property Value

bool

Remarks

Use of this property is generally discouraged, as any operation that becomes a no-op when no ambient JoinableTask is present is very cheap. For clients that have complex algorithms that are only relevant if an ambient joinable task is present, this property may serve to skip that for performance reasons.

MainThread

Gets the main thread that can be shared by tasks created by this context.

public Thread MainThread { get; }

Property Value

Thread

NoMessagePumpSynchronizationContext

Gets a SynchronizationContext which, when applied, suppresses any message pump that may run during synchronous blocks of the calling thread.

protected virtual SynchronizationContext NoMessagePumpSynchronizationContext { get; }

Property Value

SynchronizationContext

Remarks

The default implementation of this property is effective in builds of this assembly that target the .NET Framework. But on builds that target the portable profile, it should be overridden to provide an effective platform-specific solution.

Methods

Capture()

Captures the caller's context and serializes it as a string that is suitable for application via a subsequent call to RunAsync(Func<Task>, string?, JoinableTaskCreationOptions).

public string? Capture()

Returns

string

A string that represent the current context, or null if there is none.

Remarks

To optimize calling patterns, this method returns null even when inside a JoinableTask context when this JoinableTaskContext was initialized without a SynchronizationContext, which means no main thread exists and thus there is no need to capture and reapply tokens.

CreateCollection()

Creates a collection for in-flight joinable tasks.

public JoinableTaskCollection CreateCollection()

Returns

JoinableTaskCollection

A new joinable task collection.

CreateDefaultFactory()

Creates a factory without a JoinableTaskCollection.

protected virtual JoinableTaskFactory CreateDefaultFactory()

Returns

JoinableTaskFactory

Remarks

Used for initializing the Factory property.

CreateFactory(JoinableTaskCollection)

Creates a joinable task factory that automatically adds all created tasks to a collection that can be jointly joined.

public virtual JoinableTaskFactory CreateFactory(JoinableTaskCollection collection)

Parameters

collection JoinableTaskCollection

The collection that all tasks should be added to.

Returns

JoinableTaskFactory

CreateNoOpContext()

Initializes a new instance of the JoinableTaskContext class that is configured to no-op on calls to SwitchToMainThreadAsync(CancellationToken).

public static JoinableTaskContext CreateNoOpContext()

Returns

JoinableTaskContext

A new instance of JoinableTaskContext.

Remarks

This method is equivalent to calling the JoinableTaskContext() constructor with the Current property first set to null. This entry point however will have the same behavior regardless of the value of Current.

The caller's thread will still be captured for use by such properties as MainThread and IsOnMainThread. These properties generally have no effect except as used by application-specific code beyond this library.

This method is useful for creating a JoinableTaskContext in a unit test environment or in a process that does not have a main thread, but which includes code that requires an instance of JoinableTaskContext or JoinableTaskFactory. Such code can receive the instance returned by this method and use it in a normal way but no main thread switches will be honored.

Thread safety concern: Because main thread switches will not be honored, code that requests the main thread as a means of avoiding concurrency may malfunction due to data race conditions.

Dispose()

public void Dispose()

Dispose(bool)

Disposes managed and unmanaged resources held by this instance.

protected virtual void Dispose(bool disposing)

Parameters

disposing bool

true if Dispose() was called; false if the object is being finalized.

GetHangReport()

Contributes data for a hang report.

protected virtual HangReportContribution GetHangReport()

Returns

HangReportContribution

The hang report contribution. Null values should be ignored.

IsMainThreadBlocked()

Gets a value indicating whether the main thread is blocked for the caller's completion.

public bool IsMainThreadBlocked()

Returns

bool

IsMainThreadMaybeBlocked()

Gets a very likely value whether the main thread is blocked for the caller's completion. It is less accurate when the UI thread blocking task just starts and hasn't been blocked yet, or the dependency chain is just removed. However, unlike IsMainThreadBlocked(), this implementation is lock free, and faster in high contention scenarios.

public bool IsMainThreadMaybeBlocked()

Returns

bool

OnFalseHangDetected(TimeSpan, Guid)

Invoked when an earlier hang report is false alarm.

protected virtual void OnFalseHangDetected(TimeSpan hangDuration, Guid hangId)

Parameters

hangDuration TimeSpan
hangId Guid

OnHangDetected(TimeSpan, int, Guid)

Invoked when a hang is suspected to have occurred involving the main thread.

protected virtual void OnHangDetected(TimeSpan hangDuration, int notificationCount, Guid hangId)

Parameters

hangDuration TimeSpan

The duration of the current hang.

notificationCount int

The number of times this hang has been reported, including this one.

hangId Guid

A random GUID that uniquely identifies this particular hang.

Remarks

A single hang occurrence may invoke this method multiple times, with increasing values in the hangDuration parameter.

SuppressRelevance()

Conceals any JoinableTask the caller is associated with until the returned value is disposed.

public JoinableTaskContext.RevertRelevance SuppressRelevance()

Returns

JoinableTaskContext.RevertRelevance

A value to dispose of to restore visibility into the caller's associated JoinableTask, if any.

Remarks

In some cases asynchronous work may be spun off inside a delegate supplied to Run, so that the work does not have privileges to re-enter the Main thread until the Run(Func<Task>) call has returned and the UI thread is idle. To prevent the asynchronous work from automatically being allowed to re-enter the Main thread, wrap the code that calls the asynchronous task in a using block with a call to this method as the expression.

this.JoinableTaskContext.RunSynchronously(async delegate {
    using(this.JoinableTaskContext.SuppressRelevance()) {
        var asyncOperation = Task.Run(async delegate {
            // Some background work.
            await this.JoinableTaskContext.SwitchToMainThreadAsync();
            // Some Main thread work, that cannot begin until the outer RunSynchronously call has returned.
        });
    }

    // Because the asyncOperation is not related to this Main thread work (it was suppressed),
    // the following await *would* deadlock if it were uncommented.
    ////await asyncOperation;
});