4
\$\begingroup\$

I've figured out a way to switch threads in the middle of a method. The benefit of that seems super obvious to me: that's what everyone wants to do anyway, and splitting up methods, using callbacks, or lambdas, or whatever is just code bureaucracy to make it work.

But if this is so obvious to me, Microsoft guys probably thought of that too, yet didn't implement it as the default way of doing things in TPL. This makes me nervous. These custom awaiters seem to run correctly, but maybe I just haven't hit the gotchas yet.

What could possibly go wrong?

public static class UsageTest
{
    public static async Task DoItTheUsualWay()
    {
        // update UI
        Dictionary<string, string> cantUseVar = null; // have to initialize
        await Task.Run(() =>
        {
            // do heavy lifting
            var cantAccessVarOutsideTheLambda = new object();
            cantUseVar = new Dictionary<string, string>();
        });
        // update UI
    }

    public static async Task UseTheCoolNewAwaiters()
    {
        // update UI
        await GoToThreadPool.Instance;
        // do heavy lifting
        var smoothSailing = new object();
        await GoToMainThread.Instance;
        // update UI
        smoothSailing.ToString();
    }
}

public class GoToThreadPool : INotifyCompletion
{
    public static readonly GoToThreadPool Instance = new GoToThreadPool();

    public bool IsCompleted
    {
        get { return Thread.CurrentThread.IsThreadPoolThread; }
    }

    public GoToThreadPool GetAwaiter()
    {
        return this;
    }

    public void GetResult() { }

    public void OnCompleted(Action continuation)
    {
        ThreadPool.QueueUserWorkItem(o => continuation());
    }
}

public class GoToMainThread : INotifyCompletion
{
    public static readonly GoToMainThread Instance = new GoToMainThread();

    public bool IsCompleted
    {
        get { return Thread.CurrentThread == Application.Current.Dispatcher.Thread; }
    }

    public GoToMainThread GetAwaiter()
    {
        return this;
    }

    public void GetResult() { }

    public void OnCompleted(Action continuation)
    {
        Application.Current.Dispatcher.BeginInvoke(continuation);
    }
}
\$\endgroup\$
3
\$\begingroup\$

What you are after is yielding to a different thread, which has been covered in this question. One difference is that Andrew's code implements ICriticalNotifyCompletion vs you're implementing just INotifyCompletion. I haven't found any good documentation which one to prefer and what Critical exactly means. It may mean that his code works in some weird corner case scenarios where yours might not - hard to say. Most articles state that it's optional to implement the critical interface so it may not matter.

The other difference is that Andrew's code doesn't provide an option to switch back plus he's using a struct while you are using a class (which is just a potential performance optimization)

The main conclusion was that Microsoft didn't provide this out of the box since they felt it was prone to abuse.

In general I prefer Andrew's usage pattern of TaskEx.YieldToThreadPoolThread() over your more direct await GoToThreadPool.Instance since it's slightly more readable.

So combining the two implementations one could end up with something like this:

public static class TaskEx
{
    public static YieldToThreadAwaitable YieldToThreadPool()
    {
        return new YieldToThread(null);
    }

    public static YieldToThreadAwaitable YieldToMainThread()
    {
        return new YieldToThread(Application.Current.Dispatcher.Thread);
    }

    public static YieldToThreadAwaitable YieldToThread(Thread target)
    {
        return new YieldToThread(target);
    }
}

public class YieldToThreadAwaitable : INotifyCompletion
{
    private Thread _Target;

    public YieldToThreadAwaitable(Thread target)
    {
        Target = target;
    }

    public bool IsCompleted
    {
        get { return _Target != null ? Thread.CurrentThread == _Target : Thread.CurrentThread.IsThreadPoolThread; }
    }

    public YieldToThreadAwaitable GetAwaiter()
    {
        return this;
    }

    public void GetResult() { }

    public void OnCompleted(Action continuation)
    {
        if (_Target == null)
        {
            ThreadPool.QueueUserWorkItem(o => continuation());
        }
        else
        {
            Dispatcher.FromThread(_Target).BeginInvoke(continuation);
        }
    }
}
| improve this answer | |
\$\endgroup\$
2
\$\begingroup\$

What could possibly go wrong?

Stephen Cleary has a good explanation of that on SO. In short using this approach makes your code smelly, and it also means that catch and finally blocks could run in either context, which makes them harder to write correctly (assuming you even realize this is a problem).

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.