38

使用 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.SetDataandCallContext.GetData并得到了相同的行为。

在阅读了一些相关的问题和主题后:

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

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

谢谢,

4

5 回答 5

31

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

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

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

是的,不会在s 中OperationContext.Current保留。await

更新: .NET 4.5 确实支持代码Logical[Get|Set]Data我的博客上的async详细信息。

于 2012-10-22T12:39:54.277 回答
9

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

但你不必这样做。ATask已经携带状态 - 事实上,它可以通过两种不同的方式进行:

  • 有一个明确的状态对象,它可以容纳你需要的一切
  • 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;
}
于 2012-10-22T11:39:13.597 回答
8

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]
于 2019-06-30T13:15:32.740 回答
7

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

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

这些应用程序的主线程做了一些非常特别的事情,它们有一个调度程序循环(又名消息循环或消息泵)。它实现了生产者-消费者问题的一般解决方案。正是调度程序循环允许处理线程执行一些工作。这样的工作将是等待表达式之后的任务继续。该位将在调度程序线程上运行。

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

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

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

于 2012-10-22T12:06:45.347 回答
0

看看这个线程

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

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

于 2017-02-10T11:41:26.447 回答