Task.Run from UI thread throws STA error

2019-07-13 20:19发布

问题:

While I was refactoring some old C# code for document generation with Office.Interop library I found this and because of it was using UI context when function were called from it it was blocking it

Example

private void btnFooClick(object sender, EventArgs e)
{
      bool documentGenerated = chckBox.Checked ? updateDoc() : newDoc();

      if(documentGenerated){
        //do something
      }
}

Decided to change it like that to reduce from blocking UI

private async void btnFooClick(object sender, EventArgs e)
{
      bool documentGenerated; = chckBox.Checked ? updateDoc() : newDoc();

      if(chckBox.Checked)
      {
                documentGenerated = await Task.Run(() => updateDoc()).ConfigureAwait(false);
      }
      else
      {
                documentGenerated = await Task.Run(() => newDoc()).ConfigureAwait(false);
      }

      if(documentGenerated){
        //do something
      }
}

And it was throwing such error

Current thread must be set to single thread apartment (STA) mode before OLE calls can be made

Why does it happen and what is possible workaround?

回答1:

The COM components accessed through Interop require the calling thread to be a STA thread but in your case it is not STA. Using STA the component could be accessed through multiple threads. You can read more about why STA is required in Understanding and Using COM Threading Models You can make a extention method on Task class as suggested in this answer to call the COM component through Interop using task.

public static Task<T> StartSTATask<T>(Func<T> func)
{
    var tcs = new TaskCompletionSource<T>();
    Thread thread = new Thread(() =>
    {
        try
        {
            tcs.SetResult(func());
        }
        catch (Exception e)
        {
            tcs.SetException(e);
        }
    });
    thread.SetApartmentState(ApartmentState.STA);
    thread.Start();
    return tcs.Task;
}

You can also use Thread instead of task, you have to set the ApartmentState to STA like thread.SetApartmentState(ApartmentState.STA)



回答2:

Because in this case Task presumably starts a new thread that isn't an STA thread. Your calls to updateDoc and newDoc are the ones that call the Interop layer, which doesn't like MTA threads.

You could refactor this to use Thread instead of Task and set the apartment to STA by yourself. I would be careful though, because I am not sure Interop likes multi-threading.