将ThreadStatic变量与async/await一起使用

the*_*onk 35 c# multithreading synchronization async-await

使用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);
}
Run Code Online (Sandbox Code Playgroud)

将输出以下内容:

Started on thread [9]
Secret is [moo moo]
Was on thread [9]
Now on thread [11]
Finished on thread [11]
Secret is []
Run Code Online (Sandbox Code Playgroud)

我也尝试用CallContext.SetDataCallContext.GetData,得到了相同的行为.

阅读完一些相关问题和主题后:

似乎像ASP.Net这样的框架显式地跨越线程迁移HttpContext,但不是CallContext,所以使用asyncawait关键字可能会发生同样的事情吗?

通过使用async/await关键字,存储与特定执行线程相关联的数据的最佳方法是什么,可以(自动!)在回调线程上恢复?

谢谢,

Ste*_*ary 28

可以使用CallContext.LogicalSetDataCallContext.LogicalGetData,但我建议您不要使用它,因为当您使用简单并行(Task.WhenAny/ Task.WhenAll)时它们不支持任何类型的"克隆" .

我打开了一个UserVoice请求,以获得更完整async兼容的"上下文",在MSDN论坛帖子中有更详细的解释.我们似乎不可能自己建造一个.Jon Skeet 在这个主题上有一篇很好的博客文章.

因此,我建议您使用参数,lambda闭包或本地实例(this)的成员,如Marc所述.

是的,OperationContext.Current没有保留在awaits.

更新: .NET 4.5不支持Logical[Get|Set]Dataasync代码.我博客上的详细信息.

  • 你能评论 AsyncLocal<T> 吗?https://msdn.microsoft.com/en-us/library/dn906268(v=vs.110).aspx (2认同)
  • @b_levitt:`AsyncLocal <T>`是解决此问题的现代方法。 (2认同)

Mar*_*ell 10

基本上,我要强调:不要那样做.[ThreadStatic]永远不会很好地使用在线程之间跳转的代码.

但你没必要.一个Task已经携带的状态 - 事实上,它可以通过两种不同的方式实现:

  • 有一个明确的状态对象,可以容纳你需要的一切
  • lambdas/anon-methods可以在状态上形成闭包

此外,编译器还可以执行此处所需的所有操作:

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);
}
Run Code Online (Sandbox Code Playgroud)

没有静态; 没有线程或多个任务的问题.它只是有效.请注意,secret这里不仅仅是"本地"; 编译器已经处理了一些伏都教,就像它对迭代器块和捕获的变量一样.检查反射器,我得到:

[CompilerGenerated]
private struct <Start>d__0 : IAsyncStateMachine
{
    // ... lots more here not shown
    public string <secret>5__1;
}
Run Code Online (Sandbox Code Playgroud)


小智 7

AsyncLocal<T>支持维护范围为特定异步代码流的变量。

将变量类型更改为 AsyncLocal,例如,

private static AsyncLocal<string> Secret = new AsyncLocal<string>();
Run Code Online (Sandbox Code Playgroud)

给出以下所需的输出:

Started on thread [5]
Secret is [moo moo]
Was on thread [5]
Now on thread [6]
Finished on thread [6]
Secret is [moo moo]
Run Code Online (Sandbox Code Playgroud)

  • 警告:“AsyncLocal”流向下游,但不流向上游。更改“子”方法中的值不会反映在“父”方法中 (2认同)

Han*_*ant 6

要使任务继续在同一线程上执行,需要同步提供程序.这是一个昂贵的词,简单的诊断是通过在调试器中查看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从根本上说是线程不安全的.控制台没有问题,它是线程安全的.