Search code examples
c#multithreadingcomstamta

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


Introduction

This is a lengthy question! You will find some background on the problem at the beginning, then code samples, which have been simplified for representation and the Question after that. Please read in any order that you find good for you!

Background information

I am writing a Proof-of-Concept part for an application for communicating with an STA COM. This part of the application has the requirement of running in a Single-Threaded Apartment (STA) context in order to communicate with said STA COM. The rest of the application runs in a MTA context.

Current state

What I have come up with so far is creating a Communication class that contains a while loop, running in a STA. The work that needs to be relayed to the COM object is queued from the outside to the Communication class via ConcurrentQueue. The work items are then dequeued in the while loop and the work is performed.

Code context

Communication class

This is a static class, containing a loop that is intended to run in STA state and check if some work needs to be done by the COM and dispatch the work to the handler.

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
}

Work command

This class describes the work that needs to be done and any arguments that might be provided.

enum WorkCommand
{
    Initialize
}

Work unit

This enumeration defines the various tasks that can be performed by the 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
}

Owner

This is a sample of the class that owns or spawns the Communication with the COM and is an abstraction over the Communication for use in the rest of the application.

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
}

The problem

Now, take a look at the Communication.StartCommunication() method, more specifically this part:

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

If this line is substituted with the following:

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

during inspection the final stop - Communication.InternalInitializeCOM() the apartment of the thread seems to be MTA.

However, if the delaying logic is changed to

Thread.Sleep(100);

the CommunicationInternalInitializeCOM() method seems to be executed in a STA state.

The inspection was done by Thread.CurrentThread.GetApartmentState().

The Question

Can anyone explain to me why does Task.Delay break the STA state? Or am I doing something else that is wrong here?

Thank you!

Thank you for taking all this time to read the question! Have a great day!


Solution

  • Hans has nailed it. Technically, your code is breaking because there's no SynchronizationContext captured by the await. But even if you write one, it won't be enough.

    The one big problem with this approach is that your STA thread isn't pumping. STA threads must pump a Win32 message queue, or else they're not STA threads. SetApartmentState(ApartmentState.STA) is just telling the runtime that this is an STA thread; it doesn't make it an STA thread. You have to pump messages for it to be an STA thread.

    You can write that message pump yourself, though I don't know of anyone brave enough to have done this. Most people install a message pump from WinForms (a la Hans' answer) or WPF. It may also be possible to do this with a UWP message pump.

    One nice side effect of using the provided message pumps is that they also provide a SynchronizationContext (e.g., WinFormsSynchronizationContext / DispatcherSynchronizationContext), so await works naturally. Also, since every .NET UI framework defines a "run this delegate" Win32 message, the underlying Win32 message queue can also contain all the work you want to queue to your thread, so the explicit queue and its "runner" code is no longer necessary.