19

Say you're writing a custom single threaded GUI library (or anything with an event loop). From my understanding, if I use async/await, or just regular TPL continuations, they will all be scheduled on TaskScheduler.Current (or on SynchronizationContext.Current).

The problem is that the continuation might want to access the single threaded parts of the library, which means it has to execute in the same event loop. For example, given a simple game loop, the events might be processed like this:

// All continuation calls should be put onto this queue
Queue<Event> events;

// The main thread calls the `Update` method continuously on each "frame"
void Update() {
    // All accumulated events are processed in order and the queue is cleared
    foreach (var event : events) Process(event);

    events.Clear();
}

Now given my assumption is correct and TPL uses the SynchronizationContext.Current, any code in the application should be able to do something like this:

async void Foo() {
    someLabel.Text = "Processing";

    await BackgroundTask();

    // This has to execute on the main thread
    someLabel.Text = "Done";
}

Which brings me to the question. How do I implement a custom SynchronizationContext that would allow me to handle continuations on my own thread? Is this even the correct approach?

Jakub Arnold
  • 85,596
  • 89
  • 230
  • 327

1 Answers1

16

Implementing a custom SynchronizationContext is not the easiest thing in the world. I have an open-source single-threaded implementation here that you can use as a starting point (or possibly just use in place of your main loop).

By default, AsyncContext.Run takes a single delegate to execute and returns when it is fully complete (since AsyncContext uses a custom SynchronizationContext, it is able to wait for async void methods as well as regular async/sync code).

AsyncContext.Run(async () => await DoSomethingAsync());

If you want more flexibility, you can use the AsyncContext advanced members (these do not show up in IntelliSense but they are there) to keep the context alive until some external signal (like "exit frame"):

using (var context = new AsyncContext())
{
  // Ensure the context doesn't exit until we say so.
  context.SynchronizationContext.OperationStarted();

  // TODO: set up the "exit frame" signal to call `context.SynchronizationContext.OperationCompleted()`
  // (note that from within the context, you can alternatively call `SynchronizationContext.Current.OperationCompleted()`

  // Optional: queue any work you want using `context.Factory`.

  // Run the context; this only returns after all work queued to this context has completed and the "exit frame" signal is triggered.
  context.Execute();
}

AsyncContext's Run and Execute replace the current SynchronizationContext while they are running, but they save the original context and set that as current before returning. This allows them to work nicely in a nested fashion (e.g., "frames").

(I'm assuming by "frame" you mean a kind of WPF-like dispatcher frame).

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Looks like you might be solving a more complicated problem than mine. I'm not doing any nesting, by frame I meant just a single call to render, rendering a single frame on a canvas. I wouldn't have any other synchronization context, just the one for the whole program. Which I guess should simplify things a bit? – Jakub Arnold Sep 01 '16 at 13:52
  • @JakubArnold: Yes, you can just ignore the nesting part then. You'd still need to use `OperationStarted` to keep the main loop going until you get an "exit program" request. – Stephen Cleary Sep 01 '16 at 13:53
  • But that's assuming the SynchronizationContext runs the loop, right? If I wanted to manually call onto the context to process its queue, wouldn't it be enough to override just `Post` to store into a queue? – Jakub Arnold Sep 01 '16 at 14:54
  • 2
    `SynchronizationContext` never runs a loop;. If you want to build your own, you should override both `Post` and `Send`. – Stephen Cleary Sep 01 '16 at 15:58