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

4

介绍

这是一个相当冗长的问题!在问题之前,你会找到一些背景信息,然后是简化表示的代码示例和问题。请按照您认为合适的任意顺序阅读!

背景信息

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

当前状态

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

代码上下文

通信类

这是一个静态类,其中包含一个旨在以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
}

工作命令

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

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
}

所有者

这是一个拥有或生成与COM进行通信的Communication类的示例,并且是在应用程序的其余部分中使用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 状态吗?或者我在这里做错了什么?

谢谢!

感谢您抽出时间阅读这个问题!祝您拥有美好的一天!


1
.ConfigureAwait(false)指示await不要捕获上下文,这通常意味着代码将在另一个线程上执行。Thread.Sleep阻塞当前线程,与异步或公寓状态无关。 - GSerg
.ConfigureAwait(true) 实现了与 ..(false) 相同的结果。忘记在问题中添加它了。感谢您的注意! - ful-stackz
https://dev59.com/rGQn5IYBdhLWcg3wmH8s -> https://dev59.com/7m025IYBdhLWcg3wfmHw -> https://dev59.com/8GPVa4cB1Zd3GeqP3ChY#10336082? https://dev59.com/rGQn5IYBdhLWcg3wmH8s -> https://dev59.com/7m025IYBdhLWcg3wfmHw -> https://dev59.com/8GPVa4cB1Zd3GeqP3ChY#10336082? - GSerg
3
将公寓状态设置为STA是一个承诺,要发誓,希望去死。你没有遵守承诺,所以失败了。必须通过保持调度程序循环(Application.Run())来实现承诺。破坏承诺的一件事是该线程的SynchronizationContext.Current为空,这就是为什么等待在线程池线程上恢复的原因。此代码的另一个非常关键的问题是,必须保持线程处于活动状态,直到销毁所有COM对象,你无法预测何时会发生这种情况。https://dev59.com/U2Ei5IYBdhLWcg3wFooL#21684059 - Hans Passant
你可能想看一下这个链接:https://dev59.com/7Oo6XIcBkEYKwwoYKRDG#21371891,@ful-stackz。 - noseratio - open to work
1
感谢大家的回复!Hans,感谢你的解释。@noseratio,我发现你的答案非常有帮助!重新阅读我的问题,我发现我错过了一个重要的背景 - 应用程序实际上是作为Windows服务运行的。无论如何,有了提供的资源,我可以继续解决这个问题。再次感谢! - ful-stackz
2个回答

3

Hans已经解决了问题。从技术上讲,您的代码出现问题是因为没有SynchronizationContext await捕获。但即使您编写一个,也不足够。

这种方法的一个主要问题是,您的STA线程没有进行消息泵。STA线程必须泵送Win32消息队列,否则它们就不是STA线程。SetApartmentState(ApartmentState.STA)只是告诉运行时这是一个STA线程;它并不能使其成为STA线程。您必须为其提供消息泵才能使其成为STA线程。

你可以自己编写消息泵,但我不知道有谁勇敢地这样做。大多数人会从WinForms(如Hans的答案)WPF安装消息泵。也可能使用UWP消息泵来实现。
使用提供的消息泵的一个好处是它们还提供了一个SynchronizationContext(例如WinFormsSynchronizationContext/DispatcherSynchronizationContext),因此await可以自然地工作。另外,由于每个.NET UI框架都定义了一个“运行此委托”的Win32消息,底层的Win32消息队列也可以包含您想要排队到线程的所有工作,因此显式队列及其“运行器”代码不再必要。

1
我曾经尝试创建一个简单的STA消息泵,用于类似的COM使用场景。在我找到正确的Win32 API之后,它运行得相当不错。现在我感觉很勇敢 :) 令人惊讶的是,这些天人们仍然可能需要这样的东西。 - noseratio - open to work
感谢您的解释和提供的资源!重新阅读我的问题,我漏掉了一个非常重要的信息 - 应用程序实际上正在作为Windows服务运行。更重要的是,COM的开发人员(实际上是驱动程序库)已经更新了它,并且现在与MTA应用程序兼容。因此,这个练习变得毫无意义,但仍然具有教育意义!再次感谢大家! - ful-stackz

0
由于在 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();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接