简体   繁体   English

在 async/await 中使用 ThreadStatic 变量

[英]using ThreadStatic variables with async/await

With the new async/await keywords in C#, there are now impacts to the way (and when) you use ThreadStatic data, because the callback delegate is executed on a different thread to one the async operation started on.使用 C# 中的新 async/await 关键字,现在对您使用 ThreadStatic 数据的方式(以及何时)产生影响,因为回调委托在与启动async操作的线程不同的线程上执行。 For instance, the following simple Console app:例如,以下简单的控制台应用程序:

[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);
}

will output something along the line of:将输出以下内容:

Started on thread [9]
Secret is [moo moo]
Was on thread [9]
Now on thread [11]
Finished on thread [11]
Secret is []

I've also experimented with using CallContext.SetData and CallContext.GetData and got the same behaviour.我还尝试使用CallContext.SetDataCallContext.GetData并得到相同的行为。

After reading some related questions and threads:在阅读了一些相关的问题和线程后:

it seems that frameworks like ASP.Net explicitly migrates the HttpContext across threads, but not the CallContext , so perhaps the same thing is happening here with the use of async and await keywords?似乎像 ASP.Net 这样的框架显式地跨线程迁移 HttpContext ,而不是CallContext ,所以也许在这里使用asyncawait关键字会发生同样的事情?

With the use of the async/await keywords in mind, what's the best way to store data associated with a particular thread of execution that can be (automatically!) restored on the callback thread?考虑到 async/await 关键字的使用,存储与可以(自动!)在回调线程上恢复的特定执行线程相关联的数据的最佳方法是什么?

Thanks,谢谢,

You could use CallContext.LogicalSetData and CallContext.LogicalGetData , but I recommend you don't because they don't support any kind of "cloning" when you use simple parallelism ( Task.WhenAny / Task.WhenAll ).可以使用CallContext.LogicalSetDataCallContext.LogicalGetData ,但我建议您不要使用,因为当您使用简单并行( Task.WhenAny / Task.WhenAll )时,它们不支持任何类型的“克隆”。

I opened a UserVoice request for a more complete async -compatible "context", explained in more detail in an MSDN forum post .我打开了一个UserVoice 请求,以获得更完整的async兼容“上下文”,在MSDN 论坛帖子中进行了更详细的解释。 It does not seem possible to build one ourselves.似乎不可能自己建造一个。 Jon Skeet has a good blog entry on the subject. Jon Skeet 在这个主题上有一个很好的博客条目

So, I recommend you use argument, lambda closures, or the members of the local instance ( this ), as Marc described.因此,我建议您使用参数、lambda 闭包或本地实例 ( this ) 的成员,如 Marc 所述。

And yes, OperationContext.Current is not preserved across await s.是的, OperationContext.Current不会await保留。

Update: .NET 4.5 does support Logical[Get|Set]Data in async code.更新: .NET 4.5 支持async代码中的Logical[Get|Set]Data Details on my blog .详情见我的博客

Basically, I would emphasize: don't do that.基本上,我会强调:不要那样做。 [ThreadStatic] is never going to play nicely with code that jumps between threads. [ThreadStatic]永远不会很好地处理在线程之间跳转的代码。

But you don't have to.但你没有必要。 A Task already carries state - in fact, it can do it 2 different ways:一个Task已经带有状态——事实上,它可以通过两种不同的方式来实现:

  • there's an explicit state object, which can hold everything you need有一个明确的状态对象,它可以容纳你需要的一切
  • lambdas/anon-methods can form closures over state lambdas/anon-methods 可以在 state 上形成闭包

Additionally, the compiler does everything you need here anyway:此外,编译器会在这里完成您需要的一切:

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);
}

No static state;无静态; no issues with threads or multiple tasks.线程或多个任务没有问题。 It just works .只是有效 Note that secret is not just a "local" here;请注意,这里的secret不仅仅是“本地”; the compiler has worked some voodoo, like it does with iterator blocks and captured variables.编译器做了一些伏都教,就像它处理迭代器块和捕获的变量一样。 Checking reflector, I get:检查反射器,我得到:

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

Getting a task continuation to execute on the same thread requires a synchronization provider.使任务继续在同一线程上执行需要同步提供程序。 That's an expensive word, the simple diagnostic is by looking at the value of System.Threading.SynchronizationContext.Current in the debugger.这是一个昂贵的词,简单的诊断是通过查看调试器中 System.Threading.SynchronizationContext.Current 的值。

That value will be null in console mode app.该值在控制台模式应用程序中将为 There is no provider that can make code run on a specific thread in a console mode app.没有提供程序可以使代码在控制台模式应用程序中的特定线程上运行。 Only a Winforms or WPF app or ASP.NET app will have a provider.只有 Winforms 或 WPF 应用程序或 ASP.NET 应用程序才有提供程序。 And only on their main thread.并且只在他们的主线程上。

The main thread of these apps do something very special, they have a dispatcher loop (aka message loop or message pump).这些应用程序的主线程做了一些非常特别的事情,它们有一个调度程序循环(又名消息循环或消息泵)。 Which implements the general solution to the producer-consumer problem .它实现了生产者-消费者问题的一般解决方案。 It is that dispatcher loop that allows handing a thread a bit of work to perform.正是该调度程序循环允许将一些工作交给线程来执行。 Such a bit of work will be the task continuation after the await expression.这样的一点工作将是 await 表达式之后的任务延续。 And that bit will run on the dispatcher thread.该位将在调度程序线程上运行。

The WindowsFormsSynchronizationContext is the synchronization provider for a Winforms app. WindowsFormsSynchronizationContext 是 Winforms 应用的同步提供程序。 It uses Control.Begin/Invoke() to dispatch the request.它使用 Control.Begin/Invoke() 来调度请求。 For WPF it is the DispatcherSynchronizationContext class, it uses Dispatcher.Begin/Invoke() to dispatch the request.对于 WPF,它是 DispatcherSynchronizationContext 类,它使用 Dispatcher.Begin/Invoke() 来调度请求。 For ASP.NET it is the AspNetSynchronizationContext class, it uses invisible internal plumbing.对于 ASP.NET,它是 AspNetSynchronizationContext 类,它使用不可见的内部管道。 They create an instance of their respective providers in their initialization and assign it to SynchronizationContext.Current他们在初始化时创建各自提供程序的实例并将其分配给 SynchronizationContext.Current

There's no such provider for a console mode app.控制台模式应用程序没有这样的提供程序。 Primarily because the main thread is entirely unsuitable, it doesn't use a dispatcher loop.主要是因为主线程完全不合适,它不使用调度程序循环。 You would have create your own, then also create your own SynchronizationContext derived class.您将创建自己的类,然后也创建自己的 SynchronizationContext 派生类。 Hard to do, you can't make a call like Console.ReadLine() anymore since that entirely freezes the main thread on a Windows call.很难做到,你不能再像 Console.ReadLine() 这样的调用,因为它完全冻结了 Windows 调用上的主线程。 Your console mode app stops being a console app, it will start resembling a Winforms app.您的控制台模式应用程序不再是控制台应用程序,它将开始类似于 Winforms 应用程序。

Do note that these runtime environments have synchronization providers for a good reason.请注意,这些运行时环境具有同步提供程序是有充分理由的。 They have to have one because a GUI is fundamentally thread-unsafe.他们必须有一个,因为 GUI 从根本上是线程不安全的。 Not a problem with the Console, it is thread-safe.控制台没有问题,它是线程安全的。

AsyncLocal<T> provides support for maintaining variables scoped to a particular asynchronous code flow. AsyncLocal<T>支持维护范围为特定异步代码流的变量。

Changing the variable type to AsyncLocal, eg,将变量类型更改为 AsyncLocal,例如,

private static AsyncLocal<string> Secret = new AsyncLocal<string>();

gives the following, desired output:给出以下所需的输出:

Started on thread [5]
Secret is [moo moo]
Was on thread [5]
Now on thread [6]
Finished on thread [6]
Secret is [moo moo]

Have a look on this thread看看这个线程

On fields marked with ThreadStaticAttribute, initialization occurs only once, in the static constructor.在标有 ThreadStaticAttribute 的字段上,初始化仅在静态构造函数中发生一次。 In your code when the new thread with ID 11 is created a new Secret field will be created but it is empty / null.在您的代码中,当创建 ID 为 11 的新线程时,将创建一个新的 Secret 字段,但该字段为空/空。 When returning to the "Start" task after the await call the task will finish on thread 11 (as your printout shows) and therefore the string is empty.在等待调用后返回“开始”任务时,该任务将在线程 11 上完成(如您的打印输出所示),因此字符串为空。

You could solve your problem by storing the Secret in a local field inside "Start" just before calling Sleepy, then restore the Secret from the local field after returning from Sleepy.您可以通过在调用 Sleepy 之前将 Secret 存储在“Start”内的本地字段中来解决您的问题,然后在从 Sleepy 返回后从本地字段恢复 Secret。 You could also do it in Sleepy just before you call "await Task.Delay(1000);"您也可以在调用“await Task.Delay(1000);”之前在 Sleepy 中执行此操作。 that actually causes the thread switch.这实际上会导致线程切换。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM