清理TPL中的CallContext

根据我是使用基于异步/等待的代码还是基于TPL的代码,我在清理逻辑CallContext遇到两种不同的行为。

如果我使用以下async / await代码,我可以完全按照我的预期设置和清除逻辑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吗?

我对CallContext替代品不感兴趣。

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

async方法中, CallContext在write上被复制:

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

来自隐式异步上下文(“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") })); } 

MainCallContext不包含"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; } } } 

一个好问题。 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 =等待之后,Id = 11,Msg = world}
 {Place = Main,Id = 9,Msg =}

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

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

OTOH,对于非异步版本,这里没有“逻辑”范围,也没有外部ExecutionContext ,因此ExecutionContext的copy-on-write克隆成为Main线程的当前(但是continuation和Task.Run lambdas仍然得到它们的自己的克隆)。 因此,您需要在Task.Run lambda中移动CallContext.LogicalSetData("hello", "world") ,或者手动克隆上下文:

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