Языки и рамки развиваются. Мы, как разработчики, должны постоянно узнавать что-то новое и отучиться от уже освоенных знаний. Говоря за себя, обучение является самой трудной частью непрерывного обучения. Когда я впервые столкнулся с многопоточными приложениями в .NET, я наткнулся на атрибут ThreadStatic . Я помнил, что этот атрибут особенно полезен, когда у вас есть статические поля, которые не должны быть общими для потоков. В то время, когда была выпущена .NET Framework 4.0, я обнаружил класс ThreadLocal и то, как он лучше справляется с назначением значений по умолчанию для данных, специфичных для потока. Итак, я отучился ThreadStaticAttribute
, отдавая предпочтение ThreadLocal<T>
.
Быстро перенесемся на некоторое время спустя, когда я начал копаться async/await
. Я стал жертвой убеждения, что данные, относящиеся к потокам, все еще работают. Итак, я снова был неправ, и мне пришлось снова учиться! Если бы я только знал об AsyncLocal ранее.
Давайте учиться и не учиться вместе!
TL; DR
- А
Task
это не аThread
.Task
это будущее или обещание, которое в конечном итоге исполняется работникомThread
. - Если вам нужны окружающие данные, локальные для асинхронного потока управления, например, для кэширования каналов связи WCF, используйте
AsyncLocal
вместо .NET 4.6 или Core CLRThreadStaticAttribute
илиThreadLocal
предоставьте его.
Ложки нет
Некоторые вещи просто запоминаются. Когда мир вокруг нас развивается, эти вещи могут перестать быть правдой. Все мы, кто начинал программировать на .NET в эпоху, предшествующую .NET 4.0, синтезировали знания о потоках и их полезности при параллельном выполнении интенсивных операций. Мы знали разницу между потоками переднего плана и фона и насколько важно не блокировать поток пользовательского интерфейса.
Затем была выпущена библиотека параллельных задач, которая перевернула наш мир с ног на голову. Глядя на это System.Threading.Tasks.Task
, мы внезапно почувствовали себя как Нео из Матрицы и поняли, что ложки нет — или, в нашем контексте, нет Thread
!
Я не первый, кто использует аналогию Matrix для описания разницы между a Thread
и a Task
. У Стивена Клири отличная публикация. Нет темы от 2013 года, которая использует ту же аналогию и углубляется в различия. Я настоятельно рекомендую прочитать это.
А Task
под параллельной библиотекой задач — это будущее или обещание . А Task
это то, что вы хотите сделать. Напротив, это Thread
один из многих возможных работников, которые могут выполнить эту задачу. По контракту a Task
мы не знаем, будет ли это запланировано, будет ли оно выполнено немедленно или уже выполнено в тот момент, когда мы объявили это. Среда выполнения параллельной библиотеки задач имеет встроенные смарт-коды, позволяющие решить, будет ли задача выполняться в потоке, который ее создал, или ее нужно запланировать в пуле рабочих потоков или пуле потоков ввода-вывода. Более того, то, что поток работал над заданной задачей, не означает, что поток выполнит все продолжения этой задачи.
Это становится еще сложнее, когда мы начинаем вводить async / await в уравнение. Каждый раз, когда вы пишете await
оператор вместе с вашим другомConfigureAwait(false)
, поток, выполняющий задачу в данный момент, может вернуться назад и начать выполнять несколько других задач. Когда операция ввода / вывода завершается, оставшаяся часть задачи (продолжение) снова назначается Task
ответственным в данный момент TaskScheduler
. Ранее ответственный поток мог взять эту задачу и продолжить работу над ней. Альтернативно, любой другой доступный поток мог сделать это. Следующий код иллюстрирует это:
static dynamic Local;
static ThreadLocal<string> ThreadLocal = new ThreadLocal<string>(() => "Initial Value");
public async Task ThereIsNoSpoon()
{
// Assign the ThreadLocal to the dynamic field
Local = ThreadLocal;
Console.WriteLine($"Before TopOne: '{Local.Value}'");
await TopOne().ConfigureAwait(false);
Console.WriteLine($"After TopOne: '{Local.Value}'");
await TopTen().ConfigureAwait(false);
Console.WriteLine($"After TopTen: '{Local.Value}'");
}
static async Task TopOne()
{
await Task.Delay(10).ConfigureAwait(false);
Local.Value = "ValueSetBy TopOne";
await Somewhere().ConfigureAwait(false);
}
static async Task TopTen()
{
await Task.Delay(10).ConfigureAwait(false);
Local.Value = "ValueSetBy TopTen";
await Somewhere().ConfigureAwait(false);
}
static async Task Somewhere()
{
await Task.Delay(10).ConfigureAwait(false);
Console.WriteLine($"Inside Somewhere: '{Local.Value}'");
await Task.Delay(10).ConfigureAwait(false);
await DeepDown();
}
static async Task DeepDown()
{
await Task.Delay(10).ConfigureAwait(false);
Console.WriteLine($"Inside DeepDown: '{Local.Value}'");
Fire().Ignore();
}
static async Task Fire()
{
await Task.Yield();
Console.WriteLine($"Inside Fire: '{Local.Value}'");
}
Вышеприведенный код должен быть относительно простым для понимания. Точкой входа является метод ThereIsNoSpoon
, который вызывает два метода TopOne
и TopTen
. Эти методы устанавливают ThreadLocal
значение ValueSetBy TopOne
и ValueSetBy TopTen
соответственно. Оба метода вызывают метод Somewhere
, который выводит значение ThreadLocal
и вызывает в DeepDown
. DeepDown
печатает значение ThreadLocal
снова, а затем запускает асинхронный метод, вызванный, Fire
не ожидая его (следовательно, метод Ignore
, который подавляет предупреждение компилятора CS4014 ). Этот код использует крошечный трюк, который позволяет позже использовать код для демонстрации AsyncLocal
. Методы, используемые в пути выполнения, обращаются к динамическому статическому полю Local
. Оба класса обеспечиваютValue
собственность и, следовательно, мы можем просто назначить либо ThreadLocal
или AsyncLocal
без дублирования ненужного кода.
Вывод приведенного выше кода выглядит примерно так (ваш результат может отличаться):
Before TopOne: 'Initial Value'
Inside Somewhere: 'ValueSetBy TopOne'
Inside DeepDown: 'ValueSetBy TopOne'
After TopOne: 'ValueSetBy TopOne'
Inside Fire: 'Initial Value'
Inside Somewhere: 'Initial Value'
Inside DeepDown: 'ValueSetBy TopTen'
Inside Fire: 'Initial Value'
After TopTen: 'ValueSetBy TopTen'
Перед выполнением TopOne
метода значение ThreadLocal
имеет значение по умолчанию. Сам метод присваивает значение Initial Value
к ThreadLocal
. Это значение остается до тех пор, пока Fire
метод не будет запланирован без ожидания. В нашем случае Fire
метод назначается по умолчанию TaskScheduler
, ThreadPool. Поэтому ранее назначенное значение больше не связано с ThreadLocal
, что приводит к тому, что Fire
метод может только прочитать ThreadLocal
значение по умолчанию. В нашем примере чтение начального значения не имеет последствий. Но что, если вы использовали ThreadLocal
для кеширования дорогостоящих объектов связи, таких как каналы WCF? Можно предположить, что эти дорогие объекты кэшируются и используются повторно при выполненииFire
метод, но они не будут. Это создаст опасный горячий путь в вашей кодовой базе, яростно создавая новые дорогие объекты с каждым вызовом. Вам было бы трудно это обнаружить, пока ваша корзина покупок не произвела впечатляющий эффект в самый напряженный день сезона праздничных покупок.
Короче говоря: а Task
не а Thread
. Вместе с тем async/await
, мы должны попрощаться с локальными данными потока! Скажи привет AsyncLocal
.
Согни это своим разумом
AsyncLocal<T>
это класс, представленный в .NET Framework 4.6 (также доступный в новом CoreCLR ). Согласно MSDN , AsyncLocal<T>
«представляет окружающие данные, которые являются локальными для данного асинхронного потока управления».
Давайте расшифруем это утверждение. Асинхронный поток управления может рассматриваться как стек вызовов цепочки вызовов асинхронных методов. В приведенном выше примере асинхронный поток управления из представления AsyncLocal
запускается, когда мы устанавливаем Value
свойство. Таким образом, два потока управления будут
Поток 1: TopOne> Где-то> DeepDown> Огонь
Поток 2: TopTen> Где-то> DeepDown> Fire
Итак, обещание AsyncLocal
состоит в том, что мы можем присвоить ему значение, которое присутствует, пока мы находимся в одном и том же асинхронном потоке управления. Мы докажем это с помощью следующего кода.
static AsyncLocal<string> AsyncLocal = new AsyncLocal<string> { Value = "Initial Value" };
public async Task BendItWithYourMind()
{
// Assign the AsyncLocal to the dynamic field
Local = AsyncLocal;
// This code is the same as before but shown again for clarity
Console.WriteLine($"Before TopOne: '{Local.Value}'");
await TopOne().ConfigureAwait(false);
Console.WriteLine($"After TopOne: '{Local.Value}'");
await TopTen().ConfigureAwait(false);
Console.WriteLine($"After TopTen: '{Local.Value}'");
}
Вывод приведенного выше кода выглядит примерно так:
Before TopOne: 'Initial Value'
Inside Somewhere: 'ValueSetBy TopOne'
Inside DeepDown: 'ValueSetBy TopOne'
After TopOne: 'Initial Value'
Inside Fire: 'ValueSetBy TopOne'
Inside Somewhere: 'ValueSetBy TopTen'
Inside DeepDown: 'ValueSetBy TopTen'
Inside Fire: 'ValueSetBy TopTen'
After TopTen: 'Initial Value'
Как мы видим, код в конечном итоге ведет себя так, как мы ожидали с самого начала. Вне асинхронного потока управления AsyncLocal
значение по умолчанию «Начальное значение». Как только мы присваиваем Value
свойство, оно остается установленным, даже если мы вызываем Fire
метод, не ожидая его.
Иногда Локально просто не достаточно Локально
Мы только что видели, как [ThreadStaticAttribute]
или ThreadLocal
больше не работает, как ожидалось, когда мы объединяем его с асинхронным кодом с использованием async/await
ключевых слов. Итак, если вы хотите создать надежный код, который должен иметь доступ к окружающим данным, локальным по отношению к текущему асинхронному потоку управления, вы должны использовать AsyncLocal
и обновить его до .NET 4.6 или Core CLR. Если вы не можете обновить свой проект до одной из этих версий платформы, вы можете попробовать имитировать AsyncLocal
, используяExecutionContext
вместо этого.
Теперь, когда вы видели AsyncLocal
, позвольте мне сказать вам, что обучение еще не закончилось! В следующей части я покажу вам, как вы можете реструктурировать существующий код, чтобы вам больше не требовались окружающие данные. Оставайтесь с нами и не сгибайте слишком много ложек!