4

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!

ful-stackz
  • 302
  • 4
  • 10
  • 1
    `.ConfigureAwait(false)` instructs await to not capture context, which usually means the code will be executed on another thread. `Thread.Sleep` blocks the current thread and has nothing to do with async or apartment states. – GSerg Apr 19 '19 at 12:29
  • `.ConfigureAwait(true)` achieves the same result as `..(false)`. Forgot to add that to the question. Thank you for noticing! – ful-stackz Apr 19 '19 at 12:30
  • https://stackoverflow.com/q/16720496/11683 -> https://stackoverflow.com/q/5971686/11683 -> https://stackoverflow.com/a/10336082/11683? – GSerg Apr 19 '19 at 12:38
  • 3
    Setting the apartment state to STA is a promise, cross your heart, hope to die. You didn't keep your promise, so you died. You must keep it by maintaining a dispatcher loop, Application.Run(). One thing that didn't happen by breaking the promise is that the SynchronizationContext.Current for that thread is null, that's why the await resumed on a threadpool thread. Another very critical problem with this code is that you must keep the thread alive until all COM objects are destroyed, you can't predict when that happens. https://stackoverflow.com/a/21684059/17034 – Hans Passant Apr 19 '19 at 13:45
  • You may want to check [this](https://stackoverflow.com/a/21371891/1768303) out, @ful-stackz. – noseratio Apr 22 '19 at 10:30
  • 1
    Thank you all for the input! Hans, thank you for the explanation. @noseratio I've found your answer most helpful! Reading my question again and I see that I've missed a big part of the background - the application is actually running as a Windows service. Anyway, with the provided resources I can get back to tackling the problem. Thanks again! – ful-stackz Apr 23 '19 at 06:20

2 Answers2

3

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.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 1
    I once made an attempt to create a simple [STA message pump](https://stackoverflow.com/a/21371891/1768303) for a similar COM usage scenario. It worked rather well after I figured out the right Win32 APIs. Now I can feel brave :) Surprised people still may need something like that these days. – noseratio Apr 22 '19 at 10:27
  • Thank you for the explanation and the provided resources! Reading my question again, I've left out quite an important bit of information - the application is actually running as a Windows service. What is more, the developer of the COM, which is actually a driver library, has updated it and it is now compatible with MTA applications. Thus rendering this exercise quite useless, but nevertheless educational! Thank you again guys! – ful-stackz Apr 23 '19 at 06:26
0

Because after await Task.Delay() statement , your code runs inside one of the ThreadPool thread, and since the ThreadPool threads are MTA by design.

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();
Rahmat Anjirabi
  • 868
  • 13
  • 16