LWChris
LWChris

Reputation: 4221

Is it correct/necessary to use UnsafeStart to launch a second STA thread to avoid UI context in C#?

Context:

My app has a "Paste" feature which triggers a file copy or move operation for copied/cut files. The code that does the eventual IO is a method within a static helper class and is called ClipboardOperations.Paste. It is currently called from an ICommand/DelegateCommand.

I had introduced an async layer to my long-running commands some time ago, including the paste command. That broke the paste operation, since it uses some Clipboard methods, and those use OLE calls, which require an STA thread. And as my asynchronous layer used Task.Run, the code was no longer run in an STA thread.

What I did as a quick fix was to use the UI thread's Dispatcher to shove the Paste back onto the UI thread, as it is a well-known example of an STA thread.


I was now asked if I could fix that the context menu stays open while the file operation is on-going (see also this question). So I am now thinking about creating a new STA thread that is specifically not the UI thread, like suggested in this answer to the same problem.

var threadStart = new ThreadStart(action);
var thread = new Thread(threadStart);
thread.SetApartmentState(ApartmentState.STA);
thread.Start(); 
thread.Join();

But I wonder: the given code uses Thread.Start. The remarks of Thread.UnsafeStart say

Unlike Start(), which captures the current ExecutionContext and uses that context to invoke the thread's delegate, UnsafeStart() explicitly avoids capturing the current context and flowing it to the invocation.

In my current case, I am always calling my ClipboardOperations.Paste method from an asynchronous method that was launched using Task.Run, so using the same ExecutionContext won't be the UI thread's context. But what if ClipboardOperations.Paste was called from the UI thread directly? Should/must I use Thread.UnsafeStart to make sure the code does never block the UI thread?

(Note: I am creating the thread inside ClipboardOperations.Paste, so I know the code does needs an STA thread but does not access UI elements.)

Upvotes: 1

Views: 96

Answers (1)

LWChris
LWChris

Reputation: 4221

I just realized it wasn't about Start vs UnsafeStart that determines whether the calling thread would be blocked during the IO operations. It was (in retrospect rather obviously) the question which thread was calling the final thread.Join();.

To make sure I'll never accidentally block the UI thread, I have created a helper method. And as Theodor Zoulias pointed out in the comments, you don't even have to block a thread using Thread.Join, if you use a TaskCompletionSource instead:

private static async Task RunInSTAThreadAsync(Action action)
{
    Task RunInSTAThread()
    {
        var tcs = new TaskCompletionSource();
        var threadStart = new ThreadStart(() =>
        {
            try
            {
                action();
                tcs.SetResult();
            }
            catch (Exception e)
            {
                tcs.SetException(e);
            }
        });
        var thread = new Thread(threadStart);
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();
        return tcs.Task;
    }

    await Task.Run(RunInSTAThread);
}

This can be used like so:

public static class ClipboardOperations
{
    public static async Task PasteAsync(string targetDir)
    {
        await RunInSTAThreadAsync(() => Paste(targetDir));
    }

    private static void Paste(string targetDir)
    {
        // OLE calls and IO here
    }

    private static async Task RunInSTAThreadAsync(Action action)
    {
        // see above
    }
}

This will ensure that the UI thread will not be blocked during the execution of Paste, regardless of whether the call to PasteAsync was already made from a background thread or the UI thread.

I verified both cases like so:

// Call from thread pool
await Task.Run(() => ClipboardOperations.PasteAsync(targetDir));

// Call from UI thread
// _dispatcher is the UI thread's dispatcher.
await _dispatcher.Invoke(() => ClipboardOperations.PasteAsync(targetDir));

Upvotes: 2

Related Questions