0

In my Asp.Net WebApi controller (framework version 4.6.1) I have the following code:

    [Route("async_test_2")]
    public async Task<IHttpActionResult> AsyncTest2()
    {
        TelemetryDebugWriter.IsTracingDisabled = true;
        var aspNetContext = SynchronizationContext.Current;
        SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); //set context while calling AsyncMethod
        var task = AsyncMethod();
        SynchronizationContext.SetSynchronizationContext(aspNetContext); //Restore AspNet context before awaiting
        DebugContext("Before outer await");
        await Task.WhenAll(new Task[] { task });
        DebugContext("After outer await");
        return Ok();
    }

    private async Task AsyncMethod()
    {
        DebugContext("Before inner await");
        await Task.Delay(2000);
        DebugContext("After inner await");
    }

    private void DebugContext(string location)
    {
        System.Diagnostics.Debug.WriteLine(location + "  ---  SyncContext: " + (SynchronizationContext.Current?.ToString() ?? "null") + "; ManagedThreadId: " + Thread.CurrentThread.ManagedThreadId);
    }

The debug output is:

Before inner await  ---  SyncContext: System.Threading.SynchronizationContext; ManagedThreadId: 6
Before outer await  ---  SyncContext: System.Web.AspNetSynchronizationContext; ManagedThreadId: 6
After inner await  ---  SyncContext: null; ManagedThreadId: 5
After outer await  ---  SyncContext: System.Web.AspNetSynchronizationContext; ManagedThreadId: 6

Why would the continuation 'After inner await' have a null SynchronizationContext? If I simply remove the call to SetSynchronizationContext and the call to restore it (i.e. don't modify the context, leaving the default AspNetSynchronizationContext), then the context is not null in any debug output.

Before inner await  ---  SyncContext: System.Web.AspNetSynchronizationContext; ManagedThreadId: 7
Before outer await  ---  SyncContext: System.Web.AspNetSynchronizationContext; ManagedThreadId: 7
After inner await  ---  SyncContext: System.Web.AspNetSynchronizationContext; ManagedThreadId: 8
After outer await  ---  SyncContext: System.Web.AspNetSynchronizationContext; ManagedThreadId: 8

Adding 'ConfigureAwait(false)' after the inner await will cause the context to be null in the continuation as expected.

Before inner await  ---  SyncContext: System.Web.AspNetSynchronizationContext; ManagedThreadId: 7
Before outer await  ---  SyncContext: System.Web.AspNetSynchronizationContext; ManagedThreadId: 7
After inner await  ---  SyncContext: null; ManagedThreadId: 7
After outer await  ---  SyncContext: System.Web.AspNetSynchronizationContext; ManagedThreadId: 8

So it works as expected when the AspNetSynchronizationContext is active, but not when a default SynchronizationContext is active. In that case, it's always null in the continuation regardless of whether ConfigureAwait(false) is called.

Triynko
  • 18,766
  • 21
  • 107
  • 173
  • What version of .net are you running ? – user3621898 Jun 22 '18 at 22:09
  • Web app is configured to use .NET Framework v4.6.1. – Triynko Jun 22 '18 at 22:10
  • It minds my boggle how anybody is supposed to get a repro for this issue from the snippet. Use the Debug > Windows > Threads window to verify assumptions, ensure that you are still on the thread you expected to be on. – Hans Passant Jun 22 '18 at 22:12
  • Also of note, in the custom SyncContext's "Post" handler override (which just calls base.Post), I can see that 1. it is being invoked, but 2. the SynchronizationContext.Current is null, and 3. even if I set it to SynchronizationContext.SetSynchronizationContext(this) in the post handler, it's still null in the continuation. – Triynko Jun 22 '18 at 22:13
  • The thread is irrelevant here. It starts on one thread, awaits, then picks up on another thread. The point is, this is a very basic example of how when the continuation picks up, it should have the SynchronizationContext that was active at the time await was called. – Triynko Jun 22 '18 at 22:14
  • 1
    Ok, I will simply this for you with a full implemenation and simple debug output you can run yourself in a black ASP.NET app. – Triynko Jun 22 '18 at 22:33
  • 1
    Creating a new empty MVC project in Visual Studio 2017, and replacing `HomeController.vb` with [this code](https://pastebin.com/V0fY5dFi), does give me the same result you are seeing. I'm still trying to wrap my head around the finer points of async / await, so I can't really speak as to why this happens. – Bradley Uffner Jun 22 '18 at 22:50
  • My best guess at this point, since you haven't shown the code for it, is that your `CustomSyncContext` isn't doing whatever it needs to be doing in order to flow the context correctly. – Bradley Uffner Jun 22 '18 at 22:53
  • Thank you, that's a very clear reproduction of the behavior in pastebin. This is not the behavior I'm expecting, so I'm curious if anyone can explain why? – Triynko Jun 22 '18 at 22:54
  • Bradley Uffner, not only have I posted complete code, another person has posted complete code reproduced the issue, with complete code in pastebin. It's the default SynchronizationContext implementation that the custom one inherits from. It doesn't change anything. As far as I know, I shouldn't have to do anything to the SynchronizationContext implementation for the default 'await' logic to work. It's supposed to flow the current SynchronizationContext by default from what I've read, but it's not. – Triynko Jun 22 '18 at 22:55
  • @Triynko yeah.. that was me posting the reproduction :) – Bradley Uffner Jun 22 '18 at 22:56
  • Indeed. You are the other person. Thank you. – Triynko Jun 22 '18 at 23:00
  • I'd check the [reference source](https://referencesource.microsoft.com/#System.Web/AspNetSynchronizationContext.cs) to see if `AspNetSynchronizationContext` does anything special reguarding the context flow. The different contexts have different behavior, tailored to the environment they are designed to be used in. – Bradley Uffner Jun 22 '18 at 23:04
  • I already read though the entire reference source of that class. Other documentation provides code showing that 'await' is equivalent to getting the current synchronization context, and if it's not null, posting the rest of the method as a continuation on that context. – Triynko Jun 23 '18 at 01:54
  • Other documentation says: "In other words, before the async method yields to asynchronously wait for the Task ‘t’, we capture the current SynchronizationContext. When the Task being awaited completes, a continuation will run the remainder of the asynchronous method. If the captured SynchronizationContext was null, then RestOfMethod() will be executed in the original TaskScheduler (which is often TaskScheduler.Default, meaning the ThreadPool). If, however, the captured context wasn’t null, then the execution of RestOfMethod() will be posted to the captured context to run there." – Triynko Jun 23 '18 at 01:55
  • This might be the problem: "The default implementation of SynchronizationContext.Post just turns around and passes it off to the ThreadPool via QueueUserWorkItem" – Triynko Jun 23 '18 at 01:58
  • But that still doesn't make any sense. The compiler, which is processing the 'await' is what's supposed to make the continuation call on the awaitable (i.e. make the decision about what SynchronizationContext to post the continuation on. So it should be working. Must be misunderstanding something. – Triynko Jun 23 '18 at 02:03
  • "Resuming execution is called the continuation. A great feature ***of the await keyword*** is that it captures the current SynchronizationContext before it runs the asynchronous operation, then it will post the continuation to that SynchronizationContext, meaning if you are on the UI Thread when you await Foo() once Foo() finishes running your code will continue execution on the UI Thread." 'await' posting to the current sync context, everywhere I read, sound like basic, unchangeable behavior. It just isn't working that way. – Triynko Jun 23 '18 at 02:06
  • Possible duplicate of: https://stackoverflow.com/questions/44067947/why-sync-context-is-not-working-for-await – Triynko Jun 23 '18 at 02:19
  • This bizarre, how/why did this site copy and represent this post? http://50996137.hjz.pw/ – Triynko Jun 25 '18 at 23:34

2 Answers2

0

Recently, I begin to learn something about SynchronizationContext, after read .Net Framewrok source code, I find that await just capture the current SynchronizationContext and Post the remain code exectution to the context, but it does not set the SynchronizaitonContext, so you got a null.

The AspNetSynchronizationContext does some extra work to ensure that even tasks are executed in different threads, the SynchronizationContext.Current are same. You can check the ISyncContext.Enter implementaion:

 // set synchronization context for the current thread to support the async pattern
        _originalSynchronizationContext = AsyncOperationManager.SynchronizationContext;
        AspNetSynchronizationContextBase aspNetSynchronizationContext = HttpContext.SyncContext;
        AsyncOperationManager.SynchronizationContext = aspNetSynchronizationContext;

the AsyncOperationManger.SynchronizationContext is just call SynchronizationContext.SetSynchronizationContext method to set SynchronizationContext.Current

You can check out this simple implementation of SynchronizationContext to replace

new SynchronizationContext()

with

new CustomSyncContext()

CustomSyncContext:

public class CustomSyncContext : SynchronizationContext
{
    private Task lastTask = Task.FromResult<object>(null);

    private object lockObj = new object();
    public override void Post(SendOrPostCallback d, object state)
    {
        Debug.WriteLine("post back from await");
        lock (lockObj)
        {

            var newTask = lastTask.ContinueWith(_=>{
                AsyncOperationManager.SynchronizationContext = this;
                d(state);
            }, TaskScheduler.Default);
            lastTask = newTask;
        }
    }

    public override SynchronizationContext CreateCopy()
    {
        return this;
    }

}

and find out that SynchronizationContext.Current is not null any more

Asiync
  • 1
0

SynchronizationContext.Current is thread local. Task.Delay runs on a new thread (the myth that Tasks automagically "eliminate threads" is dangerous), therefore its SynchronizationContext is null. All threads in a thread pool get null (it's unsafe to carry a real object there - they never die, you'd need very careful cleanup code after every run or you are having unholy entanglement).

That null means that continuation records are sent to the thread pool's principal tread which serializes them (in a queue) but will not run them. He's a lazy bastard :-) who delegates everything he can. Instead he will send these continuation records back into his own thread pool when their time to run comes - and won't guarantee execution order at all (that serializing was just for his own safety and is a hot spot). So, at the time your "After inner await" debug print is running, SynchronizationContext is again null and you are in unknown thread.

You'd have to write your own SynchronizationContext derivative and manage it's instances (without locking). You could assign them to thread pool's threads but you'd have to be very careful and detect when the last continuation from a chain is finished since at that point you have to clean up. You'd essentially need your own derivative of the thread class as well.

Don't forget that threads in a thread pool live forever - the whole purpose of the thread pool is to never have to destroy and latter create new thread objects. Plus GC treats all thread objects as roots which means that whatever they touch becomes eternal as well (some people would call that leaking) unless you explicitly nullify pointers.

ZXX
  • 4,684
  • 27
  • 35