混合 ExecutionContext.SuppressFlow 和任务时 AsyncLocal.Value 出现意外值

Bud*_*rot 5 c# asynchronous task-parallel-library async-await .net-core

在应用程序中,由于 AsyncLocal 的错误/意外值,我遇到了奇怪的行为:尽管我抑制了执行上下文的流程,但 AsyncLocal.Value 属性有时不会在新生成的任务的执行范围内重置。

\n

下面我创建了一个最小的可重现示例来演示该问题:

\n
private static readonly AsyncLocal<object> AsyncLocal = new AsyncLocal<object>();\n[TestMethod]\npublic void Test()\n{\n    Trace.WriteLine(System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription);\n    var mainTask = Task.Factory.StartNew(() =>\n        {\n            AsyncLocal.Value = "1";\n            Task anotherTask;\n            using (ExecutionContext.SuppressFlow())\n            {\n                anotherTask = Task.Run(() =>\n                    {\n                        Trace.WriteLine(AsyncLocal.Value); // "1" <- ???\n                        Assert.IsNull(AsyncLocal.Value); // BOOM - FAILS\n                        AsyncLocal.Value = "2";\n                    });\n            }\n\n            Task.WaitAll(anotherTask);\n        });\n\n    mainTask.Wait(500000, CancellationToken.None);\n}\n
Run Code Online (Sandbox Code Playgroud)\n

在十分之九的运行中(在我的电脑上),测试方法的结果是:

\n
.NET 6.0.2\n"1"\n
Run Code Online (Sandbox Code Playgroud)\n

-> 测试失败

\n

正如您所看到的,测试失败,因为在Task.Run之前的值仍然存在于AsyncLocal.Value(Message:)中执行的操作中1

\n

我的具体问题是:

\n
    \n
  1. 为什么会发生这种情况?\n我怀疑发生这种情况是因为 Task.Run 可能使用当前线程来执行工作负载。在这种情况下,我假设缺少 async/await-operators 不会强制为该操作创建新的/单独的 ExecutionContext。就像 Stephen Cleary 所说的“从逻辑调用上下文\xe2\x80\x99s 的角度来看,所有同步调用都是 \xe2\x80\x9ccollapsed\xe2\x80\x9d - 它们\xe2\x80\x99 实际上是最接近的上下文的一部分”异步方法进一步位于调用堆栈中”。如果是这种情况,我确实理解为什么在操作中使用相同的上下文。
  2. \n
\n

这是对此行为的正确解释吗?另外,为什么它有时可以完美地工作(在我的机器上大约有十分之一)?

\n
    \n
  1. 我该如何解决这个问题?\n假设我的上述理论是正确的,那么强制引入一个新的异步“层”就足够了,如下所示:
  2. \n
\n
private static readonly AsyncLocal<object> AsyncLocal = new AsyncLocal<object>();\n[TestMethod]\npublic void Test()\n{\n    Trace.WriteLine(System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription);\n    var mainTask = Task.Factory.StartNew(() =>\n        {\n            AsyncLocal.Value = "1";\n            Task anotherTask;\n            using (ExecutionContext.SuppressFlow())\n            {\n                var wrapper = () =>\n                    {\n                        Trace.WriteLine(AsyncLocal.Value);\n                        Assert.IsNull(AsyncLocal.Value); \n                        AsyncLocal.Value = "2";\n                        return Task.CompletedTask;\n                    };\n\n                anotherTask = Task.Run(async () => await wrapper());\n            }\n\n            Task.WaitAll(anotherTask);\n        });\n\n    mainTask.Wait(500000, CancellationToken.None);\n}\n\n
Run Code Online (Sandbox Code Playgroud)\n

这似乎解决了问题(它在我的机器上始终有效),但我想确保这是解决此问题的正确方法。

\n

提前谢谢了

\n

Ste*_*ary 9

为什么会出现这种情况?我怀疑发生这种情况是因为 Task.Run 可能使用当前线程来执行工作负载。

我怀疑发生这种情况是因为Task.WaitAll将使用当前线程来内联执行任务。

具体来说,Task.WaitAll调用Task.WaitAllCore,它将尝试通过调用 来内联运行它Task.WrappedTryRunInline。我假设自始至终都使用默认的任务调度程序。在这种情况下,这将调用,如果委托已经被调用,TaskScheduler.TryRunInline它将返回false。因此,如果任务已经开始在线程池线程上运行,这将返回到WaitAllCore,这将只执行正常的等待,并且您的代码将按预期工作(十分之一)。

如果线程池线程尚未获取它(十分之九),则将TaskScheduler.TryRunInline调用TaskScheduler.TryExecuteTaskInline,其默认实现将调用Task.ExecuteEntryUnsafe,其调用Task.ExecuteWithThreadLocalTask.ExecuteWithThreadLocal具有应用ExecutionContextif one 被捕获的逻辑。假设没有捕获任何内容,则直接调用任务的委托。

所以,看起来每一步都是合乎逻辑的。从技术上讲,ExecutionContext.SuppressFlow意思是“不捕获ExecutionContext”,而这就是正在发生的事情。它并不意味着“清除ExecutionContext。有时,任务在线程池线程上运行(没有捕获ExecutionContext),并且WaitAll只会等待它完成。其他时候,任务将由WaitAll线程池线程而不是内联执行,在这种情况下,ExecutionContext不会清除(并且从技术上讲也不会捕获)。

您可以通过捕获当前线程 IDwrapper并将其与执行Task.WaitAll. 我希望对于异步本地值(意外地)继承的运行来说,它们将是相同的线程,并且对于异步本地值按预期工作的运行来说,它们将是不同的线程。