HiveBrain v1.2.0
Get Started
← Back to all entries
patterncsharpMinor

Going to the thread pool and back using custom awaiters

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
thegoingbackcustomthreadusingandpoolawaiters

Problem

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 cantUseVar = null; // have to initialize
await Task.Run(() =>
{
// do heavy lifting
var cantAccessVarOutsideTheLambda = new object();
cantUseVar = new Dictionary();
});
// 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()
{

Solution

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);
        }
    }
}

Code Snippets

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);
        }
    }
}

Context

StackExchange Code Review Q#119792, answer score: 3

Revisions (0)

No revisions yet.