使用ThreadStatic变量与async/await

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.SetDataCallContext.GetData,但得到了相同的行为。

阅读了一些相关的问题和线程:

似乎像ASP.NET这样的框架会明确地在不同线程之间传递HttpContext,但不会传递CallContext,所以也许使用asyncawait关键字时发生了同样的事情?

考虑到使用异步/等待关键字,什么是存储与特定执行线程关联的数据并可以在回调线程上(自动!)恢复的最佳方法?

谢谢,


AsyncLocal是现代实现这一目的的方法。可以考虑接受我的答案吗? - Joel Oughton
5个回答

33

你可以使用CallContext.LogicalSetDataCallContext.LogicalGetData,但我建议你不要这样做,因为当你使用简单的并行性(Task.WhenAny/Task.WhenAll)时,它们不支持任何形式的“克隆”。

我在UserVoice请求中提出了一个更完整的async兼容“上下文”,在MSDN论坛帖子中有更详细的解释。似乎我们自己无法构建它。Jon Skeet在这个主题上写了一篇好的博客文章

所以,我建议你像Marc描述的那样使用参数、lambda闭包或局部实例(this)的成员。

是的,OperationContext.Currentawait时不会被保留。

更新:.NET 4.5在async代码中支持Logical[Get|Set]Data。详情请参见我的博客


2
你能评论一下 AsyncLocal<T> 吗?https://msdn.microsoft.com/zh-cn/library/dn906268(v=vs.110).aspx - b_levitt
3
AsyncLocal<T> 是解决这个问题的现代方案。 - Stephen Cleary

13

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]

这个答案现在应该是正确的,因为它是最新和最新的。 - Remi Despres-Smyth
2
警告:AsyncLocal 向下流动,但不向上流动。在“子”方法中更改值不会反映在“父”方法中。 - Alex from Jitbit

9
基本上,我会强调:不要这样做。[ThreadStatic]永远不会正确地处理在不同线程之间跳转的代码。
但你其实不需要这样做。一个Task已经携带了状态 - 实际上,它可以用两种不同的方式来完成:
- 有一个显式的状态对象,可以保存你需要的所有内容。 - lambdas /匿名方法可以形成对状态的闭包。
此外,编译器会为你处理一切,不需要你手动操作。
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
如果你指的是实例,那么应该可以工作。但我怀疑静态的OperationContext.Current会正确地工作。所以在顶部(原始线程上)使用var ctx = OperationContext.Current;,然后只引用ctx,而不是OperationContext.Current。@theburningmonk - Marc Gravell
所以你的意思是,除非在await之前将当前的OperationContext捕获到闭包中,否则在await之后你将无法得到相同的OperationContext实例? - theburningmonk

7
在同一线程上执行任务继续需要一个同步提供程序。这是一个昂贵的词,通过在调试器中查看System.Threading.SynchronizationContext.Current的值来进行简单的诊断。
在控制台模式应用程序中,该值将为null。在控制台模式应用程序中,没有提供程序可以使代码在特定线程上运行。只有Winforms或WPF应用程序或ASP.NET应用程序才会有提供程序。而且只在它们的主线程上。
这些应用程序的主线程做了非常特殊的事情,它们有一个调度程序循环(也称为消息循环或消息泵)。 它实现了生产者-消费者问题的通用解决方案。正是这个调度程序循环允许将一点工作交给线程执行。这样一点工作将是await表达式之后的任务继续。而这一点将在调度程序线程上运行。
WindowsFormsSynchronizationContext是Winforms应用程序的同步提供程序。它使用Control.Begin/Invoke()来分派请求。对于WPF,它是DispatcherSynchronizationContext类,它使用Dispatcher.Begin/Invoke()来分派请求。对于ASP.NET,它是AspNetSynchronizationContext类,它使用不可见的内部管道。它们在初始化时创建其各自提供程序的实例并将其分配给SynchronizationContext.Current。
控制台模式应用程序没有这样的提供程序。主要是因为主线程完全不适合,它不使用调度程序循环。您需要创建自己的SynchronizationContext派生类。这很难做到,因为您不能再像Console.ReadLine()那样进行调用,因为这会完全冻结Windows上的主线程。您的控制台模式应用程序将停止成为控制台应用程序,它将开始类似于Winforms应用程序。
请注意,这些运行时环境具有同步提供程序,有很好的原因。它们必须有一个,因为GUI基本上是线程不安全的。这不是控制台的问题,它是线程安全的。

WCF 中是否有等效的同步提供程序? - theburningmonk

0

看看这个 线程

标记为ThreadStaticAttribute的字段只会在静态构造函数中初始化一次。在您的代码中,当创建一个新线程(ID为11)时,将创建一个新的Secret字段,但它是空/ null的。在await调用后返回“Start”任务时,任务将在线程11上完成(如您的输出所示),因此字符串为空。

您可以通过在调用Sleepy之前在“Start”内部将Secret存储在本地字段中,然后在从Sleepy返回后从本地字段恢复Secret来解决问题。您也可以在Sleepy中执行此操作,就在调用“await Task.Delay(1000);”实际上导致线程切换之前。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接