为什么 Task.Delay 会破坏线程的 STA 状态?

Why is Task.Delay breaking the STA state of the thread?

简介

这是一个冗长的问题!您会在开头找到有关该问题的一些背景知识,然后是代码示例,这些代码示例已针对表示进行了简化,然后是 问题。请按照您认为适合自己的顺序阅读!

背景资料

我正在为与 STA COM 通信的应用程序编写概念验证部分。应用程序的这一部分要求在单线程单元 (STA) 上下文中 运行ning 以便与所述 STA COM 通信。 MTA 上下文中的应用程序的其余部分 运行。

当前状态

到目前为止我想出的是创建一个包含 while 循环的 Communication class,运行ning 在一个STA。需要中继到 COM 对象的工作通过 ConcurrentQueue 从外部排队到 Communication class。然后工作项在 while 循环中出列并执行工作。

代码上下文

通讯class

这是一个 static class,包含一个循环,旨在 运行 处于 STA 状态并检查是否需要由 COM 完成某些工作并分派工作给处理程序。

static class Communication
{
    #region Public Events

    /// This event is raised when the COM object has been initialized
    public static event EventHandler OnCOMInitialized;

    #endregion Public Events

    #region Private Members

    /// Stores a reference to the COM object
    private static COMType s_comObject;

    /// Used to queue work that needs to be done by the COM object
    private static ConcurrentQueue<WorkUnit> s_workQueue;

    #endregion Private Members

    #region Private Methods

    /// Initializes the COM object
    private static void InternalInitializeCOM()
    {
        s_comObject = new COMType();

        if (s_comObject.Init())
        {
            OnCOMInitialized?.Invoke(null, EventArgs.Empty);
        }
    }

    /// Dispatches the work unit to the correct handler
    private static void HandleWork(WorkUnit work)
    {
        switch (work.Command)
        {
            case WorkCommand.Initialize:
                InternalInitializeCOM();
                break;
            default:
                break;
        }
    }

    #endregion Private Methods

    #region Public Methods

    /// Starts the processing loop
    public static void StartCommunication()
    {
        s_workQueue = new ConcurrentQueue<WorkUnit>();

        while (true)
        {
            if (s_workQueue.TryDequeue(out var workUnit))
            {
                HandleWork(workUnit);
            }

            // [Place for a delaying logic]
        }
    }

    /// Wraps the work unit creation for the task of Initializing the COM
    public static void InitializeCOM()
    {
        var workUnit = new WorkUnit(
            command: WorkCommand.Initialize,
            arguments: null
        );
        s_workQueue.Enqueue(workUnit);
    }

    #endregion Public Methods
}

工作指令

此 class 描述了需要完成的工作以及可能提供的任何参数。

enum WorkCommand
{
    Initialize
}

工作单位

此枚举定义了 COM 可以执行的各种任务。

class WorkUnit
{
    #region Public Properties

    public WorkCommand Command { get; private set; }

    public object[] Arguments { get; private set; }

    #endregion Public Properties

    #region Constructor

    public WorkUnit(WorkCommand command, object[] arguments)
    {
        Command = command;
        Arguments = arguments == null
            ? new object[0]
            : arguments;
    }

    #endregion Constructor
}

所有者

这是 拥有 spawns Communication 与 COM 的 class 样本是对 Communication 的抽象,用于应用程序的其余部分。

class COMController
{
    #region Public Events

    /// This event is raised when the COM object has been initialized
    public event EventHandler OnInitialize;

    #endregion Public Events

    #region Constructor

    /// Creates a new COMController instance and starts the communication
    public COMController()
    {
        var communicationThread = new Thread(() =>
        {
            Communication.StartCommunication();
        });
        communicationThread.SetApartmentState(ApartmentState.STA);
        communicationThread.Start();

        Communication.OnCOMInitialized += HandleCOMInitialized;
    }

    #endregion Constructor

    #region Private Methods

    /// Handles the initialized event raised from the Communication
    private void HandleCOMInitialized()
    {
        OnInitialize?.Invoke(this, EventArgs.Emtpy);
    }

    #endregion Private Methods

    #region Public Methods

    /// Requests that the COM object be initialized
    public void Initialize()
    {
        Communication.InitializeCOM();
    }

    #endregion Public Methods
}

问题

现在,看看 Communication.StartCommunication() 方法,更具体地说是这一部分:

...
// [Place for a delaying logic]
...

如果用以下内容替换此行:

await Task.Delay(TimeSpan.FromMilliseconds(100)).ConfigureAwait(false);
// OR
await Task.Delay(TimeSpan.FromMilliseconds(100)).ConfigureAwait(true);

在检查期间最后一站 - Communication.InternalInitializeCOM() 线程的单元似乎是 MTA

但是,如果延迟逻辑改为

Thread.Sleep(100);

CommunicationInternalInitializeCOM()方法似乎是在STA状态下执行的。

检查由Thread.CurrentThread.GetApartmentState()完成。

问题

任何人都可以向我解释为什么 Task.Delay 会打破 STA 状态吗?还是我在这里做错了什么?

谢谢!

感谢您抽出宝贵时间阅读问题!祝你有美好的一天!

因为在 await Task.Delay() 语句之后,您的代码在 ThreadPool 线程之一内运行,并且由于 ThreadPool 线程在设计上是 MTA。

var th = new Thread(async () =>
        {
            var beforAwait = Thread.CurrentThread.GetApartmentState(); // ==> STA 

             await Task.Delay(1000);

            var afterAwait = Thread.CurrentThread.GetApartmentState(); // ==> MTA

        });

        th.SetApartmentState(ApartmentState.STA);
        th.Start();

汉斯成功了。从技术上讲,您的代码被破坏是因为没有 SynchronizationContext captured by the await。但是写一个也不够

这种方法的一个大问题是您的 STA 线程没有启动。 STA 线程必须 抽取 Win32 消息队列,否则它们就不是 STA 线程。 SetApartmentState(ApartmentState.STA) 只是告诉运行时这是一个 STA 线程;它不会使成为 STA 线程。您必须为它发送消息才能成为 STA 线程。

您可以自己编写消息泵,但我不知道有谁敢这样做。大多数人从 WinForms (a la Hans' answer) or WPF. It may also be possible to do this with a UWP message pump.

安装消息泵

使用提供的消息泵的一个很好的副作用是它们还提供 SynchronizationContext(例如,WinFormsSynchronizationContext / DispatcherSynchronizationContext),因此 await 自然地工作.此外,由于每个 .NET UI 框架都定义了一个 "run this delegate" Win32 消息,因此底层 Win32 消息队列也可以包含您想要排队到线程的所有工作,因此显式队列及其 "runner"代码不再需要。