День 2833. #ЗаметкиНаПолях #Async
Ещё Раз Про Async/Await в C#. Часть 7
Сразу прошу прощения за оченьмногокода. Но раз уж взялся за эту серию, выложу её целиком (хоть и по кусочкам).
Пишем свой токен отмены с нуля. Начало
В предыдущей части мы создали класс
Самым очевидным API был бы тот, которого на самом деле не существует:
Поэтому отмена в .NET — «скоординированная» (cooperative): операции лишь сообщают, что пора завершиться, а она сама выбирает подходящий для этого момент. Механизм отмены передаёт флаг и, при необходимости, уведомляет заинтересованные стороны о его изменении.
Почему два типа?
-
-
Метод, принимающий
Токен – структура со ссылкой на источник:
Следствия:
1.
2. Свойство
Состояние источника (source) определяется одним полем:
volatile здесь не для красоты. Допустим, у вас реализована отмена через пулинг в цикле:
Внутри этого цикла ничего не меняет флаг, и ничто, что видит JIT-компилятор, его не меняет. Без volatile JIT читает его один раз, кэширует в регистре и переписывает цикл в while (true). Это допустимая оптимизация однопоточной программы, и это привело бы к тому, что отмена никогда бы не срабатывала. volatile заставляет читать значение на каждой итерации.
Флаг является односторонним. Отмена источника не может откатиться назад в неотменённый статус, т.е. операция, которая обнаружила отмену, никогда не должна гадать, изменится ли статус отмены позже.
ThrowIfCancellationRequested
Исключение содержит в себе токен. Это важно в месте перехвата (блоке
Продолжение следует…
Источник: https://www.meziantou.net/writing-your-own-cancellationtoken-from-scratch.htm
Ещё Раз Про Async/Await в C#. Часть 7
Сразу прошу прощения за оченьмногокода. Но раз уж взялся за эту серию, выложу её целиком (хоть и по кусочкам).
Пишем свой токен отмены с нуля. Начало
В предыдущей части мы создали класс
MyTask, который может запускать выполнение задачи и сообщать о её завершении. Однако он не умеет отменять выполнение. Этот не недоработка. Отмена в .NET – отдельная задача, реализованная с помощью двух типов. В этой части мы создадим MyCancellationTokenSource и MyCancellationToken, а затем интегрируем в MyTask. Реализация поможет ответить на распространённые вопросы о том, как устроен токен отмены.Самым очевидным API был бы тот, которого на самом деле не существует:
task.Kill(). В .NET когда-то было нечто подобное — Thread.Abort(). Он прерывал выполнение в произвольный момент. В результате система оказывалась в состоянии, к которому код не был готов, и понять, какие операции успели выполниться, а какие — нет, становилось невозможно. В .NET Core Thread.Abort нет, и не существует способа принудительно остановить код, который «не хочет» останавливаться.Поэтому отмена в .NET — «скоординированная» (cooperative): операции лишь сообщают, что пора завершиться, а она сама выбирает подходящий для этого момент. Механизм отмены передаёт флаг и, при необходимости, уведомляет заинтересованные стороны о его изменении.
Почему два типа?
-
MyCancellationTokenSource инициирует отмену (метод Cancel());-
MyCancellationToken лишь наблюдает за состоянием — он не может ничего отменить.Метод, принимающий
CancellationToken, получает право *узнать* о запросе на отмену, но не право *принять решение* об отмене. Иначе любой метод, которому вы передали токен, мог бы отменить всю операцию целиком — даже те её части, о которых он ничего не знает.Токен – структура со ссылкой на источник:
// MyCancellationToken.cs
public readonly struct MyCancellationToken(
MyCancellationTokenSource? source)
{
public static MyCancellationToken None
=> default;
public bool IsCancellationRequested
=> source is { IsCancellationRequested: true };
public bool CanBeCanceled
=> source is not null;
}
Следствия:
1.
default — допустимый токен, который никогда не переходит в состояние «отменён». Поэтому запись CancellationToken token = default - способ задать необязательный параметр.2. Свойство
CanBeCanceled стоит проверять. Для default оно возвращает false, что позволяет в «горячих» путях кода избежать создания механизмов регистрации отмены, которые всё равно никогда не сработают.Состояние источника (source) определяется одним полем:
// MyCancellationTokenSource.cs
public sealed class MyCancellationTokenSource
: IDisposable
{
private bool _disposed;
private volatile bool _cancellationRequested;
public bool IsCancellationRequested
=> _cancellationRequested;
public MyCancellationToken Token
{
get
{
ObjectDisposedException.ThrowIf(_disposed, this);
return new MyCancellationToken(this);
}
}
}
volatile здесь не для красоты. Допустим, у вас реализована отмена через пулинг в цикле:
while (!token.IsCancellationRequested)
DoOneChunkOfWork();
Внутри этого цикла ничего не меняет флаг, и ничто, что видит JIT-компилятор, его не меняет. Без volatile JIT читает его один раз, кэширует в регистре и переписывает цикл в while (true). Это допустимая оптимизация однопоточной программы, и это привело бы к тому, что отмена никогда бы не срабатывала. volatile заставляет читать значение на каждой итерации.
Флаг является односторонним. Отмена источника не может откатиться назад в неотменённый статус, т.е. операция, которая обнаружила отмену, никогда не должна гадать, изменится ли статус отмены позже.
ThrowIfCancellationRequested
//MyCancellationToken.cs
public void ThrowIfCancellationRequested()
{
if (IsCancellationRequested)
throw new MyOperationCanceledException(this);
}
// MyOperationCanceledException.cs
public sealed class MyOperationCanceledException(
MyCancellationToken token)
: OperationCanceledException("The operation was canceled.")
{
public MyCancellationToken Token { get; } = token;
}
Исключение содержит в себе токен. Это важно в месте перехвата (блоке
catch): в противном случае ситуации «была отменена именно та операция, которую я просил» и «была отменена какая-то другая операция, а её исключение просто перехвачено тут» выглядели бы одинаково. Свойство CancellationToken в OperationCanceledException нужно для того, чтобы их различать — благодаря ему задача может решить, перейти ли ей в состояние «отменена» (Canceled) или в состояние «завершена с ошибкой» (Faulted).Продолжение следует…
Источник: https://www.meziantou.net/writing-your-own-cancellationtoken-from-scratch.htm
👍13
День 2834. #ЗаметкиНаПолях #Async
Ещё Раз Про Async/Await в C#. Часть 7
Пишем свой токен отмены с нуля. Продолжение
Начало
Обратные вызовы и проблема «гонки»
Пулинг (периодический опрос) работает, только когда операция выполняет какой-либо код. Если же это, например, чтение из сокета приостановлено в ожидании данных, пулинг не сработает — а ведь такие операции чаще всего и требуется отменять. В подобных случаях отмена должна быть реализована через механизм событий:
Конец метода – то же состояние гонки, что было в MyTask.
Регистрация реализована в виде структуры со ссылкой на источник:
Предупреждение:
В методе Cancel выполняются зарегистрированные обратные вызовы и собираются исключения:
Важные моменты:
1. Отмена уже отменённого источника ничего не делает. Она идемпотентна, а не является ошибкой.
2. Обратные вызовы выполняются вне блокировки. Обратный вызов, который принимает другую блокировку или регистрирует другой обратный вызов, не должен приводить к взаимоблокировке с собой.
3. Сбой в одном обратном вызове не останавливает остальные. Это несвязанные части процесса очистки; исключения собираются и выбрасываются вместе.
Регистрацию можно отменить, потому что обратный вызов, который существует дольше операции, которая его требовала, является утечкой:
Окончание следует…
Источник: https://www.meziantou.net/writing-your-own-cancellationtoken-from-scratch.htm
Ещё Раз Про Async/Await в C#. Часть 7
Пишем свой токен отмены с нуля. Продолжение
Начало
Обратные вызовы и проблема «гонки»
Пулинг (периодический опрос) работает, только когда операция выполняет какой-либо код. Если же это, например, чтение из сокета приостановлено в ожидании данных, пулинг не сработает — а ведь такие операции чаще всего и требуется отменять. В подобных случаях отмена должна быть реализована через механизм событий:
// MyCancellationTokenSource.cs
private readonly Lock _lock = new();
private List<Registration>? _registrations = [];
internal MyCancellationRegistration
Register(Action callback)
{
lock (_lock)
{
if (!_cancellationRequested)
{
var registration = new Registration(callback);
_registrations!.Add(registration);
return new MyCancellationRegistration(this, registration);
}
}
// Register на отменённом источнике
// выполняет коллбек сразу вне блокировки
callback();
return default;
}
internal sealed class Registration(Action callback)
{
public Action Callback { get; } = callback;
}
Конец метода – то же состояние гонки, что было в MyTask.
Cancel() и Register() могут быть вызваны одновременно в разных потоках, при этом обратный вызов должен сработать ровно один раз: либо потому, что Cancel обнаружил его в списке, либо потому, что Register увидел, что флаг отмены уже установлен. Решение принимается внутри защищённой блокировкой области, а само действие выполняется уже вне её.Регистрация реализована в виде структуры со ссылкой на источник:
// MyCancellationRegistration.cs
public readonly struct MyCancellationRegistration : IDisposable
{
private MyCancellationTokenSource? _source;
private MyCancellationTokenSource.Registration? _registration;
internal MyCancellationRegistration(
MyCancellationTokenSource source,
MyCancellationTokenSource.Registration registration)
{
_source = source;
_registration = registration;
}
public void Dispose()
{
if (_source is not null
&& _registration is not null)
_source.Unregister(_registration);
}
}
Предупреждение:
MyCancellationRegistration.Dispose() завершает выполнение, в то время как обратный вызов всё ещё может выполняться в другом потоке. Это упрощение в нашей реализации. Стандартный метод CancellationTokenRegistration.Dispose() блокирует поток до тех пор, пока обратный вызов не завершится.В методе Cancel выполняются зарегистрированные обратные вызовы и собираются исключения:
// MyCancellationTokenSource.cs
public void Cancel()
{
List<Registration>? registrations;
lock (_lock)
{
if (_cancellationRequested)
return;
_cancellationRequested = true;
registrations = _registrations;
_registrations = null;
}
if (registrations is null)
return;
List<Exception>? exceptions = null;
foreach (var reg in registrations)
{
try
{
reg.Callback();
}
catch (Exception ex)
{
exceptions ??= [];
exceptions.Add(ex);
}
}
if (exceptions is not null)
throw new AggregateException(exceptions);
}
Важные моменты:
1. Отмена уже отменённого источника ничего не делает. Она идемпотентна, а не является ошибкой.
2. Обратные вызовы выполняются вне блокировки. Обратный вызов, который принимает другую блокировку или регистрирует другой обратный вызов, не должен приводить к взаимоблокировке с собой.
3. Сбой в одном обратном вызове не останавливает остальные. Это несвязанные части процесса очистки; исключения собираются и выбрасываются вместе.
Регистрацию можно отменить, потому что обратный вызов, который существует дольше операции, которая его требовала, является утечкой:
// MyCancellationTokenSource.cs
private bool _disposed;
internal void Unregister(Registration registration)
{
lock (_lock)
{
_registrations?.Remove(registration);
}
}
public void Dispose()
{
lock (_lock)
{
if (_disposed)
return;
_disposed = true;
_registrations = null;
}
}
Окончание следует…
Источник: https://www.meziantou.net/writing-your-own-cancellationtoken-from-scratch.htm
👍4
День 2836. #ЧтоНовенького #NuGet
Настройка Минимального Возраста NuGet-пакета
В .NET11 Release Candidate 2 в NuGet добавлена новая опция командной строки, позволяющая разработчикам задавать минимальный «возраст» (часов с момента публикации) для версий пакетов.
Это поможет избежать «слишком ранних» обновлений, чтобы избежать случаев, когда в опубликованном пакете обнаружится какая-нибудь ошибка (обычно сообщество находит их довольно быстро, за 1-2 дня).
Опция
Команды добавления и обновления пакетов учитывают это ограничение при выборе версий. Если более новая версия исключается из-за того, что она «слишком свежая», система сообщит об этом при выполнении обновления.
Эта настройка также влияет на проекты, в которых используются «плавающие» версии пакетов. Если после применения ограничения не остается ни одной подходящей версии, процесс восстановления (restore) завершается с ошибкой
В Visual Studio 2026 параметр можно настроить в меню Tools > Options… > NuGet Package Manager > Sources (Инструменты > Параметры… > Диспетчер пакетов NuGet > Источники). Там нажать Edit (Изменить) у выбранного источника и задать значение поля Minimum publish age (hours) (Минимальный возраст публикации (часы)).
Остаётся под вопросом, что делать со срочными критическими обновлениями. Пока система не предполагает наличия исключений из правила возраста.
Источник: https://visualstudiomagazine.com/articles/2026/10/14/net-11-rc2-adds-post-quantum-crypto-maui-csharp-ui-and-nuget-controls.aspx
Настройка Минимального Возраста NuGet-пакета
В .NET11 Release Candidate 2 в NuGet добавлена новая опция командной строки, позволяющая разработчикам задавать минимальный «возраст» (часов с момента публикации) для версий пакетов.
Это поможет избежать «слишком ранних» обновлений, чтобы избежать случаев, когда в опубликованном пакете обнаружится какая-нибудь ошибка (обычно сообщество находит их довольно быстро, за 1-2 дня).
Опция
--min-publish-age-hours доступна при использовании команд dotnet nuget add source и dotnet nuget update source. Она позволяет настроить параметр minPublishAgeHours в файле nuget.config без необходимости редактировать его вручную.Команды добавления и обновления пакетов учитывают это ограничение при выборе версий. Если более новая версия исключается из-за того, что она «слишком свежая», система сообщит об этом при выполнении обновления.
Эта настройка также влияет на проекты, в которых используются «плавающие» версии пакетов. Если после применения ограничения не остается ни одной подходящей версии, процесс восстановления (restore) завершается с ошибкой
NU1020 — вместо того чтобы просто продолжиться без установки нужного пакета.В Visual Studio 2026 параметр можно настроить в меню Tools > Options… > NuGet Package Manager > Sources (Инструменты > Параметры… > Диспетчер пакетов NuGet > Источники). Там нажать Edit (Изменить) у выбранного источника и задать значение поля Minimum publish age (hours) (Минимальный возраст публикации (часы)).
Остаётся под вопросом, что делать со срочными критическими обновлениями. Пока система не предполагает наличия исключений из правила возраста.
Источник: https://visualstudiomagazine.com/articles/2026/10/14/net-11-rc2-adds-post-quantum-crypto-maui-csharp-ui-and-nuget-controls.aspx
👍5
День 2837. #ЗаметкиНаПолях
5 Малоиспользуемых Функций .NET. Начало
В .NET есть компоненты, которыми большинство разработчиков пользуются постоянно, даже не задумываясь о том, как они устроены внутри. Однако, когда вы сталкиваетесь с кодом, в котором одна и та же операция выполняется миллионы раз, детали внезапно обретают огромное значение. Именно здесь на сцену выходят менее известные возможности современного .NET. Можно создать вполне работоспособное приложение, ни разу ими не воспользовавшись, но, если разобраться, для чего они предназначены, они становятся ценными инструментами, которые стоит держать под рукой на случай возникновения соответствующих задач.
1. FrozenDictionary для неизменяемых справочных данных
Обычный словарь спроектирован как изменяемая структура. В него можно добавлять элементы, удалять, обновлять значения и выполнять поиск. Это подходит, если содержимое коллекции меняется в процессе работы приложения. Но что, если оно не меняется? Представьте приложение, которое при запуске формирует справочник, а затем на протяжении всего времени своей работы только считывает из него данные:
В этом нет ничего плохого. Но вы используете структуру данных, которая изначально спроектирована с расчётом на возможность внесения изменений, хотя логике приложения этого не надо. Для такого сценария существует
Важно понимать компромисс. Создание «замороженной» коллекции не бесплатно: среда выполнения тратит дополнительные ресурсы на анализ и построение оптимизированного представления данных.
Благодаря этому он отлично подходит для таких задач, как статические конфигурации, карты ключевых слов, сопоставления MIME-типов, метаданные маршрутизации и любые другие данные, которые инициализируются единожды, а затем часто используются для поиска.
Кроме того, это даёт полезную семантическую гарантию: после создания коллекцию невозможно изменить. Это не то же самое, что просто договориться не менять обычный словарь.
Если ваш словарь меняется в процессе работы приложения,
Суть здесь не в том, что
И этот паттерн стоит взять на вооружение — он применим не только в случае словаря.
2. SearchValues<T> для многократного поиска
Поиск в тексте — операция, затратность которой часто недооценивают.
Простой вызов, например:
— это тривиально.
Однако парсерам часто приходится многократно искать один из нескольких символов. Например, вы можете искать разделители:
Это означает, что в одних и тех же входных данных приходится многократно искать различные значения. Класс
Интересная особенность в том, что набор искомых значений можно подготовить один раз, а затем использовать многократно. Это особенно полезно при разработке парсеров, токенизаторов, систем обработки протоколов и других компонентов, где приходится постоянно искать одни и те же разделители.
Это также наглядный пример того, почему при оптимизации важно измерять результат. Если вам нужно выполнить поиск в строке длиной 20 символов 1 раз,
PS: в NET9 SearchValues расширен, и теперь поддерживает поиск строк, а не только символов.
Окончание следует…
Источник: https://medium.com/codetodeploy/i-bet-youre-not-using-these-5-net-apis-937360c1b5e7
5 Малоиспользуемых Функций .NET. Начало
В .NET есть компоненты, которыми большинство разработчиков пользуются постоянно, даже не задумываясь о том, как они устроены внутри. Однако, когда вы сталкиваетесь с кодом, в котором одна и та же операция выполняется миллионы раз, детали внезапно обретают огромное значение. Именно здесь на сцену выходят менее известные возможности современного .NET. Можно создать вполне работоспособное приложение, ни разу ими не воспользовавшись, но, если разобраться, для чего они предназначены, они становятся ценными инструментами, которые стоит держать под рукой на случай возникновения соответствующих задач.
1. FrozenDictionary для неизменяемых справочных данных
Обычный словарь спроектирован как изменяемая структура. В него можно добавлять элементы, удалять, обновлять значения и выполнять поиск. Это подходит, если содержимое коллекции меняется в процессе работы приложения. Но что, если оно не меняется? Представьте приложение, которое при запуске формирует справочник, а затем на протяжении всего времени своей работы только считывает из него данные:
var statusCodes = new Dictionary<int, string>
{
[200] = "OK",
[201] = "Created",
[400] = "Bad Request",
[401] = "Unauthorized",
[404] = "Not Found",
[500] = "Internal Server Error"
};
В этом нет ничего плохого. Но вы используете структуру данных, которая изначально спроектирована с расчётом на возможность внесения изменений, хотя логике приложения этого не надо. Для такого сценария существует
FrozenDictionary<TKey, TValue>:var statusCodes = new Dictionary<int, string>
{
[200] = "OK",
[201] = "Created",
// …
}.ToFrozenDictionary();
Важно понимать компромисс. Создание «замороженной» коллекции не бесплатно: среда выполнения тратит дополнительные ресурсы на анализ и построение оптимизированного представления данных.
FrozenDictionary спроектирован с расчётом на то, что вы один раз на это потратитесь, а затем будете многократно выполнять операции поиска.Благодаря этому он отлично подходит для таких задач, как статические конфигурации, карты ключевых слов, сопоставления MIME-типов, метаданные маршрутизации и любые другие данные, которые инициализируются единожды, а затем часто используются для поиска.
Кроме того, это даёт полезную семантическую гарантию: после создания коллекцию невозможно изменить. Это не то же самое, что просто договориться не менять обычный словарь.
Если ваш словарь меняется в процессе работы приложения,
FrozenDictionary — неподходящий выбор. Затраты на постоянное пересоздание «замороженной» коллекции сведут на нет весь смысл её использования.Суть здесь не в том, что
FrozenDictionary «быстрее», чем обычный словарь. Дело в том, что среда выполнения может применять иные подходы к оптимизации, когда точно знает, что коллекция никогда не изменится.И этот паттерн стоит взять на вооружение — он применим не только в случае словаря.
2. SearchValues<T> для многократного поиска
Поиск в тексте — операция, затратность которой часто недооценивают.
Простой вызов, например:
if (text.Contains(':'))
{
// …
}— это тривиально.
Однако парсерам часто приходится многократно искать один из нескольких символов. Например, вы можете искать разделители:
, ; | :. Наивная реализация может заключаться в нескольких вызовах IndexOf() для каждого символа:int comma = text.IndexOf(',');
int semicolon = text.IndexOf(';');
int pipe = text.IndexOf('|');
int colon = text.IndexOf(':');Это означает, что в одних и тех же входных данных приходится многократно искать различные значения. Класс
SearchValues<T> предназначен для ситуаций, когда требуется неоднократно выполнять поиск одного значения из заранее известного набора:private static readonly SearchValues<char> Separators =
SearchValues.Create(",;|:");
int index = text.AsSpan().IndexOfAny(Separators);
Интересная особенность в том, что набор искомых значений можно подготовить один раз, а затем использовать многократно. Это особенно полезно при разработке парсеров, токенизаторов, систем обработки протоколов и других компонентов, где приходится постоянно искать одни и те же разделители.
Это также наглядный пример того, почему при оптимизации важно измерять результат. Если вам нужно выполнить поиск в строке длиной 20 символов 1 раз,
SearchValues<char> не поможет. Но, если вы обрабатываете гигабайты текста и постоянно ищете одни и те же разделители, ситуация кардинально меняется.PS: в NET9 SearchValues расширен, и теперь поддерживает поиск строк, а не только символов.
Окончание следует…
Источник: https://medium.com/codetodeploy/i-bet-youre-not-using-these-5-net-apis-937360c1b5e7
👍10
День 2838. #ЗаметкиНаПолях
5 Малоиспользуемых Функций .NET. Окончание
Начало
3. String.Create() для создания строк на горячих путях
Механизмы конкатенации строк за последние годы значительно усовершенствовались. Компилятор и среда выполнения теперь гораздо эффективнее оптимизируют типичные сценарии построения строк, поэтому не стоит заранее предполагать, что каждый оператор «+» приводит к созданию множества временных строковых объектов.
Тем не менее, существуют ситуации, когда приходится многократно создавать строки, заранее зная точный размер итогового результата, на помощь приходит String.Create(). Допустим, вы формируете идентификатор:
В обычном прикладном коде лучше оставить всё как есть. Это легко читается, и достаточно быстро работает. Но, если нужно генерировать миллионы идентификаторов фиксированного формата на горячем пути, может потребоваться больший контроль над выделением памяти для результирующей строки:
Это специализированный инструмент, и относиться к нему стоит именно так.
Код с его использованием получается значительно сложнее. Однако, если на горячем пути профилировщик показывает значительный объём выделяемой памяти, контроль над созданием итоговой строки может быть вполне уместен.
Но важно понимать, что в современном .NET многие типичные сценарии уже оптимизированы. Поэтому к
4. ReadOnlyMemory<T>: когда данные должны существовать дольше, чем Span<T>
Span<T> — один из самых полезных типов, появившихся в современном .NET, однако у него есть фундаментальное ограничение. Он предназначен для представления «представления» (view) участка памяти, не становясь при этом его владельцем. Он может указывать на массивы, память в стеке, неуправляемую память и другие источники, но время его жизни намеренно ограничено. Поэтому нельзя просто сохранить Span<T> в обычном поле класса:
Этот код не компилируется. Span<T> — это ref-структура (может размещаться только на стеке). На помощь придёт ReadOnlyMemory<T>:
Теперь область памяти можно сохранить как часть обычного объекта, а при необходимости — преобразовать в
Это важное различие.
Воспринимайте
Замечание:
Модель использования:
-
-
5. CollectionsMarshal.AsSpan(): когда для List<T> требуется обработка с использованием Span<T>
List<T> — одна из наиболее часто используемых коллекций в .NET. Под капотом у неё обычный массив. Иногда возникает ситуация, когда содержимое списка нужно передать в API, работающий со Span<T>. Очевидное решение — скопировать элементы в новый массив. Это сработает, но сведёт на нет одно из главных преимуществ использования Span<T>. Метод CollectionsMarshal.AsSpan() предоставляет прямой доступ к внутреннему хранилищу списка:
Новый массив при этом не создаётся. Span ссылается непосредственно на внутреннее хранилище списка, поэтому любые изменения, внесённые через span, отражаются в самом списке. Это мощный инструмент, но есть один нюанс.
Span не является независимым снимком списка. Если список изменится таким образом, что его внутренний буфер будет заменён, на ранее полученный span больше нельзя будет полагаться — это станет небезопасным. Следовательно, время жизни спана должно быть коротким, а во время его использования следует избегать модификаций структуры списка.
Ещё одна важная деталь: CollectionsMarshal.AsSpan() — это не призыв использовать List<T> в качестве универсального контейнера для работы со span. Если ваш алгоритм изначально предполагает работу с непрерывным блоком памяти, возможно, лучше сразу использовать массив или другую структуру, изначально предназначенную для работы со span.
CollectionsMarshal.AsSpan() наиболее полезен в тех случаях, когда у вас уже есть List<T>, и вам нужно эффективно передать его в API, работающие со span, не прибегая к копированию данных. Это довольно специфическая задача, но в подобных ситуациях данный API позволяет избежать лишнего выделения памяти и копирования.
Источник: https://medium.com/codetodeploy/i-bet-youre-not-using-these-5-net-apis-937360c1b5e7
5 Малоиспользуемых Функций .NET. Окончание
Начало
3. String.Create() для создания строк на горячих путях
Механизмы конкатенации строк за последние годы значительно усовершенствовались. Компилятор и среда выполнения теперь гораздо эффективнее оптимизируют типичные сценарии построения строк, поэтому не стоит заранее предполагать, что каждый оператор «+» приводит к созданию множества временных строковых объектов.
Тем не менее, существуют ситуации, когда приходится многократно создавать строки, заранее зная точный размер итогового результата, на помощь приходит String.Create(). Допустим, вы формируете идентификатор:
var id = $"USER-{userId:D8}";В обычном прикладном коде лучше оставить всё как есть. Это легко читается, и достаточно быстро работает. Но, если нужно генерировать миллионы идентификаторов фиксированного формата на горячем пути, может потребоваться больший контроль над выделением памяти для результирующей строки:
var userId = 123456;
var id = string.Create(
13,
userId,
static (span, value) =>
{
"USER-".AsSpan().CopyTo(span);
value.TryFormat(span[5..], out _, "D8");
});
// USER-00123456
String.Create() выделяет память для итоговой строки всего один раз и предоставляет доступ к её внутреннему хранилищу символов через Span, что позволяет заполнять строку напрямую. При этом не создаются никакие промежуточные строки для отдельных фрагментов данных.Это специализированный инструмент, и относиться к нему стоит именно так.
Код с его использованием получается значительно сложнее. Однако, если на горячем пути профилировщик показывает значительный объём выделяемой памяти, контроль над созданием итоговой строки может быть вполне уместен.
Но важно понимать, что в современном .NET многие типичные сценарии уже оптимизированы. Поэтому к
String.Create() стоит прибегать только тогда, когда вы убедились, что процесс создания строк и связанные с ним выделения памяти действительно оказывают существенное влияние на производительность.4. ReadOnlyMemory<T>: когда данные должны существовать дольше, чем Span<T>
Span<T> — один из самых полезных типов, появившихся в современном .NET, однако у него есть фундаментальное ограничение. Он предназначен для представления «представления» (view) участка памяти, не становясь при этом его владельцем. Он может указывать на массивы, память в стеке, неуправляемую память и другие источники, но время его жизни намеренно ограничено. Поэтому нельзя просто сохранить Span<T> в обычном поле класса:
public class BufferHolder
{
private Span<byte> _buffer;
}
Этот код не компилируется. Span<T> — это ref-структура (может размещаться только на стеке). На помощь придёт ReadOnlyMemory<T>:
public sealed class Message
{
private readonly ReadOnlyMemory<byte> _data;
public Message(ReadOnlyMemory<byte> data)
{
_data = data;
}
public ReadOnlySpan<byte> Data => _data.Span;
}
Теперь область памяти можно сохранить как часть обычного объекта, а при необходимости — преобразовать в
Span<T> для синхронной обработки.Это важное различие.
Воспринимайте
Span<T> как временное представление участка памяти. А Memory<T> или ReadOnlyMemory<T> — как способ хранения ссылки на этот участок памяти. Это особенно полезно при выполнении асинхронных операций. Span<T> нельзя безопасно передавать через границу await, тогда как Memory<T> специально разработана для сценариев, в которых доступ к памяти должен сохраняться даже после того, как текущий кадр стека перестанет существовать.Замечание:
ReadOnlyMemory<T> не делает лежащие в её основе данные неизменяемыми, а лишь предотвращает их изменение через конкретно это представление. Если исходный массив будет изменён в другом месте, данные, на которые ссылается Memory<T>, тоже могут измениться. Поэтому понимание того, кто владеет буфером и каков его жизненный цикл, столь же важно, как и понимание используемого вами типа данных.Модель использования:
-
Span<T>, когда нужно кратковременное высокопроизводительное представление данных;-
ReadOnlyMemory<T>, если это представление необходимо сохранить или если оно должно существовать дольше текущей синхронной области видимости.5. CollectionsMarshal.AsSpan(): когда для List<T> требуется обработка с использованием Span<T>
List<T> — одна из наиболее часто используемых коллекций в .NET. Под капотом у неё обычный массив. Иногда возникает ситуация, когда содержимое списка нужно передать в API, работающий со Span<T>. Очевидное решение — скопировать элементы в новый массив. Это сработает, но сведёт на нет одно из главных преимуществ использования Span<T>. Метод CollectionsMarshal.AsSpan() предоставляет прямой доступ к внутреннему хранилищу списка:
var values = new List<int>
{
10, 20, 30, 40, 50
};
Span<int> span =
CollectionsMarshal.AsSpan(values);
for (int i = 0; i < span.Length; i++)
{
span[i] *= 2;
}
Новый массив при этом не создаётся. Span ссылается непосредственно на внутреннее хранилище списка, поэтому любые изменения, внесённые через span, отражаются в самом списке. Это мощный инструмент, но есть один нюанс.
Span не является независимым снимком списка. Если список изменится таким образом, что его внутренний буфер будет заменён, на ранее полученный span больше нельзя будет полагаться — это станет небезопасным. Следовательно, время жизни спана должно быть коротким, а во время его использования следует избегать модификаций структуры списка.
Ещё одна важная деталь: CollectionsMarshal.AsSpan() — это не призыв использовать List<T> в качестве универсального контейнера для работы со span. Если ваш алгоритм изначально предполагает работу с непрерывным блоком памяти, возможно, лучше сразу использовать массив или другую структуру, изначально предназначенную для работы со span.
CollectionsMarshal.AsSpan() наиболее полезен в тех случаях, когда у вас уже есть List<T>, и вам нужно эффективно передать его в API, работающие со span, не прибегая к копированию данных. Это довольно специфическая задача, но в подобных ситуациях данный API позволяет избежать лишнего выделения памяти и копирования.
Источник: https://medium.com/codetodeploy/i-bet-youre-not-using-these-5-net-apis-937360c1b5e7
👍2
День 2839.
Давненько не рекомендовал познавательных видео.
Пару недель назад разгорелся скандал, связанный с TLS шифрованием, и ставшим уже знаменитым ключом «4».
Оказалось, что нужно серьёзно постараться, чтобы задать в качестве закрытого ключа число 4. Доктор Тим Мюллер объясняет, в чем заключается проблема «случайного» присвоения 120-значному числу значения «4». Что всё это значит? Как это работает? И на что влияет?
Об этом в очередном видео с канала Computerphile «The Private Key Mistake! (TLS Key set to "4")».
Видео на английском, но есть автоматические субтитры.
Приятного просмотра.
Давненько не рекомендовал познавательных видео.
Пару недель назад разгорелся скандал, связанный с TLS шифрованием, и ставшим уже знаменитым ключом «4».
Оказалось, что нужно серьёзно постараться, чтобы задать в качестве закрытого ключа число 4. Доктор Тим Мюллер объясняет, в чем заключается проблема «случайного» присвоения 120-значному числу значения «4». Что всё это значит? Как это работает? И на что влияет?
Об этом в очередном видео с канала Computerphile «The Private Key Mistake! (TLS Key set to "4")».
Видео на английском, но есть автоматические субтитры.
Приятного просмотра.
👍4
🔴 Открытое собеседование на Middle C# | 10 ноября, 19:00 мск
Senior C# разработчик проведёт техническое интервью в прямом эфире. Посмотришь, как всё устроено изнутри, какие задания предлагают кандидатам и насколько ты готов к такому формату.
Как всё будет проходить:
📂 Интервьюер: Александр Моргунов, Senior C# разработчик с опытом 7+ лет в европейских высоконагруженных сервисах. В роли кандидата - разработчик-доброволец.
📂 Никакого сценария: Александр задаст вопросы и предложит задачи, которые использует на технических интервью. Кандидат заранее их не знает.
📂 В конце Александр пообщается со зрителями и ответит на их вопросы
Участие бесплатное!
Эфир проходит в рамках менторской программы ШОРТКАТ для разработчиков, которые хотят сменить работу, вырасти в грейде и зарплате или прокачать технические навыки
Переходи в бота, чтобы получить ссылку на эфир → @shortcut_csharp_bot
Реклама.
О рекламодателе.
Senior C# разработчик проведёт техническое интервью в прямом эфире. Посмотришь, как всё устроено изнутри, какие задания предлагают кандидатам и насколько ты готов к такому формату.
Как всё будет проходить:
📂 Интервьюер: Александр Моргунов, Senior C# разработчик с опытом 7+ лет в европейских высоконагруженных сервисах. В роли кандидата - разработчик-доброволец.
📂 Никакого сценария: Александр задаст вопросы и предложит задачи, которые использует на технических интервью. Кандидат заранее их не знает.
📂 В конце Александр пообщается со зрителями и ответит на их вопросы
Участие бесплатное!
Эфир проходит в рамках менторской программы ШОРТКАТ для разработчиков, которые хотят сменить работу, вырасти в грейде и зарплате или прокачать технические навыки
Переходи в бота, чтобы получить ссылку на эфир → @shortcut_csharp_bot
Реклама.
О рекламодателе.
👍3
День 2841.
.NET Conf 2026 (10-13 ноября)
Ну вот опять это время года. Конференция .NET Conf 2026 и выход очередной версии .NET.
16я ежегодная конференция .NET Conf — виртуальное событие, объединяющее сообщество .NET для празднования выхода .NET 11 и обсуждения перспектив современной разработки. Присоединяйтесь, чтобы детально изучить инструменты, фреймворки и подходы, определяющие будущее экосистемы .NET. Вас ждут анонсы, мнения экспертов и практические рекомендации, которые помогут вам в создании новых проектов.
Что вас ждёт на конференции
В течение четырёх дней перед вами выступят команды разработчиков .NET, инженеры Microsoft, лидеры сообщества и программисты со всего мира. Узнайте первыми о нововведениях в .NET 11, познакомьтесь с последними достижениями в экосистеме .NET и освойте практические приёмы, которые можно сразу применить в собственных проектах. Программа .NET Conf включает в себя всё: от ключевых анонсов и глубоких технических разборов до реальных кейсов и сессий, подготовленных самим сообществом — здесь каждый разработчик .NET найдет что-то полезное для себя.
Старт конференции в 19:00 мск.
Зарегистрироваться можно здесь.
.NET Conf 2026 (10-13 ноября)
Ну вот опять это время года. Конференция .NET Conf 2026 и выход очередной версии .NET.
16я ежегодная конференция .NET Conf — виртуальное событие, объединяющее сообщество .NET для празднования выхода .NET 11 и обсуждения перспектив современной разработки. Присоединяйтесь, чтобы детально изучить инструменты, фреймворки и подходы, определяющие будущее экосистемы .NET. Вас ждут анонсы, мнения экспертов и практические рекомендации, которые помогут вам в создании новых проектов.
Что вас ждёт на конференции
В течение четырёх дней перед вами выступят команды разработчиков .NET, инженеры Microsoft, лидеры сообщества и программисты со всего мира. Узнайте первыми о нововведениях в .NET 11, познакомьтесь с последними достижениями в экосистеме .NET и освойте практические приёмы, которые можно сразу применить в собственных проектах. Программа .NET Conf включает в себя всё: от ключевых анонсов и глубоких технических разборов до реальных кейсов и сессий, подготовленных самим сообществом — здесь каждый разработчик .NET найдет что-то полезное для себя.
Старт конференции в 19:00 мск.
Зарегистрироваться можно здесь.
.NET Conf
.NET Conf 2026
Join the .NET Conf 2026 free virtual event November 10 - 13 2026 to learn about the newest developments across the .NET platform, open source, and dev tools. Mark your calendar!
День 2842. #ЗаметкиНаПолях #Async
Ещё Раз Про Async/Await в C#. Часть 9
Контекст выполнения (ExecutionContext)
Поток, который возобновляет выполнение после await, как правило, не является тем же потоком, в котором метод был запущен изначально. Поэтому возникает проблема: всё, что было привязано к исходному потоку, теряется.
Контекст выполнения — это механизм, который решает данную проблему. Он «перемещается» вместе с выполнением кода при прохождении через оператор
Что это
Контекст выполнения — это небольшой неизменяемый объект, который сопровождает логический ход выполнения программы, а не конкретный поток ОС. В момент приостановки асинхронного метода контекст сохраняется (захватывается), а при возобновлении выполнения — восстанавливается для последующего кода.
Что он переносит
- Значения
- Текущую культуру (
- Контекст безопасности, в частности — идентификатор пользователя, от имени которого выполняется код (Windows impersonation identity).
Он не переносит контекст синхронизации (SynchronizationContext). Это отдельный механизм, который захватывается самим оператором
Как это работает
Значения в
Значение также передаётся в задачи, которые вы запускаете. Методы
Изменения, внесённые в вызываемом коде (внутри асинхронной задачи), не передаются обратно в вызывающий код. Это часто становится неожиданностью: если асинхронный метод меняет значение, то внутри этого метода оно будет изменённым, однако в вызывающем коде после завершения операции по-прежнему будет наблюдаться исходное значение.
Причина в том, что контекст работает по принципу «копирование при записи» (copy-on-write). Запись в
Передачу контекста можно отключить. Метод
Предупреждение: Метод
AsyncLocal против ThreadStatic
Они похожи внешне, но решают противоположные задачи:
[ThreadStatic]
- Привязан к потоку,
- Не переходит через await,
- Не переходит в другой поток,
- Общий для всего кода в данном потоке (разные продолжения могут изменять общее состояние).
AsyncLocal<T>
- Привязан к логическому ходу выполнения,
- Переходит через await,
- Переходит в другой поток.
В асинхронном коде использование
Цена вопроса
Использование контекстного состояния не проходит бесследно, однако характер этих издержек не всегда очевиден:
Временные затраты в основном уходят на планирование пула потоков и ожидание. А по потреблению памяти можно сделать три вывода:
1. Если
2. Для асинхронных методов, приостанавливающих своё выполнение, ничего не меняется: объём выделяемой памяти составляет 96 байт независимо от того, сколько значений
3. Затраты на запись значений AsyncLocal растут по мере увеличения контекста. Это происходит потому, что механизм «копирования при записи» (copy-on-write) вынужден копировать существующую структуру данных. Причём они возникают именно при записи, а не при чтении. Чтение из
Где это проявляется
Этот механизм чаще всего остаётся незаметным. Вот три исключения:
1. Области логирования. Метод
2. Культура в тестах. Установка
3. Утечки памяти. Контекст выполнения захватывается ожидающими продолжениями, таймерами и зарегистрированными обратными вызовами. Если в
Источник: https://www.meziantou.net/executioncontext-what-flows-across-an-await.htm
Ещё Раз Про Async/Await в C#. Часть 9
Контекст выполнения (ExecutionContext)
Поток, который возобновляет выполнение после await, как правило, не является тем же потоком, в котором метод был запущен изначально. Поэтому возникает проблема: всё, что было привязано к исходному потоку, теряется.
[ThreadStatic]
private static string? CurrentUser;
CurrentUser = "alice";
await SomethingAsync();
// CurrentUser может быть null здесь
Контекст выполнения — это механизм, который решает данную проблему. Он «перемещается» вместе с выполнением кода при прохождении через оператор
await. Понимание того, что именно он переносит (а что намеренно нет), позволяет прояснить целый ряд ситуаций, которые зачастую сбивают с толку.Что это
Контекст выполнения — это небольшой неизменяемый объект, который сопровождает логический ход выполнения программы, а не конкретный поток ОС. В момент приостановки асинхронного метода контекст сохраняется (захватывается), а при возобновлении выполнения — восстанавливается для последующего кода.
Что он переносит
- Значения
AsyncLocal<T>. Это основной элемент — единственная часть, с которой вы взаимодействуете напрямую.- Текущую культуру (
CultureInfo.CurrentCulture и CurrentUICulture).- Контекст безопасности, в частности — идентификатор пользователя, от имени которого выполняется код (Windows impersonation identity).
Он не переносит контекст синхронизации (SynchronizationContext). Это отдельный механизм, который захватывается самим оператором
await. Контекст синхронизации определяет не то, какое состояние «видит» код, а то, в каком контексте (потоке) будет выполняться продолжение. О контексте синхронизации в следующей части.Как это работает
Значения в
AsyncLocal передаются через await. Поток может быть другим, но значение остаётся прежним. Благодаря этому работают такие механизмы, как идентификаторы запросов, идентификаторы трассировки и контекстное логирование.Значение также передаётся в задачи, которые вы запускаете. Методы
Task.Run, ThreadPool.QueueUserWorkItem, обратные вызовы таймеров и Task.ContinueWith — все они захватывают контекст в момент постановки задачи в очередь и восстанавливают его в момент выполнения.Изменения, внесённые в вызываемом коде (внутри асинхронной задачи), не передаются обратно в вызывающий код. Это часто становится неожиданностью: если асинхронный метод меняет значение, то внутри этого метода оно будет изменённым, однако в вызывающем коде после завершения операции по-прежнему будет наблюдаться исходное значение.
Причина в том, что контекст работает по принципу «копирование при записи» (copy-on-write). Запись в
AsyncLocal не изменяет некий общий объект, а создаёт новый контекст выполнения, который становится текущим для данного потока выполнения. Вызывающий код при этом продолжает ссылаться на старый контекст. Таким образом, значения передаются «вниз» — в вызываемый код, но никогда не возвращаются «наверх». Это не ограничение, а особенность архитектуры. Если бы изменения передавались обратно, то две параллельные операции, имеющие общего «родителя», могли бы повредить состояние друг друга. Если вам нужно получить значение в вызывающем коде, просто верните его явным образом.Передачу контекста можно отключить. Метод
ExecutionContext.SuppressFlow() предотвращает захват контекста для любых операций, запущенных внутри соответствующего блока. Это бывает полезно, например, для фоновых задач, которые должны быть полностью изолированы от текущего контекста.Предупреждение: Метод
SuppressFlow возвращает объект AsyncFlowControl, который необходимо освободить (вызвать Dispose) в том же потоке, где он был создан. Если внутри блока using использовать оператор await, выполнение может возобновиться в другом потоке, из-за чего вызов Dispose приведет к исключению InvalidOperationException. Поэтому запускайте задачу внутри блока using, а ожидайте её завершения уже за его пределами.AsyncLocal против ThreadStatic
Они похожи внешне, но решают противоположные задачи:
[ThreadStatic]
- Привязан к потоку,
- Не переходит через await,
- Не переходит в другой поток,
- Общий для всего кода в данном потоке (разные продолжения могут изменять общее состояние).
AsyncLocal<T>
- Привязан к логическому ходу выполнения,
- Переходит через await,
- Переходит в другой поток.
В асинхронном коде использование
[ThreadStatic] — это почти всегда ошибка, которая проявит себя в момент смены потока.Цена вопроса
Использование контекстного состояния не проходит бесследно, однако характер этих издержек не всегда очевиден:
Метод Переменных Время Память
Приостановка Async-метода 0 1,367.422нс 96 байт
QueueUserWorkItem 0 1,598.319нс 112 байт
UnsafeQueueUserWorkItem 0 1,626.048нс 112 байт
Запись AsyncLocal 0 3.723нс -
Приостановка Async-метода 1 1,330.274нс 96 байт
QueueUserWorkItem 1 1,678.497нс 120 байт
UnsafeQueueUserWorkItem 1 1,635.826нс 112 байт
Запись AsyncLocal 1 3.955нс -
Приостановка Async-метода 5 1,344.521нс 96 байт
QueueUserWorkItem 5 1,637.569нс 120 байт
UnsafeQueueUserWorkItem 5 1,663.684нс 112 байт
Запись AsyncLocal 5 6.188нс -
Временные затраты в основном уходят на планирование пула потоков и ожидание. А по потреблению памяти можно сделать три вывода:
1. Если
AsyncLocal не используется, контекст практически ничего не стоит. В .NET существует пустой контекст синхронизации, и его «захват» сводится лишь к копированию ссылки. Как только появляется хотя бы одно значение AsyncLocal, операция захвата начинает требовать 8 байт — это место в памяти для хранения захваченного контекста. В случае с UnsafeQueueUserWorkItem показатель остаётся на уровне 112 байт, поскольку эта операция не выполняет захват.2. Для асинхронных методов, приостанавливающих своё выполнение, ничего не меняется: объём выделяемой памяти составляет 96 байт независимо от того, сколько значений
AsyncLocal задано. В объекте конечного автомата уже предусмотрено поле для контекста, поэтому использование реального контекста вместо пустого не требует дополнительных затрат.3. Затраты на запись значений AsyncLocal растут по мере увеличения контекста. Это происходит потому, что механизм «копирования при записи» (copy-on-write) вынужден копировать существующую структуру данных. Причём они возникают именно при записи, а не при чтении. Чтение из
AsyncLocal остается «дешёвым», а вот массовая установка значений в часто выполняемом цикле может стать заметной.Где это проявляется
Этот механизм чаще всего остаётся незаметным. Вот три исключения:
1. Области логирования. Метод
ILogger.BeginScope и все библиотеки для распределённой трассировки построены на базе AsyncLocal. Благодаря этому идентификатор трассировки (trace ID), установленный в начале обработки запроса, сохраняется даже спустя несколько операций await — вплоть до кода, находящегося в другой сборке.2. Культура в тестах. Установка
CultureInfo.CurrentCulture автоматически переносится через операции await (что удобно), а также попадает в любые задачи, поставленные в очередь (что иногда бывает нежелательно). Если в тесте изменяется культура, но не восстанавливается обратно, это может повлиять на задачи, поставленные в очередь в ходе выполнения этого теста.3. Утечки памяти. Контекст выполнения захватывается ожидающими продолжениями, таймерами и зарегистрированными обратными вызовами. Если в
AsyncLocal хранится ссылка на большой граф объектов, то все элементы, ожидающие завершения этого потока операций, будут удерживать эти объекты в памяти, не давая сборщику мусора их удалить. Это — частая причина, по которой приходится проводить расследования на тему «почему этот объект всё ещё удерживается в памяти».Источник: https://www.meziantou.net/executioncontext-what-flows-across-an-await.htm
👍2