CurrentCulture 与 async/await,自定义同步上下文

Posted

技术标签:

【中文标题】CurrentCulture 与 async/await,自定义同步上下文【英文标题】:CurrentCulture with async/await, Custom synchronization context 【发布时间】:2014-03-08 18:00:14 【问题描述】:

我有一个 Web 应用程序,我通过 async/await 使用了很多异步操作。一切正常,但是当我创建自定义任务以并行运行多个事物时,我注意到,在此任务中,当前文化在等待后会发生变化。问题似乎是,线程池使用操作系统的文化,这与请求的文化不同,并且默认同步不会更新文化,即使在任务中更改当前线程的文化也是如此。

所以我创建了一个自定义同步上下文:

public sealed class CulturePreservingSynchronizationContext : SynchronizationContext

    private CultureInfo culture;
    private CultureInfo cultureUI;

    public CulturePreservingSynchronizationContext()
    
        GetCulture();
    

    public void MakeCurrent()
    
        SetCulture();

        SynchronizationContext.SetSynchronizationContext(CreateCopy());
    

    public override SynchronizationContext CreateCopy()
    
        CulturePreservingSynchronizationContext clonedContext = new CulturePreservingSynchronizationContext();
        clonedContext.culture = culture;
        clonedContext.cultureUI = cultureUI;

        return clonedContext;
    

    public override void Post(SendOrPostCallback d, object state)
    
        base.Post(s =>
        
            SetCulture();
            d(s);
        , state);
    

    public override void OperationStarted()
    
        GetCulture();

        base.OperationStarted();
    

    private void SetCulture()
    
        Thread.CurrentThread.CurrentCulture = culture;
        Thread.CurrentThread.CurrentUICulture = cultureUI;
    

    private void GetCulture()
    
        culture = CultureInfo.CurrentCulture;
        cultureUI = CultureInfo.CurrentUICulture;
    

你可以这样使用它。在我的简单示例中,它运行良好,但我对评估我的方法的细节没有真正的了解(顺便说一句:我的 os-culture 是 de-DE)。请注意,这只是一个示例,与实际代码无关。我刚刚写了这篇文章来证明等待之后的文化与等待之前的文化是不同的。

    Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");

    CulturePreservingSyncContext context = new CulturePreservingSyncContext();

    Task.Run(async () =>
    
        context.MakeCurrent();

        Console.WriteLine(CultureInfo.CurrentCulture);

        WebClient client = new WebClient();
        string s = await client.DownloadStringTaskAsync(new Uri("http://www.google.de"));

        Console.WriteLine(CultureInfo.CurrentCulture);

    ).Wait();

非常欢迎任何建议来了解同步上下文的实现是否良好,如果没有,是否有更好的解决方案。如果 async 和 await 或任务在我的情况下是好是坏,我不想展开讨论。

【问题讨论】:

可以设置默认线程文化like this。 【参考方案1】:

当我创建自定义任务以并行运行多个事物时

区分并发(同时做多件事情)和并行(使用多个线程同时做多个CPU密集型操作)很重要.

我注意到,在此任务中,当前文化会在等待后发生变化。

如果您正在执行并行代码(即ParallelTask.Run),那么您的操作应该受CPU 限制,并且它们根本不应该包含await .

最好避免在服务器上使用并行代码。 Task.Run 在 ASP.NET 上几乎总是一个错误。

您的示例代码在后台线程 (Task.Run) 中执行异步工作 (DownloadStringTaskAsync) 并同步阻塞它 (Wait)。这没有任何意义。

如果您有异步工作要做,那么您可以只使用async-ready 原语,例如Task.WhenAll。以下代码在 ASP.NET 上运行时将保留区域性,并将同时执行三个下载:

private async Task TestCultureAsync()

  Debug.WriteLine(CultureInfo.CurrentCulture);

  WebClient client = new WebClient();
  string s = await client.DownloadStringTaskAsync(new Uri("http://www.google.de"));

  Debug.WriteLine(CultureInfo.CurrentCulture);


var task1 = TestCultureAsync();
var task2 = TestCultureAsync();
var task3 = TestCultureAsync();
await Task.WhenAll(task1, task2, task3);

【讨论】:

示例代码与我的网络应用程序无关,它是一个用于演示目的的控制台应用程序。我刚刚将信息添加到我的问题中。在我的情况下,我并行运行大约 10 个操作。它们包括 IO 和 cpu 绑定操作,例如发出 Web 请求、处理结果、将结果写入数据库(这是同步的,因为 mongodb 驱动程序不好)。所以,在我看来,这是有道理的并且表现得很好。每当您更改当前区域性然后进行一些异步操作时,都会存在区域性问题。 我的建议还是一样。使用 Task.WhenAll 而不是多个线程来处理任何 I/O 绑定。 CPU 密集型工作是否同步而不是并行工作。 总的来说我完全同意,但是在当前的上下文中讨论这个没有看真实的代码和架构是没有意义的。这就像一个建议“使用属性而不是公共字段”。在大多数情况下,使用属性会更好,但在某些情况下,当您使用字段而不是属性(如数学计算)时,性能会产生巨大差异。但我的错误是不问具体问题。我想在同步上下文中获得一些反馈。对此感到抱歉。顺便说一句:你有一个有趣的博客。 我知道这是旧的,但提供的示例代码实际上并不正确。 writelines 将在调用 TestCultureAsync 的线程中执行,然后它将转到另一个线程进行下载,然后返回原始线程进行第二个 writeline。如果你用返回 Task.Run 包装整个事情,你会看到原始帖子描述的问题 @Alpine: TestCultureAsync 可能会或可能不会在同一个线程上恢复。下载没有“不同的线程”。【参考方案2】:

我不敢相信当前的文化没有成为ExecutionContext 的一部分。真可惜。

最近我在浏览身份程序集时,注意到所有异步调用都使用在Microsoft.AspNet.Identity.TaskExtensions 中实现的扩展,但它是内部的。使用ResharperIlSpy 等从那里复制并粘贴它。 它是这样使用的: await AsyncMethod().WithCurrentCulture().

这是迄今为止我见过的最完整的实现,它来自受信任的来源。

更新:此问题已在 .NET 4.6 中修复! docs 已更新,但也进行了快速测试以确认这一点。

【讨论】:

感谢您提供此信息,但我认为它仍然存在同样的问题。你必须把它放到每一个异步方法中才能让它工作。【参考方案3】:

正如Stephen's answer 所指出的,在处理 ASP.NET 请求时不应使用Task.Run。它几乎总是多余的,只会增加线程切换开销并损害 Web 应用程序的可伸缩性。即使您执行 CPU 密集型工作,在 ASP.NET 上下文中并行化它之前,您也应该三思而后行,因为您会减少您的 Web 应用程序可以处理的并发客户端请求的数量。

否则,Culture 将由 AspNetSynchronizationContext 正确流动。此外,您的代码还有另一个问题:

Task.Run(async () =>

    context.MakeCurrent();

    Console.WriteLine(CultureInfo.CurrentCulture);

    WebClient client = new WebClient();
    string s = await client.DownloadStringTaskAsync(new Uri("http://www.google.de"));

    Console.WriteLine(CultureInfo.CurrentCulture);

).Wait();

您在池线程上安装了自定义同步上下文,但没有将其删除。因此,线程会在您的上下文仍在安装的情况下返回到池中。当此线程稍后在同一个 ASP.NET 进程中重用时,这可能会带来一些令人不快的意外。

Stephen Toub's CultureAwaiter 可能会更好,它不需要安装自定义同步上下文。

【讨论】:

半年前我介绍并行化的时候,我就意识到它会导致可伸缩性问题,但到目前为止它工作得很好。我可以改进响应时间(这是一种情况,其中结果部分通过 signalR 推送到客户端)并且我没有可伸缩性问题,每月大约 3Mio 请求。我可能会在稍后删除这些任务,尤其是当 mongo-driver 获得对异步操作的支持但目前它运行良好时。感谢您指出我应该重置同步上下文。 我已经看到了等待者的解决方案,我不喜欢的是我必须把它放在每个等待(可能)上,我有数百个,并且还将责任转移到一些类不应该关心它。 @Noseration - 我觉得很奇怪 asp.net 在重用时不会删除同步上下文。安全模拟怎么样 - 也会被重用吗? @RoyiNamir,当它进入和离开延续线程时,ASP.NET 实际上会为每个等待延续安装和删除 s.context。模拟处理正确,这里没有安全问题) 好的,我可能在这里遗漏了一些东西。你说线程回到池中,你的上下文仍在安装中所以我认为这是一件坏事?我的意思是 - 查看this - 当该线程的区域 1 设置为 SC 并且线程返回以供重用时 - 执行来自不同位置的其他代码,这些代码粘在该重用线程上 - 将具有相同的 SC ???

以上是关于CurrentCulture 与 async/await,自定义同步上下文的主要内容,如果未能解决你的问题,请参考以下文章

CurrentCulture 与 async/await,自定义同步上下文

在 .Net 中:将 CurrentCulture 保持在新线程上的最佳方式?

Async / await vs then 哪个最适合性能?

对 WCF 服务的异步调用不会保留 CurrentCulture

对 WCF 服务的异步调用不会保留 CurrentCulture

String.format 根据 CurrentCulture 更改十进制变量