2

Using Visual Studio 2015 Update 3 and a C# Test project targeting .NET 4.6.1 I get the following behavior:

[TestClass]
public class AwaitTests
{
    [TestMethod]
    public void AsyncRemovingSyncContext_PartialFail()
    {
        Log("1");
        SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
        Log("2");
        HasAwait().Wait(); // The continuation in this method is on wrong thread
        Log("5");
        Assert.IsNotNull(SynchronizationContext.Current);
    }

    [TestMethod]
    public async Task AsyncRemovingSyncContext_Fail()
    {
        Log("1");
        SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
        Log("2");
        await HasAwait();
        Log("5"); // Issue is here - Sync Context is now null
        Assert.IsNotNull(SynchronizationContext.Current);
    }

    public async Task HasAwait()
    {
        Log("3");
        await Task.Delay(300);
        Log("4");
    }

    private void Log(string text)
    {
        Console.WriteLine($"{text} - Thread {System.Threading.Thread.CurrentThread.ManagedThreadId} - {SynchronizationContext.Current}");
    }
}

Here is the output:

AsyncRemovingSyncContext_PartialFail
1 - Thread 7 -
2 - Thread 7 - System.Threading.SynchronizationContext
3 - Thread 7 - System.Threading.SynchronizationContext
4 - Thread 8 -
5 - Thread 7 - System.Threading.SynchronizationContext

AsyncRemovingSyncContext_Error
1 - Thread 7 -
2 - Thread 7 - System.Threading.SynchronizationContext
3 - Thread 7 - System.Threading.SynchronizationContext
4 - Thread 8 -
5 - Thread 8 -
-Assert Exception Thrown-

I've done other tests and so far there is a 100% correlation between the existence of the await keyword in the method and the clearing of the Sync Context. This includes async lambdas.

This is important to me because it appears that as soon as an await is encountered, the sync context is removed. This means that if I perform two awaits in the same method, the second continuation will just run in the thread pool (default behavior when no Sync Context).

Is this a framework/compiler bug or am I doing something wrong?


For specifics, since I'm sure someone will ask in one form or another, I have an Active Object that I would like to enable async \ await support for, but I can only do that if I can guarentee that the continuation will be dispatched to my ActiveObjectSynchronizationContext, which at the moment, it's not because it's being cleared.

I've already looked at this question (and similar about the UI context 4.0 bug) but this isn't related since I'm running 4.6.1 and I'm using non-UI threads.

I also followed the advice from this other question and ensured my Sync Context implements CreateCopy, but I can tell from test profiling that the method isn't even called.

Community
  • 1
  • 1
Matt Klein
  • 7,856
  • 6
  • 45
  • 46
  • 1
    _"am I doing something wrong?"_ -- it would help if you would explain why it is you think the [base `SynchronizationContext` class](http://referencesource.microsoft.com/#mscorlib/system/threading/synchronizationcontext.cs,8b34a86241c7b423) should be doing anything other than exactly what it's doing here. [The documentation](https://msdn.microsoft.com/en-us/library/system.threading.synchronizationcontext(v=vs.110).aspx) seems perfectly clear to me: _"The SynchronizationContext class is a base class that provides a free-threaded context with no synchronization."_ – Peter Duniho Sep 30 '16 at 17:40
  • It wasn't that the continuation was coming back on a thread pool thread (behavior of `SynchronizationContext` class), it was the fact that I set the Sync Context, I use await, and then the Sync Context is null. I first noticed this behavior using a custom SynchronizationContext, which then became null, and then would no longer get `Post` commands sent to it. I just used the default `SynchronizationContext` class as a minimum working example of the error I was seeing. – Matt Klein Sep 30 '16 at 19:47
  • 2
    _"I set the Sync Context...and then the Sync Context is null"_ -- the context is per-thread (also documented). The context didn't "become null". You just wound up executing on a different thread from where you set it, as expected. And the context for that thread was null, also as expected. – Peter Duniho Sep 30 '16 at 19:56
  • @PeterDuniho Indeed, thanks. I've fallen into the trap of oversimplifying my problem in order to describe it. +1 for the context being per thread, I didn't completely have my head wrapped around that. In the real code there is only a single thread and the Sync Context dispatches to that thread. The issue with the real code was as Stephen said, I was setting the context in an async method and it was getting cleared after the await. – Matt Klein Oct 07 '16 at 13:28

2 Answers2

6

new SynchronizationContext() by convention is the same as a null synchronization context; that is, the thread pool context. So, the behavior in those tests is not unexpected.

await is behaving correctly; it is seeing the current synchronization context and Posting its continuation to that context. However, the thread pool sync context just executes the delegate on a thread pool thread - it doesn't set SynchronizationContext.Current to new SynchronizationContext because the convention is that null is the thread pool sync context.

I believe the problem with your real code is that your ActiveObjectSynchronizationContext is not setting itself as current when executing queued delegates. It's the responsibility of SynchronizationContext.Post to call SetSynchronizationContext if necessary just before executing the delegate, although - correct me if I'm wrong - the name "Active Object" seems to imply a single-threaded STA-like model. If this is the case, then Post wouldn't need to set it; it would just need to execute the delegate on the correct thread, which should already have the correct current sync context.

If it helps, I have a single-threaded SynchronizationContext here, though it doesn't do any kind of (explicit) message pumping.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Awesome, thanks Stephen! Indeed I was only calling `SetSynchronizationContext` once as the first command on the Active Object thread. I did not realize that it was the Synchronization Context's responsibility to also then re-set the Sync Context on each call to `Post`. – Matt Klein Sep 30 '16 at 19:43
  • @MattKlein: If you do have a dedicated Active Object thread, you should only have to set it once when the thread initially starts running. The only gotcha is that it must be set in a synchronous method, not an `async` one. – Stephen Cleary Sep 30 '16 at 19:53
  • That was the ticket! In my test code I was setting the sync context in an async method, but before the await, so I thought I'd be good to go. Thanks for clarifying, and for your book and blog! – Matt Klein Oct 07 '16 at 13:30
0

As @StephenCleary mentioned in a comment, the issue was that in my real code I was setting the Sync Context in an async method but before the await keyword, which I thought would work but apparently it doesn't (I'm guessing the state machine grabs the context at the start of the method?).

The solution is to set the Sync Context in a non-async method first.

Wrong

activeObject.Enqueue(async () =>
    {
        SynchronizationContext
            .SetSynchronizationContext(
                new ActiveObjectSynchronizationContext(activeObject));

        // do work

        await Task.Delay(500);

        // Sync Context now cleared
    });

Right

activeObject.Enqueue(() =>
        SynchronizationContext
            .SetSynchronizationContext(
                new ActiveObjectSynchronizationContext(activeObject));
    );

activeObject.Enqueue(async () =>
    {
        // do work

        await Task.Delay(500);

        // Still on Active Object thread
    });
Matt Klein
  • 7,856
  • 6
  • 45
  • 46