【问题标题】:Why is Task.Delay breaking the STA state of the thread?为什么 Task.Delay 会破坏线程的 STA 状态?
【发布时间】:2019-09-09 17:17:30
【问题描述】:

简介

这是一个很长的问题!您会在开始时找到问题的一些背景知识,然后是代码示例,这些示例已被简化以用于表示,然后是问题。请按照您认为适合您的任何顺序阅读!

背景资料

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

当前状态

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

代码上下文

通讯类

这是一个static 类,包含一个循环,旨在在 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
}

所有者

这是拥有生成Communication与COM的类的示例,是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 状态?还是我在这里做错了什么?

谢谢!

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

【问题讨论】:

  • .ConfigureAwait(false) 指示 await 不捕获上下文,这通常意味着代码将在另一个线程上执行。 Thread.Sleep 阻塞当前线程,与异步或单元状态无关。
  • .ConfigureAwait(true) 实现与..(false) 相同的结果。忘了把它添加到问题中。感谢您的关注!
  • 将公寓状态设置为STA是一个承诺,穿越你的心,希望死。你没有遵守诺言,所以你死了。您必须通过维护调度程序循环 Application.Run() 来保留它。违反承诺并没有发生的一件事是该线程的 SynchronizationContext.Current 为空,这就是为什么在线程池线程上恢复等待的原因。此代码的另一个非常关键的问题是,您必须保持线程处于活动状态,直到所有 COM 对象都被销毁,您无法预测何时会发生这种情况。 stackoverflow.com/a/21684059/17034
  • 感谢大家的意见!汉斯,谢谢你的解释。 @noseratio 我发现您的回答最有帮助!再次阅读我的问题,我发现我错过了很大一部分背景——该应用程序实际上是作为 Windows 服务运行的。无论如何,通过提供的资源,我可以重新解决问题。再次感谢!

标签: c# multithreading com sta mta


【解决方案1】:

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

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

您可以自己编写该消息泵,尽管我不知道有谁有足够的勇气这样做。大多数人从WinForms (a la Hans' answer)WPF 安装消息泵。也可以使用UWP message pump 来执行此操作。

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

【讨论】:

  • 我曾经尝试为类似的 COM 使用场景创建一个简单的STA message pump。在我找出正确的 Win32 API 之后,它运行得相当好。现在我可以感到勇敢了 :) 惊讶的人现在可能仍然需要这样的东西。
  • 感谢您的解释和提供的资源!再次阅读我的问题,我遗漏了相当重要的信息——该应用程序实际上是作为 Windows 服务运行的。更重要的是,COM(实际上是一个驱动程序库)的开发人员已经对其进行了更新,现在它与 MTA 应用程序兼容。因此,使这项练习变得毫无用处,但仍然具有教育意义!再次感谢你们!
【解决方案2】:

因为在 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();

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-03-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-07-20
    • 2023-03-24
    • 1970-01-01
    相关资源
    最近更新 更多