// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Microsoft.VisualStudio.Threading
{
using System;
using System.Threading;
using System.Threading.Tasks;
///
/// Utility methods for working across threads.
///
public static class ThreadingTools
{
internal interface ICancellationNotification
{
void OnCanceled();
}
///
/// Optimistically performs some value transformation based on some field and tries to apply it back to the field,
/// retrying as many times as necessary until no other thread is manipulating the same field.
///
/// The type of data.
/// The field that may be manipulated by multiple threads.
/// A function that receives the unchanged value and returns the changed value.
///
/// true if the location's value is changed by applying the result of the function;
/// false if the location's value remained the same because the last invocation of returned the existing value.
///
public static bool ApplyChangeOptimistically(ref T hotLocation, Func applyChange)
where T : class?
{
Requires.NotNull(applyChange, nameof(applyChange));
bool successful;
do
{
T oldValue = Volatile.Read(ref hotLocation);
T newValue = applyChange(oldValue);
if (object.ReferenceEquals(oldValue, newValue))
{
// No change was actually required.
return false;
}
T actualOldValue = Interlocked.CompareExchange(ref hotLocation, newValue, oldValue);
successful = object.ReferenceEquals(oldValue, actualOldValue);
}
while (!successful);
return true;
}
///
/// Optimistically performs some value transformation based on some field and tries to apply it back to the field,
/// retrying as many times as necessary until no other thread is manipulating the same field.
///
///
/// Use this overload when requires a single item, as is common when updating immutable
/// collection types. By passing the item as a method operand, the caller may be able to avoid allocating a closure
/// object for every call.
///
/// The type of data to apply the change to.
/// The type of argument passed to the .
/// The field that may be manipulated by multiple threads.
/// An argument to pass to .
/// A function that receives both the unchanged value and , then returns the changed value.
///
/// true if the location's value is changed by applying the result of the function;
/// false if the location's value remained the same because the last invocation of returned the existing value.
///
public static bool ApplyChangeOptimistically(ref T hotLocation, TArg applyChangeArgument, Func applyChange)
where T : class?
{
Requires.NotNull(applyChange, nameof(applyChange));
bool successful;
do
{
T oldValue = Volatile.Read(ref hotLocation);
T newValue = applyChange(oldValue, applyChangeArgument);
if (object.ReferenceEquals(oldValue, newValue))
{
// No change was actually required.
return false;
}
T actualOldValue = Interlocked.CompareExchange(ref hotLocation, newValue, oldValue);
successful = object.ReferenceEquals(oldValue, actualOldValue);
}
while (!successful);
return true;
}
///
/// Wraps a task with one that will complete as cancelled based on a cancellation token,
/// allowing someone to await a task but be able to break out early by cancelling the token.
///
/// The type of value returned by the task.
/// The task to wrap.
/// The token that can be canceled to break out of the await.
/// The wrapping task.
public static Task WithCancellation(this Task task, CancellationToken cancellationToken)
{
Requires.NotNull(task, nameof(task));
if (!cancellationToken.CanBeCanceled || task.IsCompleted)
{
return task;
}
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
return WithCancellationSlow(task, cancellationToken);
}
///
/// Wraps a task with one that will complete as cancelled based on a cancellation token,
/// allowing someone to await a task but be able to break out early by cancelling the token.
///
/// The task to wrap.
/// The token that can be canceled to break out of the await.
/// The wrapping task.
public static Task WithCancellation(this Task task, CancellationToken cancellationToken)
{
Requires.NotNull(task, nameof(task));
if (!cancellationToken.CanBeCanceled || task.IsCompleted)
{
return task;
}
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
return WithCancellationSlow(task, continueOnCapturedContext: false, cancellationToken: cancellationToken);
}
///
/// Applies the specified to the caller's context.
///
/// The synchronization context to apply.
/// A value indicating whether to check that the applied SyncContext is still the current one when the original is restored.
public static SpecializedSyncContext Apply(this SynchronizationContext? syncContext, bool checkForChangesOnRevert = true)
{
return SpecializedSyncContext.Apply(syncContext, checkForChangesOnRevert);
}
///
/// Wraps a task with one that will complete as cancelled based on a cancellation token,
/// allowing someone to await a task but be able to break out early by cancelling the token.
///
/// The task to wrap.
/// A value indicating whether *internal* continuations required to respond to cancellation should run on the current .
/// The token that can be canceled to break out of the await.
/// The wrapping task.
internal static Task WithCancellation(this Task task, bool continueOnCapturedContext, CancellationToken cancellationToken)
{
Requires.NotNull(task, nameof(task));
if (!cancellationToken.CanBeCanceled || task.IsCompleted)
{
return task;
}
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
return WithCancellationSlow(task, continueOnCapturedContext, cancellationToken);
}
///
/// Cancels a if a given is canceled.
///
/// The type of value returned by a successfully completed .
/// The to cancel.
/// The .
/// A callback to invoke when cancellation occurs.
internal static void AttachCancellation(this TaskCompletionSource taskCompletionSource, CancellationToken cancellationToken, ICancellationNotification? cancellationCallback = null)
{
Requires.NotNull(taskCompletionSource, nameof(taskCompletionSource));
if (cancellationToken.CanBeCanceled && !taskCompletionSource.Task.IsCompleted)
{
if (cancellationToken.IsCancellationRequested)
{
taskCompletionSource.TrySetCanceled(cancellationToken);
}
else
{
var tuple = new CancelableTaskCompletionSource(taskCompletionSource, cancellationCallback, cancellationToken);
tuple.CancellationTokenRegistration = cancellationToken.Register(
s =>
{
var t = (CancelableTaskCompletionSource)s!;
if (t.TaskCompletionSource.TrySetCanceled(t.CancellationToken))
{
t.CancellationCallback?.OnCanceled();
}
},
tuple,
useSynchronizationContext: false);
// In certain race conditions, our continuation could execute inline. We could force it to always run
// asynchronously, but then in the common case it becomes less efficient.
// Instead, we will optimize for the common (no-race) case and detect if we were inlined, and if so, defer the work
// to avoid making our caller block for arbitrary code since CTR.Dispose blocks for in-progress cancellation notification to complete.
taskCompletionSource.Task.ContinueWith(
(_, s) =>
{
var t = (CancelableTaskCompletionSource)s!;
if (t.ContinuationScheduled || !t.OnOwnerThread)
{
// We're not executing inline... Go ahead and do the work.
t.CancellationTokenRegistration.Dispose();
}
else if (!t.CancellationToken.IsCancellationRequested) // If the CT is canceled, the CTR is implicitly disposed.
{
// We hit the race where the task is already completed another way,
// and our continuation is executing inline with our caller.
// Dispose our CTR from the threadpool to avoid blocking on 3rd party code.
ThreadPool.QueueUserWorkItem(
s2 =>
{
try
{
var t2 = (CancelableTaskCompletionSource)s2!;
t2.CancellationTokenRegistration.Dispose();
}
catch (Exception ex)
{
// Swallow any exception.
Report.Fail(ex.Message);
}
},
s);
}
},
tuple,
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
tuple.ContinuationScheduled = true;
}
}
}
///
/// Wraps a task with one that will complete as cancelled based on a cancellation token,
/// allowing someone to await a task but be able to break out early by cancelling the token.
///
/// The type of value returned by the task.
/// The task to wrap.
/// The token that can be canceled to break out of the await.
/// The wrapping task.
private static async Task WithCancellationSlow(Task task, CancellationToken cancellationToken)
{
Assumes.NotNull(task);
Assumes.True(cancellationToken.CanBeCanceled);
var tcs = new TaskCompletionSource();
using (cancellationToken.Register(s => ((TaskCompletionSource)s!).TrySetResult(true), tcs))
{
if (task != await Task.WhenAny(task, tcs.Task).ConfigureAwait(false))
{
cancellationToken.ThrowIfCancellationRequested();
}
}
// Rethrow any fault/cancellation exception, even if we awaited above.
// But if we skipped the above if branch, this will actually yield
// on an incompleted task.
return await task.ConfigureAwait(false);
}
///
/// Wraps a task with one that will complete as cancelled based on a cancellation token,
/// allowing someone to await a task but be able to break out early by cancelling the token.
///
/// The task to wrap.
/// A value indicating whether *internal* continuations required to respond to cancellation should run on the current .
/// The token that can be canceled to break out of the await.
/// The wrapping task.
private static async Task WithCancellationSlow(this Task task, bool continueOnCapturedContext, CancellationToken cancellationToken)
{
Assumes.NotNull(task);
Assumes.True(cancellationToken.CanBeCanceled);
var tcs = new TaskCompletionSource();
using (cancellationToken.Register(s => ((TaskCompletionSource)s!).TrySetResult(true), tcs))
{
if (task != await Task.WhenAny(task, tcs.Task).ConfigureAwait(continueOnCapturedContext))
{
cancellationToken.ThrowIfCancellationRequested();
}
}
// Rethrow any fault/cancellation exception, even if we awaited above.
// But if we skipped the above if branch, this will actually yield
// on an incompleted task.
await task.ConfigureAwait(continueOnCapturedContext);
}
///
/// A state object for tracking cancellation and a TaskCompletionSource.
///
/// The type of value returned from a task.
///
/// We use this class so that we only allocate one object to support all continuations
/// required for cancellation handling, rather than a special closure and delegate for each one.
///
private class CancelableTaskCompletionSource
{
///
/// The ID of the thread on which this instance was created.
///
private readonly int ownerThreadId = Environment.CurrentManagedThreadId;
///
/// Initializes a new instance of the class.
///
/// The task completion source.
/// A callback to invoke when cancellation occurs.
/// The cancellation token.
internal CancelableTaskCompletionSource(TaskCompletionSource taskCompletionSource, ICancellationNotification? cancellationCallback, CancellationToken cancellationToken)
{
this.TaskCompletionSource = taskCompletionSource ?? throw new ArgumentNullException(nameof(taskCompletionSource));
this.CancellationToken = cancellationToken;
this.CancellationCallback = cancellationCallback;
}
///
/// Gets the cancellation token.
///
internal CancellationToken CancellationToken { get; }
///
/// Gets the Task completion source.
///
internal TaskCompletionSource TaskCompletionSource { get; }
internal ICancellationNotification? CancellationCallback { get; }
///
/// Gets or sets the cancellation token registration.
///
internal CancellationTokenRegistration CancellationTokenRegistration { get; set; }
///
/// Gets or sets a value indicating whether the continuation has been scheduled (and not run inline).
///
internal bool ContinuationScheduled { get; set; }
///
/// Gets a value indicating whether the caller is on the same thread as the one that created this instance.
///
internal bool OnOwnerThread => Environment.CurrentManagedThreadId == this.ownerThreadId;
}
}
}