4

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.)

antak
  • 19,481
  • 9
  • 72
  • 80
  • What about implementing custom TaskScheduler by copying logic of existing SynchronizationContextTaskScheduler? Copying is not required of course, the logic itself looks quite simple. – Evk May 30 '16 at 11:47
  • @Evk There's a few `internal`s you can't use, but the main idea is still there - you need your own `TaskScheduler`. – Luaan May 30 '16 at 11:55
  • 1
    Why would you want to post a task to the synchronization context? The usual use of synchronization contexts is starting in the synchronization context. – Paulo Morgado May 30 '16 at 13:01
  • @antak: If you just want a single-threaded syncctx/taskfactory, then you may find my [AsyncContextThread](http://dotnetapis.com/pkg/Nito.AsyncEx.Context/1.0-beta-1/net46/doc/Nito.AsyncEx.AsyncContextThread) type helpful. – Stephen Cleary May 30 '16 at 14:34

2 Answers2

6

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(...);
Evk
  • 98,527
  • 8
  • 141
  • 191
  • Could you also add an example of how to use this, considering the question was how to add tasks, not how to write a taskscheduler? – default May 30 '16 at 12:07
  • 1
    This looks great! I'm wondering why they don't offer a `TaskScheduler.FromSynchronizationContext(SynchronizationContext)`, and if it's because there's some design issue with allowing any old synchronization context, but I can't think of any. (edit: deleted noise) – antak May 30 '16 at 12:34
  • @antak, It also says "The returned enumerable should never be null. If there are currently no queued tasks, an empty enumerable should be returned instead." so seems indeed actual implementation violates this. As for why they don't offer - propably they didn't find any use for this. Note their implementation of SynchronizationContextTaskScheduler also do not have a way to initialize for non-current context, so they themselves did not use it, even internally – Evk May 30 '16 at 12:42
  • @Evk Yeah, I deleted that part about `GetScheduledTasks()` from my comments because the docs weren't specific either way about the not-implemented case and because reference source had callers that were checking for `null`s. – antak May 30 '16 at 13:02
3

Parallel Extensions Extras contains SynchronizationContextTaskScheduler which does exactly what you want.

If you don't want to compile PEE yourself, there is an unofficial NuGet package for it.

Note that you generally shouldn't need to do this and the fact that you're asking for this might indicate a flaw in your design.

svick
  • 236,525
  • 50
  • 385
  • 514
  • Nice to know this use-case is supported at least indirectly. *you generally shouldn't need to do this*: Say I have a highly customized task dispatcher (a single thread with a task invoking loop at its core) to which I want to (possibly retroactively) add *async* / *await* support. The natural progression seems to be, implement `SynchronizationContext` -> `TaskScheduler` -> `TaskFactory`. Is there a better way? (Or were you saying the need to create a custom dispatcher that may be the possible issue?) – antak May 31 '16 at 01:21
  • @antak Why do you even need that `SynchronizationContext`? Wouldn't a custom `TaskScheduler` be enough? – svick May 31 '16 at 01:29
  • I was excited for a moment as I thought I could set `TaskScheduler.Current` instead of `syncctx.Current` to [get *await*s to resume in the same thread](https://msdn.microsoft.com/magazine/gg598924.aspx). However, `TaskScheduler.Current` [appears to be something quite different](http://stackoverflow.com/a/23072503/1036728) and isn't some thread-local that I can set. If everything the dispatcher does comes through the `TaskScheduler` then this probably isn't a problem, but the moment it invokes a routine outside of a `Task`, then *await*s start to break. Is there something I'm missing? – antak May 31 '16 at 02:17