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

После прочтения некоторых связанных вопросов и тем:

кажется, что фреймворки, такие как ASP.Net, явно переносят HttpContext между потоками, но не CallContextтак что, возможно, то же самое происходит здесь с использованием async а также await ключевые слова?

С учетом использования ключевых слов async / await, каков наилучший способ хранения данных, связанных с конкретным потоком выполнения, которые можно (автоматически!) Восстановить в потоке обратного вызова?

Спасибо,

4 ответа

Вы могли бы использовать CallContext.LogicalSetData а также CallContext.LogicalGetData, но я рекомендую не делать этого, потому что они не поддерживают никакого "клонирования", когда вы используете простой параллелизм (Task.WhenAny / Task.WhenAll).

Я открыл запрос UserVoice для более полного async-совместимый "контекст", объясненный более подробно в сообщении форума MSDN. Кажется, невозможно построить его самим. У Джона Скита есть хорошая запись в блоге на эту тему.

Итак, я рекомендую вам использовать аргумент, лямбда-замыкания или члены локального экземпляра (this), как описал Марк.

И да, OperationContext.Current не сохраняется через awaits.

Обновление:.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);" это фактически вызывает переключение потока.

Другие вопросы по тегам