Использование переменных ThreadStatic с async/await
Новые ключевые слова async / await в C# теперь влияют на то, как (и когда) вы используете данные ThreadStatic, поскольку делегат обратного вызова выполняется в другом потоке, отличном от async
операция началась. Например, следующее простое консольное приложение:
[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);
}
выведет что-то по линии:
Started on thread [9]
Secret is [moo moo]
Was on thread [9]
Now on thread [11]
Finished on thread [11]
Secret is []
Я также экспериментировал с использованием CallContext.SetData
а также CallContext.GetData
и получил такое же поведение.
После прочтения некоторых связанных вопросов и тем:
- CallContext против ThreadStatic
- http://forum.springframework.net/showthread.php?572-CallContext-vs-ThreadStatic-vs-HttpContext&highlight=LogicalThreadContext
- http://piers7.blogspot.co.uk/2005/11/threadstatic-callcontext-and_02.html
кажется, что фреймворки, такие как ASP.Net, явно переносят HttpContext между потоками, но не CallContext
так что, возможно, то же самое происходит здесь с использованием async
а также await
ключевые слова?
С учетом использования ключевых слов async / await, каков наилучший способ хранения данных, связанных с конкретным потоком выполнения, которые можно (автоматически!) Восстановить в потоке обратного вызова?
Спасибо,
4 ответа
Вы могли бы использовать CallContext.LogicalSetData
а также CallContext.LogicalGetData
, но я рекомендую не делать этого, потому что они не поддерживают никакого "клонирования", когда вы используете простой параллелизм (Task.WhenAny
/ Task.WhenAll
).
Я открыл запрос UserVoice для более полного async
-совместимый "контекст", объясненный более подробно в сообщении форума MSDN. Кажется, невозможно построить его самим. У Джона Скита есть хорошая запись в блоге на эту тему.
Итак, я рекомендую вам использовать аргумент, лямбда-замыкания или члены локального экземпляра (this
), как описал Марк.
И да, OperationContext.Current
не сохраняется через await
s.
Обновление:.NET 4.5 поддерживает Logical[Get|Set]Data
в async
код. Подробности в моем блоге.
AsyncLocal
Изменение типа переменной на AsyncLocal, например,
private static AsyncLocal<string> Secret = new AsyncLocal<string>();
дает следующий желаемый результат:
Started on thread [5]
Secret is [moo moo]
Was on thread [5]
Now on thread [6]
Finished on thread [6]
Secret is [moo moo]
По сути, я бы подчеркнул: не делайте этого. [ThreadStatic]
никогда не будет хорошо играть с кодом, который переходит между потоками.
Но ты не обязан. Task
уже несет состояние - на самом деле, это может быть сделано двумя разными способами:
- есть явный объект состояния, который может содержать все, что вам нужно
- лямбда / анон-методы могут образовывать замыкания над состоянием
Кроме того, компилятор в любом случае делает все, что вам нужно:
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);
}
Нет статического состояния; нет проблем с потоками или несколькими задачами. Это просто работает. Обратите внимание, что secret
здесь не просто "местный"; компилятор работал немного вуду, как это происходит с блоками итераторов и захваченными переменными. Проверяя отражатель, я получаю:
[CompilerGenerated]
private struct <Start>d__0 : IAsyncStateMachine
{
// ... lots more here not shown
public string <secret>5__1;
}
Чтобы получить продолжение задачи для выполнения в том же потоке, требуется поставщик синхронизации. Это дорогое слово, простая диагностика - посмотреть на значение System.Threading.SynchronizationContext.Current в отладчике.
Это значение будет нулевым в приложении режима консоли. Не существует поставщика, который мог бы запускать код в определенном потоке в приложении в режиме консоли. Поставщик может иметь только приложение Winforms, WPF или ASP.NET. И только на их основной теме.
Основной поток этих приложений делает что-то особенное, у них есть цикл диспетчера (он же цикл сообщений или насос сообщений). Который реализует общее решение проблемы производитель-потребитель. Именно этот цикл диспетчера позволяет обрабатывать потоку небольшую часть работы. Такая небольшая работа будет продолжением задачи после выражения await. И этот бит будет выполняться в потоке диспетчера.
WindowsFormsSynchronizationContext является поставщиком синхронизации для приложения Winforms. Он использует Control.Begin/Invoke() для отправки запроса. Для WPF это класс DispatcherSynchronizationContext, он использует Dispatcher.Begin/Invoke() для отправки запроса. Для ASP.NET это класс AspNetSynchronizationContext, он использует невидимые внутренние каналы. Они создают экземпляр своих соответствующих провайдеров при инициализации и присваивают его SynchronizationContext.Current.
Для приложения в консольном режиме такого провайдера нет. Прежде всего потому, что основной поток совершенно не подходит, он не использует цикл диспетчера. Вы должны создать свой собственный, а затем создать свой собственный производный класс SynchronizationContext. Трудно сделать, вы не можете больше делать вызовы типа Console.ReadLine(), поскольку это полностью блокирует основной поток при вызове Windows. Ваше приложение в режиме консоли перестает быть консольным приложением, оно начинает напоминать приложение Winforms.
Обратите внимание, что в этих средах выполнения есть веские причины для поставщиков синхронизации. У них должен быть один, потому что GUI в основном потокобезопасен. Не проблема с консолью, она поточно-ориентированная.
Посмотрите на эту тему
в полях, помеченных ThreadStaticAttribute, инициализация будет происходить только один раз. В вашем коде при создании нового потока с идентификатором 11 будет создано новое секретное поле, но оно пустое / пустое, при возврате к задаче "Пуск" задача завершится в потоке 11 (как показывает ваша распечатка) и, следовательно, в строке пустой.
Вы можете решить свою проблему, сохранив Секрет в локальном поле внутри "Пуск" непосредственно перед вызовом Sleepy, а затем восстановите Секрет из локального поля после возвращения из Sleepy. Вы также можете сделать это в Sleepy непосредственно перед вызовом "await Task.Delay(1000);" это фактически вызывает переключение потока.