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
, который не требует установки настраиваемого контекста синхронизации.