Используя переменные ThreadStatic с async/wait
Новые ключевые слова async/await в С# теперь влияют на то, как (и когда) вы используете данные 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
и получил то же самое поведение.
После прочтения некоторых связанных вопросов и тем:
кажется, что фреймворки, такие как ASP.Net, явно переносят HttpContext между потоками, но не CallContext
, поэтому, возможно, здесь происходит то же самое с использованием ключевых слов async
и await
?
С учетом использования ключевых слов async/await, как лучше всего хранить данные, связанные с конкретным потоком выполнения, которые можно (автоматически!) Восстановить в потоке обратного вызова?
Спасибо,
Ответы
Ответ 1
Вы можете использовать CallContext.LogicalSetData
и CallContext.LogicalGetData
, но я рекомендую вас не потому, что они не поддерживают какой-либо "клонирование" при использовании простого parallelism (Task.WhenAny
/Task.WhenAll
).
Я открыл запрос UserVoice для более полного async
-совместимого "контекста", более подробно описанного в сообщение форума MSDN. Кажется невозможным построить один самостоятельно. У Jon Skeet есть хорошая запись в блоге по этому вопросу.
Итак, я рекомендую использовать аргумент, лямбда-закрытие или члены локального экземпляра (this
), как описано в Marc.
И да, OperationContext.Current
не сохраняется на await
s.
Обновление:..NET 4.5 поддерживает Logical[Get|Set]Data
в async
. Подробности в моем блоге.
Ответ 2
В принципе, я хотел бы подчеркнуть: не делайте этого. [ThreadStatic]
никогда не будет хорошо играть с кодом, который перескакивает между потоками.
Но вам это не нужно. A Task
уже имеет состояние - фактически, он может сделать это двумя разными способами:
- есть явный объект состояния, который может содержать все, что вам нужно
- lambdas/anon-methods могут образовывать замыкания по состоянию
Кроме того, компилятор делает все, что вам нужно здесь:
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;
}
Ответ 3
Для продолжения выполнения задачи в одном потоке требуется поставщик синхронизации. Это дорогое слово, простая диагностика - это посмотреть на значение System.Threading.SynchronizationContext.Current в отладчике.
Это значение будет null в приложении консоли. Существует не провайдер, который может сделать код запуска в определенном потоке в приложении режима консоли. Только приложение Winforms или WPF или приложение ASP.NET будут иметь поставщика. И только по их основной теме.
Основной поток этих приложений делает что-то очень особенное, у них есть диспетчерский цикл (например, цикл сообщений или насос сообщений). Что реализует общее решение проблемы производителя-потребителя. Это тот цикл диспетчера, который позволяет передавать потоку некоторую работу для выполнения. Такая небольшая работа будет продолжением задачи после выражения ожидания. И этот бит будет работать в потоке диспетчера.
WindowsFormsSynchronizationContext является поставщиком синхронизации для приложения Winforms. Он использует Control.Begin/Invoke() для отправки запроса. Для WPF это класс DispatcherSynchronizationContext, он использует Dispatcher.Begin/Invoke() для отправки запроса. Для ASP.NET это класс AspNetSynchronizationContext, он использует невидимую внутреннюю сантехнику. Они создают экземпляр своих соответствующих поставщиков при их инициализации и назначают его SynchronizationContext.Current
Нет такого провайдера для приложения в консольном режиме. Прежде всего потому, что основной поток полностью непригоден, он не использует цикл диспетчера. Вы бы создали свой собственный, а затем создали свой собственный производный класс SynchronizationContext. Трудно сделать, вы не можете сделать вызов, например Console.ReadLine(), поскольку это полностью замораживает основной поток при вызове Windows. Приложение в консольном режиме перестает быть консольным приложением, оно начнет напоминать приложение Winforms.
Обратите внимание, что в этих средах среды выполнения есть поставщики синхронизации по уважительной причине. У них есть, потому что графический интерфейс является принципиально небезопасным. Не проблема с консолью, она является потокобезопасной.
Ответ 4
Посмотрите на эту тему
В полях, помеченных ThreadStaticAttribute, инициализация происходит только один раз в статическом конструкторе. В вашем коде при создании нового потока с идентификатором 11 будет создано новое секретное поле, но оно будет пустым/пустым. При возврате к задаче "Пуск" после вызова await задача завершится в потоке 11 (как показывает ваша распечатка), и поэтому строка пуста.
Вы можете решить свою проблему, сохранив Секрет в локальном поле внутри "Пуск" непосредственно перед вызовом Sleepy, а затем восстановите Секрет из локального поля после возвращения из Sleepy. Вы также можете сделать это в Sleepy непосредственно перед вызовом "await Task.Delay(1000);" это фактически вызывает переключение потока.
Ответ 5
AsyncLocal <T> предоставляет поддержку для поддержки переменных, ограниченных определенным потоком асинхронного кода.
Изменение типа переменной на 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]