Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I create Tasks that are posted to a given SynchronizationContext?

Tags:

c#

async-await

Given a SynchronizationContext, which I already have (and is basically a window to a specific thread), how do I create Tasks that are posted to this context?

For reference, here's a very basic demonstration of how the SynchronizationContext is set up.

public class SomeDispatcher : SynchronizationContext
{
    SomeDispatcher() {

        new Thread(() => {

            SynchronizationContext.SetSynchronizationContext(this);

            // Dispatching loop (among other things)

        }).Start();
    }

    override void Post(SendOrPostCallback d, object state)
    {
        // Add (d, state) to a dispatch queue;
    }
}

This works fine for async / awaits that are already running in the context.

Now, I want to be able to post Tasks to this from an outside context (e.g. from a UI thread) but can't seem to find a clean way of doing this.

One way to do this is by using TaskCompletionSource<>.

Task StartTask(Action action)
{
    var tcs = new TaskCompletionSource<object>();
    SaidDispatcher.Post(state => {
        try
        {
            action.Invoke();
            tcs.SetResult(null);
        }
        catch (Exception ex)
        {
            tcs.SetException(ex);
        }
    });
    return tcs.Task;
});

But this is reinventing the wheel and a major pain supporting variations such as StartNew(Func<TResult>), StartNew(Func<Task<TResult>>), etc.

A TaskFactory interface to the SynchronizationContext is probably ideally, but I can't seem to instantiate one cleanly:

TaskFactory CreateTaskFactory()
{
    var original = SynchronizationContext.Current;
    SynchronizationContext.SetSynchronizationContext(SomeDispatcher); // yuck!
    try
    {
        return new TaskFactory(TaskScheduler.FromCurrentSynchronizationContext());
    }
    finally
    {
        SynchronizationContext.SetSynchronizationContext(original);
    }
}

(i.e. Having to temporary hose the current thread's synchronization context seems hacky.)

like image 459
antak Avatar asked Jan 07 '23 00:01

antak


1 Answers

It seems default SynchronizationContextTaskScheduler is

  1. Internal
  2. Only works with current synchronization context

But it's source code is available here and we see it's relatively simple, so we can try to roll out our own scheduler, like this:

public sealed class MySynchronizationContextTaskScheduler : TaskScheduler {
    private readonly SynchronizationContext _synchronizationContext;

    public MySynchronizationContextTaskScheduler(SynchronizationContext context) {
        _synchronizationContext = context;
    }

    [SecurityCritical]
    protected override void QueueTask(Task task) {
        _synchronizationContext.Post(PostCallback, task);
    }

    [SecurityCritical]
    protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) {
        if (SynchronizationContext.Current == _synchronizationContext) {
            return TryExecuteTask(task);
        }
        else
            return false;
    }

    [SecurityCritical]
    protected override IEnumerable<Task> GetScheduledTasks() {
        return null;
    }

    public override Int32 MaximumConcurrencyLevel
    {
        get { return 1; }
    }

    private void PostCallback(object obj) {
        Task task = (Task) obj;
        base.TryExecuteTask(task);
    }
}

Then your CreateTaskFactory becomes:

TaskFactory CreateTaskFactory() {
    return new TaskFactory(new MySynchronizationContextTaskScheduler(SomeDispatcher));
}

And you create tasks with:

var factory = CreateTaskFactory();
var task = factory.StartNew(...);
like image 88
Evk Avatar answered Jan 27 '23 07:01

Evk