Почему Task.Delay нарушает STA-состояние потока?

2

Вступление

Это длинный вопрос! В начале вы найдете некоторую предысторию проблемы, затем примеры кода, которые были упрощены для представления, и Вопрос после этого. Пожалуйста, читайте в любом порядке, который вам подходит!

Исходная информация

Я пишу часть Proof-of-Concept для приложения для связи с STA COM. Эта часть приложения требует выполнения в контексте однопотоковой квартиры (STA) для связи с указанным STA COM. Остальная часть приложения работает в контексте MTA.

Текущее состояние

То, что я придумал до сих пор создает класс связи, который содержит while цикл, работающий в ГНА. Работа, которая должна быть передана в COM-объект, ставится в очередь извне в класс Communication через ConcurrentQueue. Затем рабочие элементы удаляются из цикла 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? Или я делаю что-то еще, что здесь не так?

Спасибо!

Спасибо, что нашли все это время, чтобы прочитать вопрос! Хорошего дня!

  • 1
    .ConfigureAwait(false) инструктирует .ConfigureAwait(false) контекст, что обычно означает, что код будет выполнен в другом потоке. Thread.Sleep блокирует текущий поток и не имеет ничего общего с асинхронными состояниями или состояниями квартиры.
  • 0
    .ConfigureAwait(true) достигает того же результата, что и ..(false) . Забыл добавить это к вопросу. Спасибо, что заметили!
Показать ещё 4 комментария
Теги:
multithreading
com
sta
mta

2 ответа

0

Ганс прибил это. Технически, ваш код ломается, потому что нет никакого SynchronizationContext захваченного await. Но даже если вы напишите один, этого будет недостаточно.

Одна большая проблема с этим подходом состоит в том, что ваш поток STA не качает. Потоки STA должны перекачивать очередь сообщений Win32, иначе они не являются потоками STA. SetApartmentState(ApartmentState.STA) просто сообщает среде выполнения, что это поток STA; это не делает это потоком STA. Вы должны качать сообщения, чтобы это было потоком STA.

Вы можете написать это сообщение самостоятельно, хотя я не знаю никого достаточно смелого, чтобы сделать это. Большинство людей устанавливают насос сообщений из WinForms (ответ от Ганса) или WPF. Это также может быть возможно сделать с помощью насоса сообщений UWP.

Одним приятным побочным эффектом использования предоставляемых насосов сообщений является то, что они также предоставляют SynchronizationContext (например, WinFormsSynchronizationContext/DispatcherSynchronizationContext), поэтому await работает естественным образом. Кроме того, поскольку каждая среда .NET UI определяет сообщение Win32 "запустить этого делегата", базовая очередь сообщений Win32 также может содержать всю работу, которую вы хотите поставить в очередь в своем потоке, поэтому явная очередь и ее код "бегущего" больше не являются необходимо.

  • 1
    Однажды я попытался создать простой насос сообщений STA для аналогичного сценария использования COM. Это сработало довольно хорошо после того, как я выяснил правильные Win32 API. Теперь я могу чувствовать себя смелым :) Удивленным людям все еще может понадобиться нечто подобное в эти дни.
  • 0
    Спасибо за объяснение и предоставленные ресурсы! Читая мой вопрос еще раз, я упустил довольно важную информацию - приложение фактически работает как служба Windows. Более того, разработчик COM, который на самом деле является библиотекой драйверов, обновил его, и теперь он совместим с приложениями MTA. Таким образом, делая это упражнение совершенно бесполезным, но, тем не менее, образовательным! Еще раз спасибо, ребята!
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();

Ещё вопросы

Сообщество Overcoder
Наверх
Меню