/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/EditorFeatures/TestUtilities/Threading/StaTaskScheduler.cs
119 строк
6 KB
Cyrus Najmabadi
Use file scoped namespaces. (#77854)
27 мар 2025, 09:55
Не верифицирован
27 мар 2025, 09:55
98d41b8
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. #nullable disable using System; using System.Reflection; using System.Threading; using System.Windows.Threading; using Microsoft.CodeAnalysis.Test.Utilities; namespace Roslyn.Test.Utilities; public sealed class StaTaskScheduler : IDisposable { /// <summary>Gets a <see cref="StaTaskScheduler"/> for the current <see cref="AppDomain"/>.</summary> /// <remarks>We use a count of 1, because the editor ends up re-using <see cref="DispatcherObject"/> /// instances between tests, so we need to always use the same thread for our Sta tests.</remarks> public static StaTaskScheduler DefaultSta { get; } = new StaTaskScheduler(); /// <summary>The STA threads used by the scheduler.</summary> public Thread StaThread { get; } public bool IsRunningInScheduler => StaThread.ManagedThreadId == Environment.CurrentManagedThreadId; static StaTaskScheduler() { // Overwrite xunit's app domain handling to not call AppDomain.Unload var getDefaultDomain = typeof(AppDomain).GetMethod("GetDefaultDomain", BindingFlags.NonPublic | BindingFlags.Static); var defaultDomain = (AppDomain)getDefaultDomain.Invoke(null, null); var hook = (XunitDisposeHook)defaultDomain.CreateInstanceFromAndUnwrap(typeof(XunitDisposeHook).Assembly.CodeBase, typeof(XunitDisposeHook).FullName, ignoreCase: false, BindingFlags.CreateInstance, binder: null, args: null, culture: null, activationAttributes: null); hook.Execute(); // We've created an STA thread, which has some extra requirements for COM Runtime // Callable Wrappers (RCWs). If any COM object is created on the STA thread, calls to that // object must be made from that thread; when the RCW is no longer being used by any // managed code, the RCW is put into the finalizer queue, but to actually finalize it // it has to marshal to the STA thread to do the work. This means that in order to safely // clean up any RCWs, we need to ensure that the thread is pumping past the point of // all RCWs being finalized // // This constraint is particularly problematic if our tests are running in an AppDomain: // when the AppDomain is unloaded, any threads (including our STA thread) are going to be // aborted. Once the thread and AppDomain is being torn down, the CLR is going to try cleaning up // any RCWs associated them, because if the thread is gone for good there's no way // it could ever clean anything further up. The code there waits for the finalizer queue // -- but the finalizer queue might be already trying to clean up an RCW, which is marshaling // to the STA thread. This could then deadlock. // // The suggested workaround from the CLR team is to do an explicit GC.Collect and // WaitForPendingFinalizers before we let the AppDomain shut down. The belief is subscribing // to DomainUnload is a reasonable place to do it. AppDomain.CurrentDomain.DomainUnload += (sender, e) => { GC.Collect(); GC.WaitForPendingFinalizers(); }; } /// <summary>Initializes a new instance of the <see cref="StaTaskScheduler"/> class.</summary> public StaTaskScheduler() { using (var threadStartedEvent = new ManualResetEventSlim(initialState: false)) { DispatcherSynchronizationContext synchronizationContext = null; StaThread = new Thread(() => { var oldContext = SynchronizationContext.Current; try { // All WPF Tests need a DispatcherSynchronizationContext and we dont want to block pending keyboard // or mouse input from the user. So use background priority which is a single level below user input. synchronizationContext = new DispatcherSynchronizationContext(); // xUnit creates its own synchronization context and wraps any existing context so that messages are // still pumped as necessary. So we are safe setting it here, where we are not safe setting it in test. SynchronizationContext.SetSynchronizationContext(synchronizationContext); threadStartedEvent.Set(); Dispatcher.Run(); } finally { SynchronizationContext.SetSynchronizationContext(oldContext); } }); StaThread.Name = $"{nameof(StaTaskScheduler)} thread"; StaThread.IsBackground = true; StaThread.SetApartmentState(ApartmentState.STA); StaThread.Start(); threadStartedEvent.Wait(); DispatcherSynchronizationContext = synchronizationContext; } // Work around the WeakEventTable Shutdown race conditions AppContext.SetSwitch("Switch.MS.Internal.DoNotInvokeInWeakEventTableShutdownListener", isEnabled: true); } public DispatcherSynchronizationContext DispatcherSynchronizationContext { get; } /// <summary> /// Cleans up the scheduler by indicating that no more tasks will be queued. /// This method blocks until all threads successfully shutdown. /// </summary> public void Dispose() { if (StaThread.IsAlive) { DispatcherSynchronizationContext.Post(_ => Dispatcher.ExitAllFrames(), null); StaThread.Join(); } } }