【问题标题】:Cleaning up CallContext in TPL清理 TPL 中的 CallContext
【发布时间】:2015-03-12 03:12:08
【问题描述】:

根据我使用的是基于 async/await 的代码还是基于 TPL 的代码,我在清理逻辑 CallContext 时遇到了两种不同的行为。

如果我使用以下异步/等待代码,我可以完全按照我的预期设置和清除逻辑 CallContext

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

        return;
    }

    static void Main(string[] args)
    {
        DoSomething().Wait();

        Debug.WriteLine(new
        {
            Place = "Main",
            Id = Thread.CurrentThread.ManagedThreadId,
            Msg = CallContext.LogicalGetData("hello")
        });

    }
}

以上输出如下:

{ Place = Task.Run, Id = 9, Msg = world }
{ Place = Main, Id = 8, Msg = }

注意Msg =,它表示主线程上的CallContext已被释放并且为空。

但是当我切换到纯 TPL/TAP 代码时,我无法达到同样的效果......

class Program
{
    static Task DoSomething()
    {
        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")
                );

        return result;
    }

    static void Main(string[] args)
    {
        DoSomething().Wait();

        Debug.WriteLine(new
        {
            Place = "Main",
            Id = Thread.CurrentThread.ManagedThreadId,
            Msg = CallContext.LogicalGetData("hello")
        });
    }
}

以上输出如下:

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

我能做些什么来强制 TPL 以与 async/await 代码相同的方式“释放”逻辑 CallContext

我对@9​​87654328@ 的替代品不感兴趣。

我希望修复上面的 TPL/TAP 代码,以便我可以在针对 .net 4.0 框架的项目中使用它。如果这在 .net 4.0 中是不可能的,我仍然很好奇它是否可以在 .net 4.5 中完成。

【问题讨论】:

    标签: c# asynchronous task-parallel-library async-await task


    【解决方案1】:

    async 方法中,CallContext 在写入时被复制:

    当异步方法启动时,它会通知其逻辑调用上下文以激活写时复制行为。这意味着当前的逻辑调用上下文实际上并没有改变,但是它被标记为如果你的代码确实调用了CallContext.LogicalSetData,逻辑调用上下文数据在它被改变之前被复制到一个新的当前逻辑调用上下文中。

    来自Implicit Async Context ("AsyncLocal")

    这意味着在您的async 版本中CallContext.FreeNamedDataSlot("hello") 延续是多余的,即使没有它:

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

    Main 中的 CallContext 不会包含 "hello" 插槽:

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

    在 TPL 等效项中,Task.Run 之外的所有代码(应为 Task.Factory.StartNew,因为在 .Net 4.5 中添加了 Task.Run)在同一线程上运行,完全相同的 CallContext。如果您想清理它,您需要在该上下文中执行此操作(而不是在延续中):

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

    您甚至可以从中抽象出一个范围,以确保您始终自行清理:

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

    使用:

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

    【讨论】:

    • 在您的 TPL 版本中,是否存在逻辑 CallContext 在 Task.Factory.StartNew 有机会捕获它之前被释放的风险?我还需要确保来自 Task.Factory.StartNew 的所有延续(如果有)确实具有 CallContext,即使它已被主线程“释放”。
    • @BrentArias 你可以用 Thread.Sleep 测试它(我做过)。 Task.Factory.StartNew 和 Task.Run 一样捕获(复制)上下文并将其存储在 Task 中,因此您无需担心。您可以在此处了解更多信息:blogs.msdn.com/b/pfxteam/archive/2012/06/15/…
    • @BrentArias "当您使用 Task.Run 时,对 Run 的调用会从调用线程中捕获 ExecutionContext,将该 ExecutionContext 实例存储到 Task 对象中。当委托提供给 Task.Run稍后作为该任务执行的一部分调用,它是通过 ExecutionContext.Run 使用存储的上下文来完成的。对于 Task.Run、ThreadPool.QueueUserWorkItem、Delegate.BeginInvoke、Stream.BeginRead、DispatcherSynchronizationContext.Post、以及您能想到的任何其他异步 API。"
    • 我很乐意在very similar ASP.NET Core AsyncLocal topic 上提供您的意见。
    【解决方案2】:

    一个好问题。 await 版本可能无法像您认为的那样工作。让我们在 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();
        }
    }
    

    输出:

    { Place = Task.Run, Id = 10, Msg = world } { Place = await 之后,Id = 11,Msg = world } { 地点 = 主要,ID = 9,消息 = }

    注意"world"await 之后仍然存在,因为它在await 之前存在。它在DoSomething().Wait() 之后不存在,因为它首先不存在。

    有趣的是,DoSomethingasync 版本在第一个LogicalSetData 上为其作用域创建了LogicalCallContext 的写时复制克隆。即使内部没有异步,它也会这样做 - 尝试await Task.FromResult(0)。我假设在第一次写入操作时,整个 ExecutionContext 被克隆到 async 方法的范围内。

    OTOH,对于非异步版本,这里没有“逻辑”范围和外部ExecutionContext,因此ExecutionContext 的写时复制克隆成为Main 线程的最新版本(但延续Task.Run lambdas 仍然有自己的克隆)。因此,您要么需要将 CallContext.LogicalSetData("hello", "world") 移动到 Task.Run lambda 中,要么手动克隆上下文:

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

    【讨论】:

      猜你喜欢
      • 2010-09-21
      • 1970-01-01
      • 2016-12-26
      • 1970-01-01
      • 2012-04-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多