I'm getting confusing behavior when using a different SynchronizationContext inside an async function than outside.
Most of my program's code uses a custom SynchronizationContext that simply queues up the SendOrPostCallbacks and calls them at a specific known point in my main thread. I set this custom SynchronizationContext at the beginning of time and everything works fine when I only use this one.
The problem I'm running into is that I have functions that I want their await continuations to run in the thread pool.
void BeginningOfTime() {
// MyCustomContext queues each endOrPostCallback and runs them all at a known point in the main thread.
SynchronizationContext.SetSynchronizationContext( new MyCustomContext() );
// ... later on in the code, wait on something, and it should continue inside
// the main thread where MyCustomContext runs everything that it has queued
int x = await SomeOtherFunction();
WeShouldBeInTheMainThreadNow(); // ********* this should run in the main thread
}
async int SomeOtherFunction() {
// Set a null SynchronizationContext because this function wants its continuations
// to run in the thread pool.
SynchronizationContext prevContext = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext( null );
try {
// I want the continuation for this to be posted to a thread pool
// thread, not MyCustomContext.
await Blah();
WeShouldBeInAThreadPoolThread(); // ********* this should run in a thread pool thread
} finally {
// Restore the previous SetSynchronizationContext.
SynchronizationContext.SetSynchronizationContext( prevContext );
}
}
The behavior I'm getting is that the code right after each await is executed in a seemingly-random thread. Sometimes, WeShouldBeInTheMainThreadNow() is running in a thread pool thread and sometimes the main thread. Sometimes WeShouldBeInAThreadPoolThread() is running
I don't see a pattern here, but I thought that whatever SynchronizationContext.Current was set to at the line where you use await is the one that will define where the code following the await will execute. Is that an incorrect assumption? If so, is there a compact way to do what I'm trying to do here?
I would expect your code to work, but there are a few possible reasons why it's not:
SynchronizationContext
is current when it executes its continuations.SynchronizationContext
is captured.SynchronizationContext
is to establish the current one in one method, and then run another (possibly-asynchronous) method that depends on it.SynchronizationContext
is to appendConfigureAwait(false)
to all tasks that are awaited.There is a common misconception about
await
, that somehow calling anasync
-implemented function is treated specially.However, the
await
keyword operates on an object, it does not care at all where the awaitable object comes from.That is, you can always rewrite
await Blah();
withvar blahTask = Blah(); await blahTask;
So what happens when you rewrite the outer
await
call that way?And then, there is the other issue: The
finally
from the inner method is executed in the continuation, meaning that it is executed on the thread pool - so not only you have unset yourSynchronizationContext
, but yourSynchronizationContext
will (potentially) be restored at some time in the future, on another thread. However, because I do not really understand the way that theSynchronizationContext
is flowed, it is quite possible that theSynchronizationContext
is not restored at all, that it is simply set on another thread (remember thatSynchronizationContext.Current
is thread-local...)These two issues, combined, would easily explain the randomness that you observe. (That is, you are manipulating quasi-global state from multiple threads...)
The root of the issue is that the
await
keyword does not allow scheduling of the continuation task.In general, you simply want to specify "It is not important for the code after the
await
to be on the same context as the code beforeawait
", and in that case, usingConfigureAwait(false)
would be appropriate;However, if you absolutely want to specify "I want the code after the
await
to run on the thread pool" - which is something that should be rare, then you cannot do it withawait
, but you can do it e.g. withContinueWith
- however, you are going to mix multiple ways of usingTask
objects, and that can lead to pretty confusing code.