Cleaning up CallContext in TPL

后端 未结 2 1819
梦谈多话
梦谈多话 2021-02-07 16:43

Depending on whether I\'m using async/await based code or TPL based code, I\'m getting two different behaviors regarding the clean-up of logical CallContext.

<
2条回答
  •  小蘑菇
    小蘑菇 (楼主)
    2021-02-07 17:24

    A good question. The await version may not work the way you may think it does here. Let's add another logging line inside DoSomething:

    class Program
    {
        static async Task DoSomething()
        {
            CallContext.LogicalSetData("hello", "world");
    
            await Task.Run(() =>
                Debug.WriteLine(new
                {
                    Place = "Task.Run",
                    Id = Thread.CurrentThread.ManagedThreadId,
                    Msg = CallContext.LogicalGetData("hello")
                }))
                .ContinueWith((t) =>
                    CallContext.FreeNamedDataSlot("hello")
                    );
    
            Debug.WriteLine(new
            {
                Place = "after await",
                Id = Thread.CurrentThread.ManagedThreadId,
                Msg = CallContext.LogicalGetData("hello")
            });
        }
    
        static void Main(string[] args)
        {
    
            DoSomething().Wait();
    
            Debug.WriteLine(new
            {
                Place = "Main",
                Id = Thread.CurrentThread.ManagedThreadId,
                Msg = CallContext.LogicalGetData("hello")
            });
    
            Console.ReadLine();
        }
    }
    

    Output:

    { Place = Task.Run, Id = 10, Msg = world }
    { Place = after await, Id = 11, Msg = world }
    { Place = Main, Id = 9, Msg =  }
    

    Note the "world" is still there after await, because it was there before await. And it is not there after DoSomething().Wait() because it wasn't there before it, in the first place.

    Interestingly enough, the async version of DoSomething creates a copy-on-write clone of the LogicalCallContext for its scope, upon the first LogicalSetData. It does that even when there is no asynchrony inside it - try await Task.FromResult(0). I presume the whole ExecutionContext gets cloned for the scope of the async method, upon the 1st write operation.

    OTOH, for the non-async version there is no "logical" scope and no outer ExecutionContext here, so the copy-on-write clone of ExecutionContext becomes current for the Main thread (but the continuations and the Task.Run lambdas still get their own clones). So, you'd either need to move CallContext.LogicalSetData("hello", "world") inside the Task.Run lambda, or clone the context manually:

    static Task DoSomething()
    {
        var ec = ExecutionContext.Capture();
        Task task = null;
        ExecutionContext.Run(ec, _ =>
        {
            CallContext.LogicalSetData("hello", "world");
    
            var result = Task.Run(() =>
                Debug.WriteLine(new
                {
                    Place = "Task.Run",
                    Id = Thread.CurrentThread.ManagedThreadId,
                    Msg = CallContext.LogicalGetData("hello")
                }))
                .ContinueWith((t) =>
                    CallContext.FreeNamedDataSlot("hello")
                    );
    
            task = result;
        }, null);
    
        return task;
    }
    

提交回复
热议问题