使用带有 async/await 的 ThreadStatic 变量
Posted
技术标签:
【中文标题】使用带有 async/await 的 ThreadStatic 变量【英文标题】:using ThreadStatic variables with async/await 【发布时间】:2012-10-12 05:04:39 【问题描述】:使用 C# 中的新 async/await 关键字,现在会影响您使用 ThreadStatic 数据的方式(和时间),因为回调委托在与 async
操作开始的线程不同的线程上执行。例如,以下简单的控制台应用程序:
[ThreadStatic]
private static string Secret;
static void Main(string[] args)
Start().Wait();
Console.ReadKey();
private static async Task Start()
Secret = "moo moo";
Console.WriteLine("Started on thread [0]", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("Secret is [0]", Secret);
await Sleepy();
Console.WriteLine("Finished on thread [0]", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("Secret is [0]", Secret);
private static async Task Sleepy()
Console.WriteLine("Was on thread [0]", Thread.CurrentThread.ManagedThreadId);
await Task.Delay(1000);
Console.WriteLine("Now on thread [0]", Thread.CurrentThread.ManagedThreadId);
将输出如下内容:
Started on thread [9]
Secret is [moo moo]
Was on thread [9]
Now on thread [11]
Finished on thread [11]
Secret is []
我也尝试过使用 CallContext.SetData
和 CallContext.GetData
并得到相同的行为。
在阅读了一些相关的问题和线程后:
CallContext vs ThreadStatic http://forum.springframework.net/showthread.php?572-CallContext-vs-ThreadStatic-vs-HttpContext&highlight=LogicalThreadContext http://piers7.blogspot.co.uk/2005/11/threadstatic-callcontext-and_02.html似乎像 ASP.Net 这样的框架显式地跨线程迁移 HttpContext,而不是 CallContext
,所以这里使用 async
和 await
关键字可能会发生同样的事情?
考虑到 async/await 关键字的使用,存储与可以(自动!)在回调线程上恢复的特定执行线程关联的数据的最佳方式是什么?
谢谢,
【问题讨论】:
AsyncLocal 是现在实现这一目标的现代方式。介意接受我的回答吗? 【参考方案1】:您可以使用CallContext.LogicalSetData
和CallContext.LogicalGetData
,但我建议您不要使用,因为当您使用简单并行时它们不支持任何类型的“克隆” (Task.WhenAny
/ Task.WhenAll
)。
我打开了一个UserVoice request 以获得更完整的async
兼容“上下文”,在an MSDN forum post 中有更详细的解释。似乎不可能自己建造一个。 Jon Skeet 有一个关于这个主题的good blog entry。
因此,我建议您使用参数、lambda 闭包或本地实例的成员 (this
),如 Marc 所述。
是的,OperationContext.Current
不保留在 await
s 中。
更新: .NET 4.5 确实在 async
代码中支持 Logical[Get|Set]Data
。详情on my blog.
【讨论】:
你能评论一下 AsyncLocalAsyncLocal<T>
是这个问题的现代解决方案。【参考方案2】:
基本上,我会强调:不要那样做。 [ThreadStatic]
永远不会很好地处理在线程之间跳转的代码。
但你不必这样做。 Task
已经带有状态 - 事实上,它可以通过两种不同的方式实现:
此外,编译器无论如何都会在这里完成您需要的一切:
private static async Task Start()
string secret = "moo moo";
Console.WriteLine("Started on thread [0]",
Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("Secret is [0]", secret);
await Sleepy();
Console.WriteLine("Finished on thread [0]",
Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("Secret is [0]", secret);
无静态;线程或多个任务没有问题。它正常工作。请注意,secret
不仅仅是这里的“本地”;编译器已经工作了一些巫术,就像它对迭代器块和捕获的变量一样。检查反射器,我得到:
[CompilerGenerated]
private struct <Start>d__0 : IAsyncStateMachine
// ... lots more here not shown
public string <secret>5__1;
【讨论】:
在 WCF 的情况下呢?我应该只使用OperationContext
,前提是它被迁移到新线程吗?
@theburningmonk 如果您的意思是 instance,那么应该可以。但我怀疑静态 OperationContext.Current
是否能正常工作。所以var ctx = OperationContext.Current;
在顶部(在原始线程上),然后只引用ctx
,而不是OperationContext.Current
所以你的意思是,除非你像之前await
一样在闭包中捕获当前的OperationContext
,否则你不会在OperationContext
之后返回OperationContext
的相同实例@?
【参考方案3】:
AsyncLocal<T> 支持维护范围为特定异步代码流的变量。
将变量类型改为AsyncLocal,例如,
private static AsyncLocal<string> Secret = new AsyncLocal<string>();
提供以下所需的输出:
Started on thread [5]
Secret is [moo moo]
Was on thread [5]
Now on thread [6]
Finished on thread [6]
Secret is [moo moo]
【讨论】:
这个答案现在应该是正确的,因为它是最新的和最新的。 警告:AsyncLocal
向下游流动,但不向上游流动。更改“子”方法中的值不会反映在“父”方法中【参考方案4】:
要在同一线程上继续执行任务需要同步提供程序。这是一个昂贵的词,简单的诊断是通过查看调试器中 System.Threading.SynchronizationContext.Current 的值。
在控制台模式应用中,该值将为 null。没有提供程序可以使代码在控制台模式应用程序的特定线程上运行。只有 Winforms 或 WPF 应用程序或 ASP.NET 应用程序将具有提供程序。并且只在他们的主线程上。
这些应用程序的主线程做了一些非常特别的事情,它们有一个调度程序循环(又名消息循环或消息泵)。它实现了producer-consumer problem 的通用解决方案。正是调度程序循环允许处理线程执行一些工作。这样的工作将是等待表达式之后的任务继续。该位将在调度程序线程上运行。
WindowsFormsSynchronizationContext 是 Winforms 应用程序的同步提供程序。它使用 Control.Begin/Invoke() 来分派请求。对于 WPF,它是 DispatcherSynchronizationContext 类,它使用 Dispatcher.Begin/Invoke() 来分派请求。对于 ASP.NET,它是 AspNetSynchronizationContext 类,它使用不可见的内部管道。他们在初始化时创建各自提供者的实例并将其分配给 SynchronizationContext.Current
控制台模式应用程序没有这样的提供程序。主要是因为主线程完全不合适,它不使用调度程序循环。您将创建自己的,然后还创建自己的 SynchronizationContext 派生类。很难做到,你不能再像 Console.ReadLine() 这样调用,因为这会完全冻结 Windows 调用的主线程。您的控制台模式应用不再是控制台应用,而是开始类似于 Winforms 应用。
请注意,这些运行时环境具有同步提供程序是有充分理由的。他们必须拥有一个,因为 GUI 从根本上说是线程不安全的。控制台没有问题,它是线程安全的。
【讨论】:
WCF 中是否有等效的同步提供程序?【参考方案5】:看看这个thread
在标有 ThreadStaticAttribute 的字段上,初始化仅在静态构造函数中发生一次。在您的代码中,当创建 ID 为 11 的新线程时,将创建一个新的 Secret 字段,但它为空/null。在等待调用后返回“开始”任务时,任务将在线程 11 上完成(如您的打印输出所示),因此字符串为空。
您可以通过在调用 Sleepy 之前将 Secret 存储在“Start”内的本地字段中来解决您的问题,然后在从 Sleepy 返回后从本地字段中恢复 Secret。您也可以在调用“await Task.Delay(1000);”之前在 Sleepy 中执行此操作这实际上会导致线程切换。
【讨论】:
以上是关于使用带有 async/await 的 ThreadStatic 变量的主要内容,如果未能解决你的问题,请参考以下文章
使用带有 async/await 的 ThreadStatic 变量
使用带有 async/await 的 mongoose 承诺
使用带有 async\await 的 mysql 池 |节点JS