From a24603fd9fd33e38c87bae4d2d356df6e08ecdcc Mon Sep 17 00:00:00 2001 From: Lifeng Lu Date: Sat, 25 Feb 2023 18:40:30 -0800 Subject: [PATCH 1/9] Fix integer overflow. --- src/Microsoft.VisualStudio.Threading/InternalUtilities.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.VisualStudio.Threading/InternalUtilities.cs b/src/Microsoft.VisualStudio.Threading/InternalUtilities.cs index 61ad43124..f8049bad9 100644 --- a/src/Microsoft.VisualStudio.Threading/InternalUtilities.cs +++ b/src/Microsoft.VisualStudio.Threading/InternalUtilities.cs @@ -62,7 +62,7 @@ internal static bool RemoveMidQueue(this Queue queue, T valueToRemove) } /// - /// Walk the continuation objects inside "async state machines" to generate the return callstack. + /// Walk the continuation objects inside "async state machines" to generate the return call stack. /// FOR DIAGNOSTIC PURPOSES ONLY. /// /// The delegate that represents the head of an async continuation chain. @@ -85,7 +85,7 @@ internal static IEnumerable GetAsyncReturnStackFrames(this Delegate cont stateMachine.GetType().FullName, state, AsyncReturnStackPrefix, - (int)GetAddress(stateMachine)); // the int cast allows hex formatting + (long)GetAddress(stateMachine)); // the int cast allows hex formatting Delegate[]? continuationDelegates = FindContinuationDelegates(stateMachine).ToArray(); if (continuationDelegates.Length == 0) From 6d73d6ed685a962fcd368f8185b32f32547d1f16 Mon Sep 17 00:00:00 2001 From: Lifeng Lu Date: Sat, 25 Feb 2023 18:53:26 -0800 Subject: [PATCH 2/9] Fix a problem that reader/writer lock hardcoded task scheduler. Preparing resource is a common routine to access evaluated project, most of the time, it is close to no-op when the evaluation is up to date. Because the code always schedules the call to the thread pool, it defeats the very reason to have a priority queue for high priority work when thread pool is too busy. It also makes it hard to throttle project evaluations through special task scheduler. The change is to try not to change the behavior, when the code is called on UI thread (as it can lead into new problems), but try to use the current task scheduler if possible. --- .../AsyncReaderWriterResourceLock`2.cs | 36 +++++++++++-------- .../CancellableJoinComputation.cs | 24 +++++++------ 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterResourceLock`2.cs b/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterResourceLock`2.cs index 2a8379541..49e66e998 100644 --- a/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterResourceLock`2.cs +++ b/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterResourceLock`2.cs @@ -503,7 +503,7 @@ internal class Helper /// /// A map of resources to the status of tasks that most recently began evaluating them. /// - private WeakKeyDictionary resourcePreparationStates = new WeakKeyDictionary(capacity: 2); + private readonly WeakKeyDictionary resourcePreparationStates = new WeakKeyDictionary(capacity: 2); /// /// Initializes a new instance of the class. @@ -697,7 +697,7 @@ internal async Task GetResourceAsync(TMoniker resourceMoniker, Cancel { using (AsyncReaderWriterResourceLock.ResourceReleaser resourceLock = this.AcquirePreexistingLockOrThrow()) { - TResource? resource = await this.service.GetResourceAsync(resourceMoniker, cancellationToken).ConfigureAwait(false); + TResource? resource = await this.service.GetResourceAsync(resourceMoniker, cancellationToken).ConfigureAwaitRunInline(); Task preparationTask; lock (this.service.SyncObject) @@ -707,7 +707,7 @@ internal async Task GetResourceAsync(TMoniker resourceMoniker, Cancel preparationTask = this.PrepareResourceAsync(resource, cancellationToken); } - await preparationTask.ConfigureAwait(false); + await preparationTask.ConfigureAwaitRunInline(); return resource; } } @@ -733,6 +733,7 @@ private void SetUnknownResourceState(TResource resource) this.resourcePreparationStates[resource] = ResourcePreparationTaskState.Create( _ => previousState?.InnerTask ?? Task.CompletedTask, ResourceState.Unknown, + TaskScheduler.Default, CancellationToken.None).PreparationState; } } @@ -768,6 +769,7 @@ private Task PrepareResourceAsync(TResource resource, CancellationToken cancella AsyncReaderWriterResourceLock.Helper.ResourceState finalState = forConcurrentUse ? ResourceState.Concurrent : ResourceState.Exclusive; Task? preparationTask = null; + TaskScheduler taskScheduler = TaskScheduler.Current.MaximumConcurrencyLevel > 1 ? TaskScheduler.Current : TaskScheduler.Default; if (!this.resourcePreparationStates.TryGetValue(resource, out ResourcePreparationTaskState? preparationState)) { @@ -778,7 +780,7 @@ private Task PrepareResourceAsync(TResource resource, CancellationToken cancella // We kick this off on a new task because we're currently holding a private lock // and don't want to execute arbitrary code. // Let's also hide the ARWL from the delegate if this is a shared lock request. - using (forConcurrentUse ? this.service.HideLocks() : default(Suppression)) + using (forConcurrentUse ? this.service.HideLocks() : default) { // We can't currently use the caller's cancellation token for this task because // this task may be shared with others or call this method later, and we wouldn't @@ -789,8 +791,9 @@ private Task PrepareResourceAsync(TResource resource, CancellationToken cancella forConcurrentUse ? Tuple.Create(resource, combinedCancellationToken) : Tuple.Create(resource, this.service.GetAggregateLockFlags(), combinedCancellationToken), combinedCancellationToken, TaskCreationOptions.None, - TaskScheduler.Default).Unwrap(), + taskScheduler).Unwrap(), finalState, + taskScheduler, cancellationToken); } } @@ -803,7 +806,7 @@ private Task PrepareResourceAsync(TResource resource, CancellationToken cancella ? this.prepareResourceConcurrentContinuationDelegate : this.prepareResourceExclusiveContinuationDelegate; } - else if (!preparationState.TryJoinPrepationTask(out preparationTask, cancellationToken)) + else if (!preparationState.TryJoinPreparationTask(out preparationTask, taskScheduler, cancellationToken)) { preparationDelegate = forConcurrentUse ? this.prepareResourceConcurrentContinuationOnPossibleCancelledTaskDelegate @@ -817,7 +820,7 @@ private Task PrepareResourceAsync(TResource resource, CancellationToken cancella // We kick this off on a new task because we're currently holding a private lock // and don't want to execute arbitrary code. // Let's also hide the ARWL from the delegate if this is a shared lock request. - using (forConcurrentUse ? this.service.HideLocks() : default(Suppression)) + using (forConcurrentUse ? this.service.HideLocks() : default) { (preparationState, preparationTask) = ResourcePreparationTaskState.Create( combinedCancellationToken => preparationState.InnerTask.ContinueWith( @@ -825,8 +828,9 @@ private Task PrepareResourceAsync(TResource resource, CancellationToken cancella forConcurrentUse ? Tuple.Create(resource, combinedCancellationToken) : Tuple.Create(resource, this.service.GetAggregateLockFlags(), combinedCancellationToken), CancellationToken.None, TaskContinuationOptions.RunContinuationsAsynchronously, - TaskScheduler.Default).Unwrap(), + taskScheduler).Unwrap(), finalState, + taskScheduler, cancellationToken); } } @@ -879,12 +883,13 @@ internal ResourcePreparationTaskState(Func taskCreation /// /// A callback method to create the preparation task. /// The final resource state when the preparation is done. + /// A task scheduler for continuation. /// A cancellation token to abort the preparation task. /// The preparation task and its status to be used to join more waiting tasks later. - internal static (ResourcePreparationTaskState PreparationState, Task InitialTask) Create(Func taskCreation, ResourceState finalState, CancellationToken cancellationToken) + internal static (ResourcePreparationTaskState PreparationState, Task InitialTask) Create(Func taskCreation, ResourceState finalState, TaskScheduler taskScheduler, CancellationToken cancellationToken) { var preparationState = new ResourcePreparationTaskState(taskCreation, finalState, cancellationToken.CanBeCanceled); - Assumes.True(preparationState.TryJoinComputation(isInitialTask: true, out Task? initialTask, cancellationToken)); + Assumes.True(preparationState.TryJoinComputation(isInitialTask: true, out Task? initialTask, taskScheduler, cancellationToken)); return (preparationState, initialTask); } @@ -892,12 +897,13 @@ internal static (ResourcePreparationTaskState PreparationState, Task InitialTask /// /// Try to join an existing preparation task. /// - /// The new waiting task to be compeleted when the resource preparation is done. - /// A cancellation token to abandone the new waiting task. - /// True if it joins sucessfully, it return false, if the current task has been cancelled. - internal bool TryJoinPrepationTask([NotNullWhen(true)] out Task? task, CancellationToken cancellationToken) + /// The new waiting task to be completed when the resource preparation is done. + /// A task scheduler for continuation. + /// A cancellation token to abandon the new waiting task. + /// True if it joins successfully, it return false, if the current task has been cancelled. + internal bool TryJoinPreparationTask([NotNullWhen(true)] out Task? task, TaskScheduler taskScheduler, CancellationToken cancellationToken) { - return this.TryJoinComputation(isInitialTask: false, out task, cancellationToken); + return this.TryJoinComputation(isInitialTask: false, out task, taskScheduler, cancellationToken); } } } diff --git a/src/Microsoft.VisualStudio.Threading/CancellableJoinComputation.cs b/src/Microsoft.VisualStudio.Threading/CancellableJoinComputation.cs index 150a136f7..b121d2c1c 100644 --- a/src/Microsoft.VisualStudio.Threading/CancellableJoinComputation.cs +++ b/src/Microsoft.VisualStudio.Threading/CancellableJoinComputation.cs @@ -22,7 +22,7 @@ internal class CancellableJoinComputation /// /// A list of task completion sources which represents joined waiting requests with cancellable cancellation tokens. - /// When an individule cancellation token is triggered, we may allow that specific waiting task to continue, and only all of them abandone the computation then we may + /// When an individual cancellation token is triggered, we may allow that specific waiting task to continue, and only all of them abandon the computation then we may /// cancel the inner computation. /// private List? joinedWaitingList; @@ -142,13 +142,14 @@ internal CancellableJoinComputation(Func taskFactory, b /// /// It is true for the initial task starting the computation. This must be called once right after the constructor. /// Returns a task which can be waited on. + /// A task scheduler for continuation. /// A cancellation token to abort this waiting. /// It returns false, if the inner task is aborted. In which case, no way to join the existing computation. - internal bool TryJoinComputation(bool isInitialTask, [NotNullWhen(true)] out Task? task, CancellationToken cancellationToken) + internal bool TryJoinComputation(bool isInitialTask, [NotNullWhen(true)] out Task? task, TaskScheduler taskScheduler, CancellationToken cancellationToken) { if (!this.isCancellationAllowed) { - task = this.JoinNotCancellableTaskAsync(isInitialTask, cancellationToken); + task = this.JoinNotCancellableTaskAsync(isInitialTask, taskScheduler, cancellationToken); return true; } @@ -184,7 +185,7 @@ internal bool TryJoinComputation(bool isInitialTask, [NotNullWhen(true)] out Tas } } - // if the inner task is joined by a new uncancellable task, we will abandone the cancellation token source because we will never use it anymore. + // if the inner task is joined by a new not cancellable task, we will abandon the cancellation token source because we will never use it anymore. // we do it outside of our lock. CancellationTokenSource? combinedCancellationTokenSourceToDispose = null; @@ -213,11 +214,11 @@ internal bool TryJoinComputation(bool isInitialTask, [NotNullWhen(true)] out Tas this.isCancellationAllowed = false; - task = this.JoinNotCancellableTaskAsync(isInitialTask, CancellationToken.None); + task = this.JoinNotCancellableTaskAsync(isInitialTask, taskScheduler, CancellationToken.None); } else if (!this.isCancellationAllowed) { - task = this.JoinNotCancellableTaskAsync(isInitialTask, cancellationToken); + task = this.JoinNotCancellableTaskAsync(isInitialTask, taskScheduler, cancellationToken); } else { @@ -225,7 +226,7 @@ internal bool TryJoinComputation(bool isInitialTask, [NotNullWhen(true)] out Tas WaitingCancellationStatus status; - // we need increase the outstanding count before creating WiatingCancellationStatus. + // we need increase the outstanding count before creating WaitingCancellationStatus. // Under a rare race condition the cancellation token can be trigger with this time frame, and lead OnWaitingTaskCancelled to be called recursively // within this lock. It would be critical to make sure the outstandingWaitingCount to increase before decreasing there. this.outstandingWaitingCount++; @@ -257,14 +258,15 @@ internal bool TryJoinComputation(bool isInitialTask, [NotNullWhen(true)] out Tas /// A simple way to join if the inner task cannot be cancelled. /// /// Whether it is the first task to start the computation. + /// A task scheduler for continuation. /// A cancellation token to abort the waiting. /// A task to complete when the computation ends. - private Task JoinNotCancellableTaskAsync(bool isInitialTask, CancellationToken cancellationToken) + private Task JoinNotCancellableTaskAsync(bool isInitialTask, TaskScheduler taskScheduler, CancellationToken cancellationToken) { if (this.InnerTask.IsCompleted || (isInitialTask && !cancellationToken.CanBeCanceled)) { // Note: we don't reuse the inner task directly even for second request which is not cancellable. - // This is to prevent two task sychorized continuations, which can be blocking each other causing unexpected deadlocks. + // This is to prevent two task synchronized continuations, which can be blocking each other causing unexpected deadlocks. // Adding an extra async task continuation prevents this problem, because all async continuation will be queued before calling synchronized task continuations, // which are called one by one. return this.InnerTask; @@ -282,7 +284,7 @@ private Task JoinNotCancellableTaskAsync(bool isInitialTask, CancellationToken c t => t.GetAwaiter().GetResult(), cancellationToken, TaskContinuationOptions.RunContinuationsAsynchronously, - TaskScheduler.Default); + taskScheduler); } /// @@ -327,7 +329,7 @@ private class WaitingCancellationStatus : TaskCompletionSource /// /// The cancellation registration to handle the cancellation token of the request. /// - private CancellationTokenRegistration cancellationTokenRegistration; + private readonly CancellationTokenRegistration cancellationTokenRegistration; /// /// Initializes a new instance of the class. From 01417e7cf159ec365b421d9a6bcd1e2dbcb03cf2 Mon Sep 17 00:00:00 2001 From: Lifeng Lu Date: Sat, 25 Feb 2023 18:53:42 -0800 Subject: [PATCH 3/9] Fix some warnings/spellings. --- .../AsyncReaderWriterLock.cs | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterLock.cs b/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterLock.cs index c0ea76470..ced7522fb 100644 --- a/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterLock.cs +++ b/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterLock.cs @@ -126,6 +126,11 @@ public partial class AsyncReaderWriterLock : IDisposable /// private readonly Queue> beforeWriteReleasedCallbacks = new Queue>(); + /// + /// A helper class to produce ETW trace events. + /// + private readonly EventsHelper etw; + /// /// A value indicating whether extra resources should be spent to collect diagnostic information /// that may be useful in deadlock investigations. @@ -144,11 +149,6 @@ public partial class AsyncReaderWriterLock : IDisposable /// private bool completeInvoked; - /// - /// A helper class to produce ETW trace events. - /// - private EventsHelper etw; - /// /// A timer to recheck potential deadlock caused by pending writer locks. /// @@ -676,11 +676,11 @@ protected virtual Task OnBeforeExclusiveLockReleasedAsync() /// /// Get the task scheduler to execute the continuation when the lock is acquired. - /// AsyncReaderWriterLock uses a special to handle execusive locks, and will ignore task scheduler provided, so this is only used in a read lock scenario. + /// AsyncReaderWriterLock uses a special to handle exclusive locks, and will ignore task scheduler provided, so this is only used in a read lock scenario. /// This method is called within the execution context to wait the read lock, so it can pick up based on the current execution context. /// Note: the task scheduler is only used, when the lock is issued later. If the lock is issued immediately when returns true, it will be ignored. /// - /// A task scheduler to schedule the continutation task when a lock is issued. + /// A task scheduler to schedule the continuation task when a lock is issued. protected virtual TaskScheduler GetTaskSchedulerForReadLockRequest() { return TaskScheduler.Default; @@ -2198,32 +2198,41 @@ public class Awaiter : ICriticalNotifyCompletion /// /// The instance of the lock class to which this awaiter is affiliated. /// - private AsyncReaderWriterLock lck; + private readonly AsyncReaderWriterLock lck; /// /// The type of lock requested. /// - private LockKind kind; + private readonly LockKind kind; /// /// The "parent" lock (i.e. the lock within which this lock is nested) if any. /// - private Awaiter? nestingLock; + private readonly Awaiter? nestingLock; /// /// The cancellation token that would terminate waiting for a lock that is not yet available. /// - private CancellationToken cancellationToken; + private readonly CancellationToken cancellationToken; /// - /// The cancellation token event that should be disposed of to free memory when we no longer need to receive cancellation notifications. + /// The flags applied to this lock. /// - private CancellationTokenRegistration cancellationRegistration; + private readonly LockFlags options; /// - /// The flags applied to this lock. + /// The stack trace of the caller originally requesting the lock. /// - private LockFlags options; + /// + /// This field is initialized only when is constructed with + /// the captureDiagnostics parameter set to . + /// + private readonly StackTrace? requestingStackTrace; + + /// + /// The cancellation token event that should be disposed of to free memory when we no longer need to receive cancellation notifications. + /// + private CancellationTokenRegistration cancellationRegistration; /// /// Any exception to throw back to the lock requestor. @@ -2258,15 +2267,6 @@ public class Awaiter : ICriticalNotifyCompletion /// private SynchronizationContext? synchronizationContext; - /// - /// The stacktrace of the caller originally requesting the lock. - /// - /// - /// This field is initialized only when is constructed with - /// the captureDiagnostics parameter set to . - /// - private StackTrace? requestingStackTrace; - /// /// An arbitrary object that may be set by a derived type of the containing lock class. /// From b14c241a02a020ef7f483a3748dfffa640bbc7b6 Mon Sep 17 00:00:00 2001 From: Lifeng Lu Date: Sun, 26 Feb 2023 18:27:17 -0800 Subject: [PATCH 4/9] Add a unit test. --- .../AsyncReaderWriterResourceLockTests.cs | 84 +++++++++++++++++-- 1 file changed, 77 insertions(+), 7 deletions(-) diff --git a/test/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterResourceLockTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterResourceLockTests.cs index d250161a5..6e2b28336 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterResourceLockTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterResourceLockTests.cs @@ -762,6 +762,31 @@ public async Task PreparationReservesLock() Assert.True(!preparationStartTask.Result.CanBeCanceled); } + [Fact] + public async Task PreparationReservesThrottlingScheduler() + { + var resourceTask = new TaskCompletionSource(); + + this.resourceLock.SetPreparationCallback( + this.resources[1], + (r, c) => + { + resourceTask.SetResult(TaskScheduler.Current); + return Task.CompletedTask; + }); + + var schedulerPair = new ConcurrentExclusiveSchedulerPair(TaskScheduler.Default, 2); + + await schedulerPair.ConcurrentScheduler.SwitchTo(); + + using (AsyncReaderWriterResourceLock.ResourceReleaser access = await this.resourceLock.ReadLockAsync()) + { + _ = await access.GetResourceAsync(1); + } + + Assert.Equal(schedulerPair.ConcurrentScheduler, await resourceTask.Task); + } + [Fact] public async Task PreparationResourceTaskCanBeCancelled() { @@ -1519,7 +1544,7 @@ private class ResourceLockWrapper : AsyncReaderWriterResourceLock { private readonly List resources; - private readonly Dictionary, Task>> preparationTasks = new Dictionary, Task>>(); + private readonly Dictionary preparationTasks = new Dictionary(); private readonly AsyncAutoResetEvent preparationTaskBegun = new AsyncAutoResetEvent(); @@ -1538,6 +1563,11 @@ internal ResourceLockWrapper(List resources, ITestOutputHelper logger, this.logger = logger; } + private interface IResourcePreparation + { + Task PrepareResourceAsync(Resource resource, CancellationToken cancellationToken); + } + internal AsyncAutoResetEvent PreparationTaskBegun { get { return this.preparationTaskBegun; } @@ -1553,12 +1583,20 @@ internal Task SetPreparationTask(Resource resource, Task task var tcs = new TaskCompletionSource(); lock (this.preparationTasks) { - this.preparationTasks[resource] = Tuple.Create(tcs, task); + this.preparationTasks[resource] = new ResourcePreparationState(tcs, task); } return tcs.Task; } + internal void SetPreparationCallback(Resource resource, Func callback) + { + lock (this.preparationTasks) + { + this.preparationTasks[resource] = new ResourcePreparationCallback(callback); + } + } + internal new void SetResourceAsAccessed(Resource resource) { base.SetResourceAsAccessed(resource); @@ -1615,22 +1653,54 @@ private async Task GetPreparationTask(Resource resource, CancellationToken cance Assert.True(this.IsWriteLockHeld || !this.IsAnyLockHeld); Assert.False(Monitor.IsEntered(this.SyncObject)); - Tuple, Task>? tuple; + IResourcePreparation? resourcePreparation; lock (this.preparationTasks) { - if (this.preparationTasks.TryGetValue(resource, out tuple)) + if (this.preparationTasks.TryGetValue(resource, out resourcePreparation)) { this.preparationTasks.Remove(resource); // consume task } } - if (tuple is object) + if (resourcePreparation is object) { - tuple.Item1.SetResult(cancellationToken); // signal that the preparation method has been entered - await tuple.Item2; + await resourcePreparation.PrepareResourceAsync(resource, cancellationToken); } Assert.True(this.IsWriteLockHeld || !this.IsAnyLockHeld); } + + private class ResourcePreparationState : IResourcePreparation + { + private readonly TaskCompletionSource preparationStart; + private readonly Task preparationTask; + + public ResourcePreparationState(TaskCompletionSource preparationStart, Task preparationTask) + { + this.preparationStart = preparationStart; + this.preparationTask = preparationTask; + } + + public Task PrepareResourceAsync(Resource resource, CancellationToken cancellationToken) + { + this.preparationStart.SetResult(cancellationToken); // signal that the preparation method has been entered + return this.preparationTask; + } + } + + private class ResourcePreparationCallback : IResourcePreparation + { + private readonly Func callback; + + public ResourcePreparationCallback(Func callback) + { + this.callback = callback; + } + + public Task PrepareResourceAsync(Resource resource, CancellationToken cancellationToken) + { + return this.callback(resource, cancellationToken); + } + } } } From a2e5072684449a9cd90380edc97175a28b7b1a54 Mon Sep 17 00:00:00 2001 From: Lifeng Lu Date: Mon, 27 Feb 2023 14:43:43 -0800 Subject: [PATCH 5/9] Update test/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterResourceLockTests.cs Co-authored-by: Andrew Arnott --- .../AsyncReaderWriterResourceLockTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterResourceLockTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterResourceLockTests.cs index 6e2b28336..286e4a45c 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterResourceLockTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterResourceLockTests.cs @@ -763,7 +763,7 @@ public async Task PreparationReservesLock() } [Fact] - public async Task PreparationReservesThrottlingScheduler() + public async Task PreparationPreservesThrottlingScheduler() { var resourceTask = new TaskCompletionSource(); From e2ed02143747f23559028b446ec94ce18cf2f2d8 Mon Sep 17 00:00:00 2001 From: Lifeng Lu Date: Mon, 27 Feb 2023 14:44:04 -0800 Subject: [PATCH 6/9] Update src/Microsoft.VisualStudio.Threading/InternalUtilities.cs Co-authored-by: Andrew Arnott --- src/Microsoft.VisualStudio.Threading/InternalUtilities.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.VisualStudio.Threading/InternalUtilities.cs b/src/Microsoft.VisualStudio.Threading/InternalUtilities.cs index f8049bad9..ab6150081 100644 --- a/src/Microsoft.VisualStudio.Threading/InternalUtilities.cs +++ b/src/Microsoft.VisualStudio.Threading/InternalUtilities.cs @@ -85,7 +85,7 @@ internal static IEnumerable GetAsyncReturnStackFrames(this Delegate cont stateMachine.GetType().FullName, state, AsyncReturnStackPrefix, - (long)GetAddress(stateMachine)); // the int cast allows hex formatting + (long)GetAddress(stateMachine)); // the long cast allows hex formatting Delegate[]? continuationDelegates = FindContinuationDelegates(stateMachine).ToArray(); if (continuationDelegates.Length == 0) From a47420bdc55b79fbc1b8a2c6a9713695193b4628 Mon Sep 17 00:00:00 2001 From: Lifeng Lu Date: Tue, 28 Feb 2023 11:48:02 -0800 Subject: [PATCH 7/9] Add a virtual method to compute TaskScheduler. --- .../AsyncReaderWriterResourceLock`2.cs | 12 +++++++++++- .../net6.0-windows/PublicAPI.Unshipped.txt | 1 + .../AsyncReaderWriterResourceLockTests.cs | 5 +++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterResourceLock`2.cs b/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterResourceLock`2.cs index 49e66e998..42cbbd914 100644 --- a/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterResourceLock`2.cs +++ b/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterResourceLock`2.cs @@ -213,6 +213,16 @@ protected void SetAllResourcesToUnknownState() return (LockFlags)base.GetAggregateLockFlags(); } + /// + /// Gets a task scheduler to prepare a resource for concurrent access. + /// + /// The resource to prepare. + /// A . + protected virtual TaskScheduler GetTaskSchedulerToPrepareResourcesForConcurrentAccess(TResource resource) + { + return TaskScheduler.Default; + } + /// /// Prepares a resource for concurrent access. /// @@ -769,7 +779,7 @@ private Task PrepareResourceAsync(TResource resource, CancellationToken cancella AsyncReaderWriterResourceLock.Helper.ResourceState finalState = forConcurrentUse ? ResourceState.Concurrent : ResourceState.Exclusive; Task? preparationTask = null; - TaskScheduler taskScheduler = TaskScheduler.Current.MaximumConcurrencyLevel > 1 ? TaskScheduler.Current : TaskScheduler.Default; + TaskScheduler taskScheduler = this.service.GetTaskSchedulerToPrepareResourcesForConcurrentAccess(resource); if (!this.resourcePreparationStates.TryGetValue(resource, out ResourcePreparationTaskState? preparationState)) { diff --git a/src/Microsoft.VisualStudio.Threading/net6.0-windows/PublicAPI.Unshipped.txt b/src/Microsoft.VisualStudio.Threading/net6.0-windows/PublicAPI.Unshipped.txt index e69de29bb..6fe70cc92 100644 --- a/src/Microsoft.VisualStudio.Threading/net6.0-windows/PublicAPI.Unshipped.txt +++ b/src/Microsoft.VisualStudio.Threading/net6.0-windows/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.GetTaskSchedulerToPrepareResourcesForConcurrentAccess(TResource! resource) -> System.Threading.Tasks.TaskScheduler! \ No newline at end of file diff --git a/test/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterResourceLockTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterResourceLockTests.cs index 6e2b28336..b2b4c0546 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterResourceLockTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterResourceLockTests.cs @@ -1617,6 +1617,11 @@ protected override Task GetResourceAsync(int resourceMoniker, Cancella return Task.FromResult(this.resources[resourceMoniker]); } + protected override TaskScheduler GetTaskSchedulerToPrepareResourcesForConcurrentAccess(Resource resource) + { + return TaskScheduler.Current.MaximumConcurrencyLevel > 1 ? TaskScheduler.Current : TaskScheduler.Default; + } + protected override async Task PrepareResourceForConcurrentAccessAsync(Resource resource, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); From cf594560a1421c941f6882074357d06470c40ed4 Mon Sep 17 00:00:00 2001 From: Lifeng Lu Date: Tue, 28 Feb 2023 11:51:24 -0800 Subject: [PATCH 8/9] Update unit test name. --- .../AsyncReaderWriterResourceLockTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterResourceLockTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterResourceLockTests.cs index b9ab7ed35..8a6dc242a 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterResourceLockTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterResourceLockTests.cs @@ -763,7 +763,7 @@ public async Task PreparationReservesLock() } [Fact] - public async Task PreparationPreservesThrottlingScheduler() + public async Task LockServiceMayPreservesThrottlingScheduler() { var resourceTask = new TaskCompletionSource(); From c88393b9045e620d19880b6e4f61316376ed993e Mon Sep 17 00:00:00 2001 From: Lifeng Lu Date: Tue, 28 Feb 2023 14:01:59 -0800 Subject: [PATCH 9/9] Update public API for other target frameworks. --- .../net472/PublicAPI.Unshipped.txt | 1 + .../net6.0/PublicAPI.Unshipped.txt | 1 + .../netstandard2.0/PublicAPI.Unshipped.txt | 1 + 3 files changed, 3 insertions(+) diff --git a/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Unshipped.txt b/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Unshipped.txt index e69de29bb..6fe70cc92 100644 --- a/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Unshipped.txt +++ b/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.GetTaskSchedulerToPrepareResourcesForConcurrentAccess(TResource! resource) -> System.Threading.Tasks.TaskScheduler! \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.Threading/net6.0/PublicAPI.Unshipped.txt b/src/Microsoft.VisualStudio.Threading/net6.0/PublicAPI.Unshipped.txt index e69de29bb..6fe70cc92 100644 --- a/src/Microsoft.VisualStudio.Threading/net6.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.VisualStudio.Threading/net6.0/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.GetTaskSchedulerToPrepareResourcesForConcurrentAccess(TResource! resource) -> System.Threading.Tasks.TaskScheduler! \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Unshipped.txt b/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Unshipped.txt index e69de29bb..6fe70cc92 100644 --- a/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.GetTaskSchedulerToPrepareResourcesForConcurrentAccess(TResource! resource) -> System.Threading.Tasks.TaskScheduler! \ No newline at end of file