CurrentCulture с async/await, пользовательский контекст синхронизации

У меня есть веб-приложение, и я использую много асинхронных операций, используя async/wait. Все работало нормально, но когда я создавал пользовательские задачи для параллельной работы нескольких вещей, я заметил, что в рамках этой задачи текущая культура меняется после ожидания. Проблема заключается в том, что threadpool использует культуру операционной системы, которая отличается от культуры запроса и что синхронизация по умолчанию не обновляет культуру даже при изменении культуры текущего потока в задаче.

Итак, я создаю настраиваемый контекст синхронизации:

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-культура де-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 и ждут, или задачи хороши или нет в моей ситуации.

Ответ 1

когда я создал пользовательские задачи для параллельной работы нескольких элементов

Важно различать одновременное (одновременное выполнение нескольких вещей) и параллельное (используя несколько потоков для одновременного выполнения нескольких операций с ЦП).

Я заметил, что в рамках этой задачи текущая культура меняется после ожидания.

Если вы выполняете параллельный код (т.е. Parallel или Task.Run), то ваши операции должны быть связаны с ЦП, и они не должны содержать await вообще.

Это хорошая идея, чтобы избежать параллельного кода на сервере. Task.Run на ASP.NET почти всегда является ошибкой.

Ваш примерный код выполняет асинхронную работу (DownloadStringTaskAsync) в фоновом потоке (Task.Run) и синхронно блокирует его (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);

Ответ 2

Я не могу поверить, что текущая культура не течет как часть ExecutionContext. Это позор.

Недавно я просматривал сборки идентификаторов, когда заметил, что все асинхронные вызовы используют расширение, которое реализовано в Microsoft.AspNet.Identity.TaskExtensions, но оно внутренне. Скопируйте и вставьте его с помощью Resharper, IlSpy и т.д. Он используется следующим образом: await AsyncMethod().WithCurrentCulture().

Это самая полная реализация, которую я видел до сих пор, и она исходит из надежного источника.

ОБНОВЛЕНИЕ: эта проблема исправлена ​​в .NET 4.6! docs были обновлены, но также сделали быструю проверку, чтобы подтвердить это.

Ответ 3

Как указано Stephen answer, вы не должны использовать Task.Run при обработке запроса ASP.NET. Он почти всегда избыточен, только добавляет накладные расходы на поток и повреждает масштабируемость вашего веб-приложения. Даже если вы выполняете работу с процессором, вы должны подумать дважды, прежде чем распараллелить его в контексте ASP.NET, так как вы уменьшите количество одновременных запросов на стороне клиента, которые может обрабатывать ваше веб-приложение.

В противном случае Культура будет правильно протекать на 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 CultureAwaiter, который не требует установки настраиваемого контекста синхронизации.