Используя переменные 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]