ExecutionContext 是否总是流入 Task.Run 或 Parallel.ForEach?

问题描述 投票:0回答:1

在 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";
}
c# multithreading asynchronous task
1个回答
0
投票

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 的引用,这可能会导致内存泄漏。

© www.soinside.com 2019 - 2024. All rights reserved.