// 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; } } }