[英]Cleaning up CallContext in TPL
根據我是使用基於異步/等待的代碼還是基於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
在寫入時被復制:
當異步方法啟動時,它會通知其邏輯調用上下文以激活寫時復制行為。 這意味着當前的邏輯調用上下文實際上沒有更改,但它被標記為如果您的代碼調用
CallContext.LogicalSetData
,則邏輯調用上下文數據在更改之前將被復制到新的當前邏輯調用上下文中。
這意味着在您的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;
}
}
}
一個好問題。 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().Wait()
因為它首先不在它之前。
有趣的是, DoSomething
的async
版本在第一個LogicalSetData
為其作用域創建了LogicalCallContext
的寫時復制克隆。 即使內部沒有異步,它也await Task.FromResult(0)
- 嘗試await Task.FromResult(0)
。 我假設在第一次寫操作時,整個ExecutionContext
被克隆為async
方法的范圍。
OTOH,對於非異步版本沒有“邏輯”范圍,沒有外部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;
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.