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

时间:2012-10-22 11:32:14

标签: 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);
}

将输出以下内容:

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可能会发生同样的事情。关键字?

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

谢谢,

5 个答案:

答案 0 :(得分:25)

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

我打开UserVoice request以获得更完整的async - 兼容“上下文”,在an MSDN forum post中有更详细的解释。我们似乎不可能自己建造一个。 Jon Skeet在这个问题上有一个good blog entry

因此,我推荐你使用参数,lambda闭包或本地实例的成员(this),正如Marc所描述的那样。

是的,OperationContext.Current在<{1}}之间保留

更新:.NET 4.5支持await代码中的Logical[Get|Set]Data。详情on my blog

答案 1 :(得分: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);
}

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

[CompilerGenerated]
private struct <Start>d__0 : IAsyncStateMachine
{
    // ... lots more here not shown
    public string <secret>5__1;
}

答案 2 :(得分:7)

要在同一个线程上执行任务继续,需要同步提供程序。这是一个昂贵的词,简单的诊断是通过在调试器中查看System.Threading.SynchronizationContext.Current的值。

控制台模式应用中该值为 null 。没有提供程序可以在控制台模式应用程序中的特定线程上运行代码。只有Winforms或WPF应用程序或ASP.NET应用程序才有提供程序。而且只在他们的主线上。

这些应用程序的主要线程非常特殊,它们有一个调度程序循环(也就是消息循环或消息泵)。这实现了producer-consumer problem的一般解决方案。这是一个调度程序循环,允许交给一个线程执行一些工作。这样一点工作将是await表达式之后的任务延续。该位将在调度程序线程上运行。

WindowsFormsSynchronizationContext是Winforms应用程序的同步提供程序。它使用Control.Begin / Invoke()来分派请求。对于WPF,它是DispatcherSynchronizationContext类,它使用Dispatcher.Begin / Invoke()来分派请求。对于ASP.NET,它是AspNetSynchronizationContext类,它使用不可见的内部管道。他们在初始化时创建各自提供者的实例,并将其分配给SynchronizationContext.Current

控制台模式应用程序没有这样的提供程序。主要是因为主线程完全不适合,它不使用调度程序循环。您可以创建自己的,然后也创建自己的SynchronizationContext派生类。很难做到,你不能再调用Console.ReadLine(),因为它完全冻结了Windows调用中的主线程。您的控制台模式应用程序将停止作为控制台应用程序,它将开始类似于Winforms应用程序。

请注意,这些运行时环境具有同步提供程序是有充分理由的。他们拥有一个,因为GUI从根本上说是线程不安全的。控制台没有问题,它是线程安全的。

答案 3 :(得分:0)

查看此thread

在标有ThreadStaticAttribute的字段上,初始化仅在静态构造函数中发生一次。在您的代码中创建ID为11的新线程时,将创建一个新的Secret字段,但它是空/ null。在等待调用后返回“开始”任务时,任务将在第11个线程上完成(如打印输出所示),因此该字符串为空。

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

答案 4 :(得分:0)

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]