简体   繁体   中英

C# await in async method clears SynchronizationContext (with repro tests)

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 await s 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.

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 Post ing 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.

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
    });

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM