在 ASP.NET 应用程序中,我使用
Task.Run
和 Parallel.ForEach
做了很多事情。在某些时候,我可以达到 2-3 级深度:
// This is in a controller somewhere (i.e. so the Request object is in scope)
Task.Run(() => {
Parallel.ForEach(things, t => {
Task.Run(() => {
Parallel.ForEach(otherThings, t => {
// etc..
现在,这是一个夸张的想法(也是一个愚蠢的想法;我明白),但问题是:
Request
对象是否应该一直可用?
我已经做了很多阅读,并且我明白
ExecutionContext
应该“流入”所有这些代码(除非你明确阻止它)。
第一个
Task.Run
应获得与“外部”代码相同的 ExecutionContext
。它应该将其传递给第一个Parallel.ForEach
,依此类推。根据我的理解,该堆栈底部的代码应该能够访问与原始调用代码相同的数据。
在我的情况下,我在某些级别遇到一些奇怪的空错误,很明显该块没有将
ExecutionContext
传递给它。例如,在 Task.Run
内部的事物为 null,而在其外部则不为 null。这些错误与某些代码一致,但我看不出此代码与其他未收到错误的代码有何不同。
我是否误解了
ExecutionContext
流程的一些细微差别?
更新:这里有一些愚蠢的、人为的代码:
void Main()
{
var things = new string[] { "foo", "bar", "baz" };
Console.WriteLine(1);
Task.Run(() =>
{
Console.WriteLine(2);
Parallel.ForEach(things, t =>
{
Console.WriteLine(3);
Task.Run(() =>
{
Console.WriteLine(4);
Parallel.ForEach(things, t =>
{
Console.WriteLine(5);
Console.WriteLine(t);
});
});
});
});
}
果然,我得到了输出,深度为五层。因此,最内部的
Parallel.ForEach
在 things
中具有正确的数据(在调用代码中设置)。
什么可能会导致这种情况不会发生?
更新:根据评论,我测试了任务仍在运行时请求之前结束的理论。我认为这就是正在发生的事情,因为我没有使用 async/await。这意味着请求没有等待任务完成,Request 对象就消失了。
这是我有效的测试代码(意味着,在这个特定的测试用例中,它会永远持续下去......)
[Route("test")]
public async Task<string> Test()
{
await Task.Run(() =>
{
var counter = 0;
while (true)
{
counter++;
ServiceLocator.Current.GetInstance<IHttpContextAccessor>().HttpContext.Items.Add(counter.ToString(), "foo");
Debug.WriteLine(counter);
}
});
return "OK";
}
这不起作用——当尝试使用
HttpContext
时,它会抛出空引用。由于我没有等待任务完成,因此它尝试在请求完成且请求为空后运行。
[Route("test")]
public string Test()
{
Task.Run(() =>
{
var counter = 0;
while (true)
{
counter++;
ServiceLocator.Current.GetInstance<IHttpContextAccessor>().HttpContext.Items.Add(counter.ToString(), "foo");
Debug.WriteLine(counter);
}
});
return "OK";
}
ExecutionContext
与它的 AsyncLocal
包一起流动,通常位于下游 - 在每个“流”上制作副本(在 .NET Core 下)。
HttpContext
然而(通过IHttpContextAccessor
访问)有点不同,因为存在一定程度的间接性,允许所有下游副本受到影响。这是来自主要实现HttpContextAccessor
:
public class HttpContextAccessor : IHttpContextAccessor
{
private static readonly AsyncLocal<HttpContextHolder> _httpContextCurrent = new AsyncLocal<HttpContextHolder>();
/// <inheritdoc/>
public HttpContext? HttpContext
{
get
{
return _httpContextCurrent.Value?.Context;
}
set
{
var holder = _httpContextCurrent.Value;
if (holder != null)
{
// Clear current HttpContext trapped in the AsyncLocals, as its done.
holder.Context = null;
}
if (value != null)
{
// Use an object indirection to hold the HttpContext in the AsyncLocal,
// so it can be cleared in all ExecutionContexts when its cleared.
_httpContextCurrent.Value = new HttpContextHolder { Context = value };
}
}
}
private sealed class HttpContextHolder
{
public HttpContext? Context;
}
}
正如评论中所述,它使用
HttpContextHolder
对象来有效地为所有可能从不同 HttpContext
访问的 ExecutionContexts
实例提供“空开关”:
if (holder != null) {
// Clear current HttpContext trapped in the AsyncLocals, as its done.
holder.Context = null;
}
我猜 ASP.NET Core 团队决定不允许“即发即忘”的异步任务捕获并可能永远保留对陈旧 HttpContext 的引用,这可能会导致内存泄漏。