问题 CurrentCulture with async / await,自定义同步上下文


我有一个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或者任务在我的情况下好或不好,我不想打开讨论。


9621
2018-03-08 18:00


起源

您可以设置默认线程文化 喜欢这个。 - Mike Fuchs


答案:


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

区分之间很重要 同时 (同时做多件事)和 平行 (使用多个线程同时执行多个CPU绑定操作)。

我注意到,在这项任务中,当前的文化在等待之后发生了变化。

如果你正在做 平行 代码(即 Parallel 要么 Task.Run),那么你的操作应该是CPU绑定的,它们不应该包含 await 一点都不

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

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

如果您要进行异步工作,那么您可以使用 async已经原始的如 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);

7
2018-03-08 19:43



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


我无法相信当前的文化不会像现在这样流淌 ExecutionContext。真是太遗憾了。

最近我浏览了身份集,当我注意到所有的异步调用都使用了一个实现的扩展 Microsoft.AspNet.Identity.TaskExtensions,但它是内部的。使用复制并粘贴它 ResharperIlSpy等 它被这样使用: await AsyncMethod().WithCurrentCulture()

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

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


3
2018-06-09 22:12



感谢您提供此信息,但我认为它仍然存在同样的问题。你必须把它放到每个异步方法来使它工作。 - SebastianStehle


正如所指出的那样 斯蒂芬的回答,你不应该使用 Task.Run 处理ASP.NET请求时。它几乎总是多余的,只会增加线程切换开销并损害Web应用程序的可伸缩性。即使你做了一个受CPU限制的工作,在ASP.NET上下文中并行化之前你应该三思而后行,因为你减少了你的web应用程序能够处理的并发客户端请求的数量。

否则,文化得到正确的流动 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进程中重新使用时,这可能会带来一些令人不快的意外。

你可能会更好 斯蒂芬图布的 CultureAwaiter,不需要安装自定义同步上下文。


0
2018-03-08 23:21



半年前我介绍并行化时,我意识到它可能导致可伸缩性问题,但到目前为止它工作得非常好。我可以缩短响应时间(这是一个场景,其中结果通过signalR部分推送到客户端)并且我没有可扩展性问题,但每月有大约3Mio请求。我可能会在以后删除这些任务,特别是当mongo-driver获得对异步操作的支持时,但目前它工作得非常好。感谢您指出我应该重置同步上下文。 - SebastianStehle
我已经看到了与awaiter的解决方案和我不喜欢的是我必须把它放在每个单独等待(可能)并且我有数百个并且还将repsonsiblity移动到一些不应该关心它的类。 - SebastianStehle
@Noseration - 我发现非常奇怪的是asp.net在重用时不会删除同步上下文。关于安全模拟的问题 - 是否也会被重用? - Royi Namir
@RoyiNamir,ASP.NET实际安装并删除每个等待继续的s.context,当它进入和离开连续线程时。正确处理模拟,这里没有安全问题) - Noseratio
好的,我可能会遗漏一些东西。你说 线程返回到池,仍然安装了您的上下文 所以我认为这是件坏事?我的意思是 - 看着 这个  - 当该线程的区域1设置为SC,并且该线程返回以供重用时 - 从粘贴到该重用线程的不同位置执行其他代码 - 将具有相同的SC ??? - Royi Namir