Cleaning up CallContext in TPL

后端 未结 2 1824
梦谈多话
梦谈多话 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:21

    In an async method the CallContext is copied on write:

    When an async method starts, it notifies its logical call context to activate copy-on-write behavior. This means the current logical call context is not actually changed, but it is marked so that if your code does call CallContext.LogicalSetData, the logical call context data is copied into a new current logical call context before it is changed.

    From Implicit Async Context ("AsyncLocal")

    That means that in your async version the CallContext.FreeNamedDataSlot("hello") continuation is redundant as even without it:

    static async Task DoSomething()
    {
        CallContext.LogicalSetData("hello", "world");
    
        await Task.Run(() =>
            Console.WriteLine(new
            {
                Place = "Task.Run",
                Id = Thread.CurrentThread.ManagedThreadId,
                Msg = CallContext.LogicalGetData("hello")
            }));
    }
    

    The CallContext in Main wouldn't contain the "hello" slot:

    { Place = Task.Run, Id = 3, Msg = world }
    { Place = Main, Id = 1, Msg = }

    In the TPL equivalent all code outside the Task.Run (which should be Task.Factory.StartNew as Task.Run was added in .Net 4.5) runs on the same thread with the same exact CallContext. If you want to clean it you need to do that on that context (and not in the continuation):

    static Task DoSomething()
    {
        CallContext.LogicalSetData("hello", "world");
    
        var result = Task.Factory.StartNew(() =>
            Debug.WriteLine(new
            {
                Place = "Task.Run",
                Id = Thread.CurrentThread.ManagedThreadId,
                Msg = CallContext.LogicalGetData("hello")
            }));
    
        CallContext.FreeNamedDataSlot("hello");
        return result;
    }
    

    You can even abstract a scope out of it to make sure you always clean up after yourself:

    static Task DoSomething()
    {
        using (CallContextScope.Start("hello", "world"))
        {
            return Task.Factory.StartNew(() =>
                Debug.WriteLine(new
                {
                    Place = "Task.Run",
                    Id = Thread.CurrentThread.ManagedThreadId,
                    Msg = CallContext.LogicalGetData("hello")
                }));
        }
    }
    

    Using:

    public static class CallContextScope
    {
        public static IDisposable Start(string name, object data)
        {
            CallContext.LogicalSetData(name, data);
            return new Cleaner(name);
        }
    
        private class Cleaner : IDisposable
        {
            private readonly string _name;
            private bool _isDisposed;
    
            public Cleaner(string name)
            {
                _name = name;
            }
    
            public void Dispose()
            {
                if (_isDisposed)
                {
                    return;
                }
    
                CallContext.FreeNamedDataSlot(_name);
                _isDisposed = true;
            }
        }
    }
    

提交回复
热议问题