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: 2 additions & 12 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 16
VisualStudioVersion = 16.0.28413.118
# Visual Studio Version 17
VisualStudioVersion = 17.3.32517.449
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,8 +18,6 @@ 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 @@ -58,14 +56,6 @@ 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
270 changes: 12 additions & 258 deletions src/Microsoft.VisualStudio.Threading/AwaitExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,49 +204,20 @@ public static ExecuteContinuationSynchronouslyAwaitable<T> ConfigureAwaitRunInli
/// </returns>
private static async Task WaitForRegistryChangeAsync(SafeRegistryHandle registryKeyHandle, bool watchSubtree, RegistryChangeNotificationFilters change, CancellationToken cancellationToken)
{
IDisposable? dedicatedThreadReleaser = null;
try
{
using (var evt = new ManualResetEvent(false))
{
static void DoNotify(SafeRegistryHandle registryKeyHandle, bool watchSubtree, RegistryChangeNotificationFilters change, WaitHandle evt)
{
int win32Error = NativeMethods.RegNotifyChangeKeyValue(
registryKeyHandle,
watchSubtree,
change,
evt.SafeWaitHandle,
true);
if (win32Error != 0)
{
throw new Win32Exception(win32Error);
}
}

if (LightUps.IsWindows8OrLater)
{
change |= NativeMethods.REG_NOTIFY_THREAD_AGNOSTIC;
DoNotify(registryKeyHandle, watchSubtree, change, 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, change, evt);
dedicatedThreadReleaser = DownlevelRegistryWatcherSupport.ExecuteOnDedicatedThreadAsync(registerAction).GetAwaiter().GetResult();
}

await evt.ToTask(cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
finally
using ManualResetEvent evt = new(false);
change |= NativeMethods.REG_NOTIFY_THREAD_AGNOSTIC;
int win32Error = NativeMethods.RegNotifyChangeKeyValue(
registryKeyHandle,
watchSubtree,
change,
evt.SafeWaitHandle,
true);
if (win32Error != 0)
{
dedicatedThreadReleaser?.Dispose();
throw new Win32Exception(win32Error);
}

await evt.ToTask(cancellationToken: cancellationToken).ConfigureAwait(false);
}

/// <summary>
Expand Down Expand Up @@ -770,222 +741,5 @@ 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 Queue<Tuple<Action, TaskCompletionSource<EmptyStruct>>>();

/// <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: 0 additions & 36 deletions src/Microsoft.VisualStudio.Threading/LightUps.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netcoreapp3.1;net5.0;net472</TargetFrameworks>
<TargetFrameworks>netstandard2.0;netcoreapp3.1;net6.0;net472</TargetFrameworks>

<Summary>Async synchronization primitives, async collections, TPL and dataflow extensions.</Summary>
<Description>Async synchronization primitives, async collections, TPL and dataflow extensions. The JoinableTaskFactory allows synchronously blocking the UI thread for async work. This package is applicable to any .NET application (not just Visual Studio).</Description>
Expand All @@ -9,8 +9,8 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition="$([MSBuild]::IsOSPlatform('Windows'))">
<TargetFrameworks>$(TargetFrameworks);net5.0-windows</TargetFrameworks>
<UseWPF Condition=" '$(TargetFramework)' != 'net5.0' ">true</UseWPF>
<TargetFrameworks>$(TargetFrameworks);net6.0-windows</TargetFrameworks>
<UseWPF Condition=" '$(TargetFramework)' != 'net6.0' ">true</UseWPF>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Update="Strings.resx">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@ namespace Microsoft.VisualStudio.Threading.Analyzers.Tests

internal static class ReferencesHelper
{
public static ReferenceAssemblies DefaultReferences = ReferenceAssemblies.Default
#if NETFRAMEWORK
public static ReferenceAssemblies DefaultReferences = ReferenceAssemblies.NetFramework.Net471.Default
#elif NETCOREAPP3_1
public static ReferenceAssemblies DefaultReferences = ReferenceAssemblies.NetCore.NetCoreApp31
#elif NET6_0
public static ReferenceAssemblies DefaultReferences = ReferenceAssemblies.Net.Net60
#else
#error Fix TFM conditions
#endif
.WithPackages(ImmutableArray.Create(
new PackageIdentity("System.Collections.Immutable", "5.0.0"),
new PackageIdentity("System.Threading.Tasks.Extensions", "4.5.4"),
Expand Down
Loading