Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions Microsoft.VisualStudio.Threading.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.3.32517.449
# Visual Studio Version 16
VisualStudioVersion = 16.0.28413.118
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.Threading.Analyzers", "src\Microsoft.VisualStudio.Threading.Analyzers\Microsoft.VisualStudio.Threading.Analyzers.csproj", "{536F3F9A-B457-43B8-BC93-CE1C16959037}"
EndProject
Expand All @@ -18,6 +18,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
version.json = version.json
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.Threading.Tests.Win7RegistryWatcher", "test\Microsoft.VisualStudio.Threading.Tests.Win7RegistryWatcher\Microsoft.VisualStudio.Threading.Tests.Win7RegistryWatcher.csproj", "{4961AA84-088C-46C0-BAC0-F9E87A9F03A7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.Threading", "src\Microsoft.VisualStudio.Threading\Microsoft.VisualStudio.Threading.csproj", "{D9BB9FB6-3833-44E8-B7A7-DE729FCE214D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.Threading.Tests", "test\Microsoft.VisualStudio.Threading.Tests\Microsoft.VisualStudio.Threading.Tests.csproj", "{CBEDB102-ABAE-40B1-AF3F-A6226DB6713D}"
Expand Down Expand Up @@ -56,6 +58,14 @@ Global
{620ED702-B6DA-4454-BF3E-5494D3652724}.Release|Any CPU.Build.0 = Release|Any CPU
{620ED702-B6DA-4454-BF3E-5494D3652724}.Release|NonWindows.ActiveCfg = Release|Any CPU
{620ED702-B6DA-4454-BF3E-5494D3652724}.Release|NonWindows.Build.0 = Release|Any CPU
{4961AA84-088C-46C0-BAC0-F9E87A9F03A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4961AA84-088C-46C0-BAC0-F9E87A9F03A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4961AA84-088C-46C0-BAC0-F9E87A9F03A7}.Debug|NonWindows.ActiveCfg = Debug|Any CPU
{4961AA84-088C-46C0-BAC0-F9E87A9F03A7}.Debug|NonWindows.Build.0 = Debug|Any CPU
{4961AA84-088C-46C0-BAC0-F9E87A9F03A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4961AA84-088C-46C0-BAC0-F9E87A9F03A7}.Release|Any CPU.Build.0 = Release|Any CPU
{4961AA84-088C-46C0-BAC0-F9E87A9F03A7}.Release|NonWindows.ActiveCfg = Release|Any CPU
{4961AA84-088C-46C0-BAC0-F9E87A9F03A7}.Release|NonWindows.Build.0 = Release|Any CPU
{D9BB9FB6-3833-44E8-B7A7-DE729FCE214D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D9BB9FB6-3833-44E8-B7A7-DE729FCE214D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D9BB9FB6-3833-44E8-B7A7-DE729FCE214D}.Debug|NonWindows.ActiveCfg = Debug|Any CPU
Expand Down
273 changes: 260 additions & 13 deletions src/Microsoft.VisualStudio.Threading/AwaitExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
namespace Microsoft.VisualStudio.Threading
{
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
Expand Down Expand Up @@ -206,28 +207,57 @@ public static ExecuteContinuationSynchronouslyAwaitable<T> ConfigureAwaitRunInli
private static async Task WaitForRegistryChangeAsync(SafeRegistryHandle registryKeyHandle, bool watchSubtree, RegistryChangeNotificationFilters change, CancellationToken cancellationToken)
{
#if NET5_0_OR_GREATER
if (!OperatingSystem.IsWindowsVersionAtLeast(8))
if (!OperatingSystem.IsWindowsVersionAtLeast(7))
#else
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
#endif
{
throw new PlatformNotSupportedException();
}

using ManualResetEvent evt = new(false);
REG_NOTIFY_FILTER dwNotifyFilter = (REG_NOTIFY_FILTER)change | REG_NOTIFY_FILTER.REG_NOTIFY_THREAD_AGNOSTIC;
WIN32_ERROR win32Error = PInvoke.RegNotifyChangeKeyValue(
registryKeyHandle,
watchSubtree,
dwNotifyFilter,
evt.SafeWaitHandle,
true);
if (win32Error != 0)
IDisposable? dedicatedThreadReleaser = null;
try
{
throw new Win32Exception((int)win32Error);
}
using ManualResetEvent evt = new(false);
REG_NOTIFY_FILTER dwNotifyFilter = (REG_NOTIFY_FILTER)change;

static void DoNotify(SafeRegistryHandle registryKeyHandle, bool watchSubtree, REG_NOTIFY_FILTER change, WaitHandle evt)
{
WIN32_ERROR win32Error = PInvoke.RegNotifyChangeKeyValue(
registryKeyHandle,
watchSubtree,
change,
evt.SafeWaitHandle,
true);
if (win32Error != 0)
{
throw new Win32Exception((int)win32Error);
}
}

await evt.ToTask(cancellationToken: cancellationToken).ConfigureAwait(false);
if (LightUps.IsWindows8OrLater)
{
dwNotifyFilter |= REG_NOTIFY_FILTER.REG_NOTIFY_THREAD_AGNOSTIC;
DoNotify(registryKeyHandle, watchSubtree, dwNotifyFilter, evt);
}
else
{
// Engage our downlevel support by using a single, dedicated thread to guarantee
// that we request notification on a thread that will not be destroyed later.
// Although we *could* await this, we synchronously block because our caller expects
// subscription to have begun before we return: for the async part to simply be notification.
// This async method we're calling uses .ConfigureAwait(false) internally so this won't
// deadlock if we're called on a thread with a single-thread SynchronizationContext.
Action registerAction = () => DoNotify(registryKeyHandle, watchSubtree, dwNotifyFilter, evt);
dedicatedThreadReleaser = DownlevelRegistryWatcherSupport.ExecuteOnDedicatedThreadAsync(registerAction).GetAwaiter().GetResult();
}

await evt.ToTask(cancellationToken: cancellationToken).ConfigureAwait(false);
}
finally
{
dedicatedThreadReleaser?.Dispose();
}
}

/// <summary>
Expand Down Expand Up @@ -751,5 +781,222 @@ public void OnCompleted(Action continuation)
TaskScheduler.Default);
}
}

/// <summary>
/// Provides a dedicated thread for requesting registry change notifications.
/// </summary>
/// <remarks>
/// For versions of Windows prior to Windows 8, requesting registry change notifications
/// required that the thread that made the request remain alive or else the watcher would
/// simply signal the event and stop watching for changes.
/// This class provides a single, dedicated thread for requesting such notifications
/// so that they don't get canceled when a thread happens to exit.
/// The dedicated thread is released when no one is watching the registry any more.
/// </remarks>
private static class DownlevelRegistryWatcherSupport
{
/// <summary>
/// The size of the stack allocated for a thread that expects to stay within just a few methods in depth.
/// </summary>
/// <remarks>
/// The default stack size for a thread is 1MB.
/// </remarks>
private const int SmallThreadStackSize = 100 * 1024;

/// <summary>
/// The object to lock when accessing any fields.
/// This is also the object that is waited on by the dedicated thread,
/// and may be pulsed by others to wake the dedicated thread to do some work.
/// </summary>
private static readonly object SyncObject = new object();

/// <summary>
/// A queue of actions the dedicated thread should take.
/// </summary>
private static readonly Queue<Tuple<Action, TaskCompletionSource<EmptyStruct>>> PendingWork = new();

/// <summary>
/// The number of callers that still have an interest in the survival of the dedicated thread.
/// The dedicated thread will exit when this value reaches 0.
/// </summary>
private static int keepAliveCount;

/// <summary>
/// The thread that should stay alive and be dequeuing <see cref="PendingWork"/>.
/// </summary>
private static Thread? liveThread;

/// <summary>
/// Executes some action on a long-lived thread.
/// </summary>
/// <param name="action">The delegate to execute.</param>
/// <returns>
/// A task that either faults with the exception thrown by <paramref name="action"/>
/// or completes after successfully executing the delegate
/// with a result that should be disposed when it is safe to terminate the long-lived thread.
/// </returns>
/// <remarks>
/// This thread never posts to <see cref="SynchronizationContext.Current"/>, so it is safe
/// to call this method and synchronously block on its result.
/// </remarks>
internal static async Task<IDisposable> ExecuteOnDedicatedThreadAsync(Action action)
{
Requires.NotNull(action, nameof(action));

var tcs = new TaskCompletionSource<EmptyStruct>();
bool keepAliveCountIncremented = false;
try
{
lock (SyncObject)
{
PendingWork.Enqueue(Tuple.Create(action, tcs));

try
{
// This block intentionally left blank.
}
finally
{
// We make these two assignments within a finally block
// to guard against an untimely ThreadAbortException causing
// us to execute just one of them.
keepAliveCountIncremented = true;
++keepAliveCount;
}

if (keepAliveCount == 1)
{
Assumes.Null(liveThread);
liveThread = new Thread(Worker, SmallThreadStackSize)
{
IsBackground = true,
Name = "Registry watcher",
};
liveThread.Start();
}
else
{
// There *could* temporarily be multiple threads in some race conditions.
// Pulse all of them so that the live one is sure to get the message.
Monitor.PulseAll(SyncObject);
}
}

await tcs.Task.ConfigureAwait(false);
return new ThreadHandleRelease();
}
catch
{
if (keepAliveCountIncremented)
{
// Our caller will never have a chance to release their claim on the dedicated thread,
// so do it for them.
ReleaseRefOnDedicatedThread();
}

throw;
}
}

/// <summary>
/// Decrements the count of interested parties in the live thread,
/// and helps it to terminate if necessary.
/// </summary>
private static void ReleaseRefOnDedicatedThread()
{
lock (SyncObject)
{
if (--keepAliveCount == 0)
{
liveThread = null;

// Wake up any obsolete thread(s) so they can go to exit.
Monitor.PulseAll(SyncObject);
}
}
}

/// <summary>
/// Executes thread-affinitized work from a queue until both the queue is empty
/// and any lingering interest in the survival of the dedicated thread has been released.
/// </summary>
/// <remarks>
/// This method serves as the <see cref="ThreadStart"/> for our dedicated thread.
/// </remarks>
private static void Worker()
{
while (true)
{
Tuple<Action, TaskCompletionSource<EmptyStruct>>? work = null;
lock (SyncObject)
{
if (Thread.CurrentThread != liveThread)
{
// Regardless of our PendingWork and keepAliveCount,
// it isn't meant for this thread any more.
// This happens when keepAliveCount (at least temporarily)
// hits 0, so this thread must be assumed to be on its exit path,
// and another thread will be spawned to process new requests.
Assumes.True(liveThread is object || (keepAliveCount == 0 && PendingWork.Count == 0));
return;
}

if (PendingWork.Count > 0)
{
work = PendingWork.Dequeue();
}
else if (keepAliveCount == 0)
{
// No work, and no reason to stay alive. Exit the thread.
return;
}
else
{
// Sleep until another thread wants to wake us up with a Pulse.
Monitor.Wait(SyncObject);
}
}

if (work is object)
{
try
{
work.Item1();
work.Item2.SetResult(EmptyStruct.Instance);
}
catch (Exception ex)
{
work.Item2.SetException(ex);
}
}
}
}

/// <summary>
/// Decrements the dedicated thread use counter by at most one upon disposal.
/// </summary>
private class ThreadHandleRelease : IDisposable
{
/// <summary>
/// A value indicating whether this instance has already been disposed.
/// </summary>
private bool disposed;

/// <summary>
/// Release the keep alive count reserved by this instance.
/// </summary>
public void Dispose()
{
lock (SyncObject)
{
if (!this.disposed)
{
this.disposed = true;
ReleaseRefOnDedicatedThread();
}
}
}
}
}
}
}
36 changes: 36 additions & 0 deletions src/Microsoft.VisualStudio.Threading/LightUps.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation. 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;

/// <summary>
/// A non-generic class used to store statics that do not vary by generic type argument.
/// </summary>
internal static class LightUps
{
/// <summary>
/// Gets a value indicating whether we execute Windows 7 code even on later versions of Windows.
/// </summary>
internal const bool ForceWindows7Mode = false;

/// <summary>
/// The <see cref="OperatingSystem.Version"/> for Windows 8.
/// </summary>
private static readonly Version Windows8Version = new Version(6, 2, 9200);

/// <summary>
/// Gets a value indicating whether the current operating system is Windows 8 or later.
/// </summary>
internal static bool IsWindows8OrLater
{
get
{
return !ForceWindows7Mode
&& Environment.OSVersion.Platform == PlatformID.Win32NT
&& Environment.OSVersion.Version >= Windows8Version;
}
}
}
}
Loading