1

I am running some code in a separate thread, which might throw an exception (after all, code tends to do this). The thread will be spawned from the main thread (GUI) so this is where the exceptions will eventually have to be handled (like setting an error message text block). I have two solutions, but neither allows direct catching of exceptions in the GUI thread.

Note: I cannot use stuff like Task and BackgroundWorker (at least not out of the box) as I need to be able to change the ApartmentState of the thread.

Here is what I would like:

var thread = new Thread(() =>
{
    // Code which can throw exceptions
});

try 
{
    thread.SetApartmentState(ApartmentState.STA);
    thread.Start();
    MethodThatAwaitsThread(thread);
}
catch
{
    // Exception handling in the GUI thread
}

This does not work, as the exception never leaves the thread. I am aware that it cannot leave the thread at any time, but I am fine with waiting for the thread to end, and then catch it.

Here is my current solution, which utilizes the Dispatcher to talk to the GUI thread:

var thread = new Thread(() => 
{
    try 
    {
        // Code which can throw exceptions
        Dispatcher.Invoke(UpdateGuiAsSuccess);
    }
    catch (Exception ex)
    {
        Dispatcher.Invoke(UpdateGuiAsError);
    }
}

An alternative solution is to store the Exception in an object and then explicitely check for it afterwards. But this comes at a risk of people forgetting to check the exception:

Exception ex = null;    

var thread = new Thread(() => 
{
    try 
    {
        // Code which can throw exceptions            
    }
    catch (Exception threadEx)
    {
        ex = threadEx;
    }
}

if (ex != null) 
{
    UpdateGuiAsError();
}
else 
{
    UpdateGuiAsSuccess();
}

Is there anyway I can get the error to be re-thrown in the GUI thread, once the worker thread dies?

Jakob Busk Sørensen
  • 5,599
  • 7
  • 44
  • 96
  • I didn't read it fully but it was worth an upvote given the prep and detail. Such refreshing change :) you might find THIS handy: https://stackoverflow.com/q/30326673/495455 – Jeremy Thompson Nov 15 '19 at 09:27
  • Also, get the VS addin: Single Debug Mode – Jeremy Thompson Nov 15 '19 at 09:30
  • 1
    "*I cannot use stuff like Task*" is wrong. You just haven't searched StackOverflow for `[c#] Task STA` – Clemens Nov 15 '19 at 09:46
  • Possible duplicate: [Set ApartmentState on a Task](https://stackoverflow.com/questions/16720496/set-apartmentstate-on-a-task) – Theodor Zoulias Nov 15 '19 at 13:19
  • Another solution (much more verbose but also more in the spirit of the TPL origins): [ParallelExtensionsExtras Tour – #5 – StaTaskScheduler](https://devblogs.microsoft.com/pfxteam/parallelextensionsextras-tour-5-stataskscheduler/) – Theodor Zoulias Nov 15 '19 at 13:23

2 Answers2

5

You can use Task with an STA thread (which I assume is what you want).

To do so, you can write some helper methods to start a task on a thread that has been set to STA:

public static class STATask
{
    /// <summary>
    /// Similar to Task.Run(), except this creates a task that runs on a thread
    /// in an STA apartment rather than Task's MTA apartment.
    /// </summary>
    /// <typeparam name="TResult">The return type of the task.</typeparam>
    /// <param name="function">The work to execute asynchronously.</param>
    /// <returns>A task object that represents the work queued to execute on an STA thread.</returns>

    [NotNull] public static Task<TResult> Run<TResult>([NotNull] Func<TResult> function)
    {
        var tcs = new TaskCompletionSource<TResult>();

        var thread = new Thread(() =>
        {
            try
            {
                tcs.SetResult(function());
            }

            catch (Exception e)
            {
                tcs.SetException(e);
            }
        });

        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();

        return tcs.Task;
    }

    /// <summary>
    /// Similar to Task.Run(), except this creates a task that runs on a thread
    /// in an STA apartment rather than Task's MTA apartment.
    /// </summary>
    /// <param name="action">The work to execute asynchronously.</param>
    /// <returns>A task object that represents the work queued to execute on an STA thread.</returns>

    [NotNull] public static Task Run([NotNull] Action action)
    {
        var tcs = new TaskCompletionSource<object>(); // Return type is irrelevant for an Action.

        var thread = new Thread(() =>
        {
            try
            {
                action();
                tcs.SetResult(null); // Irrelevant.
            }

            catch (Exception e)
            {
                tcs.SetException(e);
            }
        });

        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();

        return tcs.Task;
    }
}

Once you have that, you can easily create an STA task and then use .ContinueWith() to handle exceptions thrown in the task, or use await to catch the exceptions.

(Note: [NotNull] is from Resharper annotations - remove them if you're not using Resharper.)

Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • See also this answer: https://stackoverflow.com/a/16722767/1136211 – Clemens Nov 15 '19 at 09:44
  • This seems quite clever, at least it works as intended. I don't really get what causes the method to wait for the thread to complete, before returning. Is it possible to add some very short comments on how this happens, in your code? :-) – Jakob Busk Sørensen Nov 15 '19 at 10:12
  • 1
    @Noceo The `Run()` methods don't actually wait - they return a "hot" task that is already started, but may or may not be completed when it's returned. It's the *caller* that waits for the task to be completed. Inside the `Run()` implementation, a new thread is started which executes your action/func and then calls `TaskCompletionSource,SetResult()` to indicate that the task is complete. – Matthew Watson Nov 15 '19 at 10:50
  • @Noceo It's actually the calls to `await SomeAsyncMethod();` that wait for the task to complete (and rethrow any exceptions). – Matthew Watson Nov 15 '19 at 10:59
1

ExceptionDispatchInfo is a good solution for rethrowing exceptions in a different thread since it preserves the original stacktrace.

ExceptionDispatchInfo exceptionInfo = null;

var thread = new Thread(() =>
{
    try
    {
        // Code which can throw exceptions
    }
    catch (Exception ex)
    {
        exceptionInfo = ExceptionDispatchInfo.Capture(ex);
    }
});

try 
{
    thread.SetApartmentState(ApartmentState.STA);
    thread.Start();
    MethodThatAwaitsThread(thread);
    exceptionInfo?.Throw();
}
catch
{
    // Exception handling in the GUI thread
}
max
  • 466
  • 3
  • 7