清理 TPL 中的 CallContext
Cleaning up CallContext in TPL
根据我使用的是基于 async/await 的代码还是基于 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 以 "free" 逻辑 CallContext
与 async/await 代码相同的方式吗?
我对 CallContext
的替代品不感兴趣。
我希望修复上面的 TPL/TAP 代码,以便我可以在针对 .net 4.0 框架的项目中使用它。如果这在 .net 4.0 中是不可能的,我仍然很好奇它是否可以在 .net 4.5 中完成。
好问题。 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 = after await, Id = 11, Msg = world }
{ Place = Main, Id = 9, Msg = }
请注意 "world"
在 await
之后仍然存在,因为它在 await
之前存在。它在 DoSomething().Wait()
之后不存在,因为它首先不存在。
有趣的是,DoSomething
的 async
版本在第一个 LogicalSetData
上为其范围创建了 LogicalCallContext
的写时复制克隆。即使内部没有异步,它也会这样做——试试 await Task.FromResult(0)
。我假设在第一次写入操作时,整个 ExecutionContext
被克隆到 async
方法的范围内。
OTOH,对于非异步版本,这里没有 "logical" 作用域,也没有外部 ExecutionContext
,因此 ExecutionContext
的写时复制克隆成为当前版本Main
线程(但延续和 Task.Run
lambda 仍然有自己的克隆)。因此,您需要在 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;
}
在 async
方法中,CallContext
在写入时被复制:
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.
来自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
,因为 Task.Run
是在 .Net 4.5 中添加的)与 [=43 在同一个线程上运行=]完全相同 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;
}
}
}
根据我使用的是基于 async/await 的代码还是基于 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 以 "free" 逻辑 CallContext
与 async/await 代码相同的方式吗?
我对 CallContext
的替代品不感兴趣。
我希望修复上面的 TPL/TAP 代码,以便我可以在针对 .net 4.0 框架的项目中使用它。如果这在 .net 4.0 中是不可能的,我仍然很好奇它是否可以在 .net 4.5 中完成。
好问题。 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 = after await, Id = 11, Msg = world } { Place = Main, Id = 9, Msg = }
请注意 "world"
在 await
之后仍然存在,因为它在 await
之前存在。它在 DoSomething().Wait()
之后不存在,因为它首先不存在。
有趣的是,DoSomething
的 async
版本在第一个 LogicalSetData
上为其范围创建了 LogicalCallContext
的写时复制克隆。即使内部没有异步,它也会这样做——试试 await Task.FromResult(0)
。我假设在第一次写入操作时,整个 ExecutionContext
被克隆到 async
方法的范围内。
OTOH,对于非异步版本,这里没有 "logical" 作用域,也没有外部 ExecutionContext
,因此 ExecutionContext
的写时复制克隆成为当前版本Main
线程(但延续和 Task.Run
lambda 仍然有自己的克隆)。因此,您需要在 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;
}
在 async
方法中,CallContext
在写入时被复制:
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.
来自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
,因为 Task.Run
是在 .Net 4.5 中添加的)与 [=43 在同一个线程上运行=]完全相同 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;
}
}
}