Это вторая запись в серии Представление об асинхронном и параллельном программировании в .NET.

Предыдущая запись блога этой сериипоказывает, как задачи планируются и выполняются в потоках в .NET. Предыдущая запись в блоге является обязательным условием для этой записи в блоге.

Если мы вспомним последнее сообщение в блоге, подход Thread-Per-Request выполняет каждый запрос в выделенном потоке, тогда как в асинхронном подходе на основе задач выполнение одного и того же запроса не гарантируется. выполняется в том же потоке.

И это основное различие между этими двумя подходами, которое заставляет нас думать, проектировать и делать что-то по-другому.

Сегодня мы обсудим проблему, решение которой в этих двух подходах решается по-разному. Описанная здесь проблема рассматривается с точки зрения сервера.

Давайте начнем…

Как получить доступ к данным запроса из любого места? (Проблема):

На сервере некоторые данные/информация специфичны для запроса. Например, когда пользователь приходит, мы получаем информацию о пользователе из заявки, а иногда мы извлекаем дополнительную информацию из базы данных.

Обычно мы получаем такую ​​информацию внутри промежуточного программного обеспечения или класса контроллера.

Однако эта информация может понадобиться нам позже, чем контроллеру или промежуточному программному обеспечению, например, внутри класса бизнес-логики.

Теперь вопрос в том, как мы можем передать данные в класс бизнес-логики из контроллера или промежуточного программного обеспечения авторизации?

Наивным решением было бы передавать данные повсюду в качестве параметра метода. Но при таком подходе вам нужно взять на себя бремя передачи его в методы, которые его не используют. Более того, может существовать множество других данных, специфичных для запроса, и обработка их всех таким образом невозможна.

Запросить конкретный глобальный контекст (идея):

Более осуществимое решение — поддерживать Глобальный контекст запроса для каждого запроса, в котором хранятся данные. Глобальный контекст должен быть доступен из любой задачи этого запроса.

Тогда все, что нам нужно сделать, это сохранить данные в этом Контексте, и когда нам понадобятся данные, мы сможем просто получить их из Контекста.

С помощью такого Контекста мы можем передавать данные откуда угодно куда угодно. Например, от контроллера/промежуточного программного обеспечения к классу бизнес-логики, от задачи, связанной с бизнес-логикой, к другой задаче, связанной с бизнес-логикой, и т. д.

Где хранить глобальный контекст конкретного запроса? (Проблема):

Теперь вопрос в том, где мы храним этот Глобальный контекст запроса, к которому можно получить доступ из любой задачи этого запроса?

В подходе Thread-Per-Request мы могли бы сохранить это в Thread-Local-Storage, поскольку весь запрос обрабатывается одним и тем же потоком.

Но проблема асинхронного программирования на основе задач заключается в том, что задачи одного и того же запроса не гарантируются для выполнения в одном и том же потоке.

Так что здесь мы не можем просто сохранить этот контекст в Thread-Local-Storage.

Решение:

Чтобы решить эту проблему, .NET предоставляет встроенный Глобальный контекст, специфичный для запроса, в котором мы можем хранить данные, специфичные для запроса. И это Контекст выполнения.

Если вам нравится мой контент, вы можете поддержать меня по ссылке ниже.

Контекст выполнения:

Контекст выполнения — это просто хранилище состояний, в котором мы храним данные, специфичные для запроса.

Контекст выполнения не хранится в Thread-Local-Storage; скорее, он прикрепляется к задачам и загружается в поток перед выполнением задачи. Таким образом, он перемещается между потоками. Вот почему данные, хранящиеся в контексте выполнения, могут быть доступны любому потоку того же запроса.

Последовательность действий (захват и восстановление) контекста выполнения:

При каждом создании задачи контекст выполнения родительской задачи захватывается и сохраняется во внутренней переменной вновь созданной задачи.

И перед выполнением этой задачи в потоке Контекст выполнения восстанавливается в потоке, чтобы Задача могла получить доступ к данным, специфичным для запроса.

Поэтому не имеет значения, в каком потоке выполняется задача; он всегда может получить доступ к конкретным данным запроса благодаря потоку контекста выполнения, описанному выше.

Обычно контекст выполнения фиксируется всякий раз, когда создается новая задача. Например, для точек Task.Run(), Task.ContinueWith(), await и т. д.

Но есть также некоторые API на основе обратного вызова, в которых захватывается ExecutionContext. Например, ThreadPool.QueueUserWorkItem(Callback), CancellationToken.Register(Callback) и т. д.

В большинстве случаев вам не нужно беспокоиться о контексте выполнения и его потоке, но знание того, что происходит «под капотом», поможет вам эффективно и безопасно писать код, а также устранять критические ошибки.

Подавление потока:

Мы можем подавить поток, используя ExecutionContext.SupressFlow, а затем восстановить его позже, используя ExecutionContext.RestoreFlow.

Задачи, созданные между ExecutionContext.SuppressFlow и ExecutionContext.RestoreFlow, не будут захватывать контекст выполнения.

Таким образом, эти задачи будут выполняться без контекста выполнения.

Внимание! Вам следует использовать ExecutionContext.SupressFlow только в том случае, если вы знаете, что делаете, g.

Иногда это делается для оптимизации производительности, когда нам не нужен контекст выполнения. Однако в большинстве случаев лучше позволить контексту выполнения выполняться, а не выполнять преждевременную оптимизацию, которая может привести к ошибкам.

Как установить и получить данные из контекста выполнения:

Мы не можем напрямую манипулировать контекстом выполнения. Мы получаем и устанавливаем данные в контексте выполнения через поля класса типа AsyncLocal. Рекомендуется оставить поле Static.

AsyncLocal:

Мы храним данные в контексте выполнения, используя поле Static AsyncLocal‹T><.

Внутри поля AsyncLocal хранят и извлекают данные из контекста выполнения.

Но почему поле AsyncLocal должно быть статическим? Чтобы понять это, вы должны знать, как AsyncLocal хранит и извлекает данные в контексте выполнения. Об этом говорится в конце поста.

Ошибки AsyncLocal:

Работа с AsyncLocal может привести к ошибкам, если мы не будем достаточно осторожны. Некоторые распространенные ошибки описаны ниже.

Потокобезопасность контекста выполнения:

Если родительская задача создает несколько дочерних задач, то контекст выполнения одной и той же родительской задачи будет передан всем из них. Так что же произойдет, если кто-то внесет какое-то обновление в контекст выполнения? Каково влияние?

Если быть более точным, Обновления могут быть двух типов:

  1. Переназначение значения AsyncLocal.
  2. Изменение объекта, хранящегося в AsyncLocal, вместо повторного использования.

Переназначение значения AsyncLocal:

Контекст выполнения является неизменяемым и использует метод копирования при записи.

По умолчанию контекст выполнения по умолчанию устанавливается как контекст выполнения в потоках.

Когда какое-либо значение AsyncLocal переназначается в каком-либо потоке, новый контекст выполнения создается и устанавливается для этого потока.

Новый контекст выполнения содержит новое/обновленное значение и другие значения из предыдущего контекста выполнения.

Контекст выполнения других потоков остается прежним. Только поток, выполнивший обновление, получает новый контекст выполнения.

Таким образом, переназначение значения AsyncLocal потокобезопасно.

Изменение объекта, хранящегося в AsyncLocal:

Но если вы измените изменяемый объект, хранящийся в AsyncLocal, он не создаст новый ExecutionContext, поэтому все остальные потоки получат изменения этого объекта.

Он не поточно-ориентирован, поскольку несколько потоков могут одновременно обращаться к объекту и изменять его.

Некоторые из вас, возможно, уже заблудились. Но не волнуйтесь, я создал консольное приложение с этими двумя типами примеров.

Получите это отсюда. Код объясняет сам себя, и вы также можете скачать и запустить его, если хотите.

Чтобы обеспечить потокобезопасность при использовании AsyncLocals, следуйте этим практическим правилам в порядке возрастания.

  • Если данные не нужно изменять, сделайте их только для чтения.
  • Если его необходимо изменить, сделайте его Неизменяемым, чтобы изменения в одном потоке не влияли на другой.
  • Если его невозможно сделать неизменяемым, сделайте изменение потокобезопасным. Например, переназначьте новое значение вместо изменения, используйте потокобезопасные коллекции и т. д.

Другие проблемы:

Время существования контекста выполнения и утечка памяти:

Как и другие объекты .NET, контекст выполнения не будет подвергаться сборке мусора, пока на него ссылаются некоторые другие объекты.

Задачи сохраняют ссылку на контекст выполнения, в котором они были созданы. А перед выполнением в поток загружается Контекст выполнения, т. е. ссылка на Контекст выполнения сохраняется во внутренней переменной thread.

Таким образом, здесь контекст выполнения может быть очищен только тогда, когда задача готова к сбору мусора и поток больше не ссылается на контекст выполнения (путем загрузки контекста выполнения другой задачи).

Таким образом, время жизни контекста выполнения более или менее равно запросу, для которого он создан, если даже после отправки ответа не выполняется ни одна независимая задача. Кстати, это нормально, если какая-то независимая задача, содержащая контекст выполнения запроса, выполняется немного дольше, чем время жизни запроса.

Но иногда контексты выполнения могут иметь неоправданно долгое время жизни, что приводит к огромным утечкам памяти. Например, если на контексты выполнения ссылаются одноэлементные экземпляры или что-то, срок жизни которого больше, чем у запроса.

Если мы не будем осторожны, могут возникнуть и другие проблемы.

Например, если мы храним одноразовый объект в Asynclocal и объект удаляется до завершения запроса, то попытка доступа к нему приведет к ошибке.

Гуру .NET Дэвид Фаулер обсудил такие проблемы с дополнительными пояснениями и примерами кода. Вы можете прочитать это здесь".

Пример из реальной жизни:

Предположим, ваш новый проект приложения состоит из нескольких микросервисов. Вы также создали библиотеку классов, которая добавляется в эти микросервисы как DLL. И теперь вы хотите получить доступ к Принципу пользователя из этой библиотеки классов.

Как бы вы это сделали?

Вы можете создать собственное промежуточное программное обеспечение, в котором для пользовательского контекста будет задан Thread.CurrentPrincipal. CurrentPrincipal — это свойство Static класса Thread, которое хранит данные внутри себя в поле Private Static AsyncLocal.

Таким образом, к контексту пользователя можно получить доступ из любой сторонней библиотеки с помощью свойства Thread.CurrentPrincipal.

Почему переменная AsyncLocal должна быть статической:

Возможно, вы были удивлены, узнав, что статические поля используются для хранения данных, специфичных для запроса/данных контекста выполнения.

Поскольку создается только одна копия статического поля, как оно может содержать данные параллельных запросов/данные нескольких контекстов выполнения одновременно?

Давайте обсудим, как AsyncLocal работает и хранит данные в контексте выполнения, чтобы понять, почему работает статическое поле и почему рекомендуется использовать статическое поле.

Если упростить, контекст выполнения — это хранилище состояний, в котором данные хранятся в виде пар ключ-значение. Хотя они и разные, вы можете представить себе «Мешок состояний» как словарь.

Когда мы присваиваем какое-либо значение свойству Value поля AsyncLocal, внутренне запись (пара ключ-значение) добавляется в пакет состояний контекста выполнения, где находится ключ. само поле AsyncLocal, а Значение — это значение, которое мы присвоили свойству Value.

Прочтите это еще раз; поле AsyncLocal не хранит данные; он просто используется как ключ для хранилища состояний, где хранится пара ключ-значение.

Таким образом, использование статического AsyncLocal означает, что ключ всегда один и тот же/общий для разных запросов, а не значение. Для того же ключа значение будет таким же, если контекст выполнения тот же.

Если контекст выполнения отличается, один и тот же ключ получит разные ссылки.

Но это не значит, что вы не можете использовать нестатическое поле. Для нестатических полей вам необходимо убедиться, что ключи установки и получения (поле AsyncLocal) принадлежат как минимум тому же экземпляру класса для одного и того же запроса.

Зависимость от экземпляра класса и поддержание одного и того же экземпляра для метода получения и установки создает дополнительную нагрузку. Вот почему чаще всего используется поле Static AsyncLocal.

Кстати, не рекомендуется выставлять поле напрямую. Обычно мы сохраняем свойство, которое получает и устанавливает поле, и поле остается закрытым. В нашем сценарии мы можем использовать Static Public свойство для получения и установки данных для Static Private поля.

Если вы посмотрите на примеры в этом репозитории, вы увидите, что мы сохранили частное поле AsyncLocal и общедоступное свойство, которое получает и устанавливает частное поле.

Если вам нравится мой контент, вы можете поддержать меня по ссылке ниже.

Заключительные слова

Поздравляем, мы наконец-то дошли до конца.

Понимание того, как работает внутренний контекст выполнения, может оказаться полезным, особенно для разработчиков ASP.NET Core.

Но даже для несерверных приложений, таких как приложения пользовательского интерфейса, знание этого поможет вам создавать более качественные приложения.

Ссылки:



https://github.com/dotnet/aspnetcore/issues/34636