使用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
,所以使用async
和await
可能会发生同样的事情。关键字?
考虑到使用async / await关键字,存储与特定执行线程相关联的数据的最佳方法是什么,可以(自动!)在回调线程上恢复?
谢谢,
答案 0 :(得分:25)
你可以使用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
在<{1}}之间保留
更新:.NET 4.5支持await
代码中的Logical[Get|Set]Data
。详情on my blog。
答案 1 :(得分:10)
基本上,我要强调:不要那样做。 [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;
}
答案 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]