Какой пост выпустить следующим?
Anonymous Poll
40%
ref readonly или in
7%
TechEmpower benchmark — можно ли верить
36%
.NET 8 breaking changes
18%
Какую-нибудь хрень про асинхронность
Ночные заметки: GUI на .NET неуспешен по той причине, что десктоп мало кому нужен в 2023, а мобилки и веб поделены между кабанистыми фреймворками.
P.S. Avalonia — гениальный фреймворк, со своей нишей
23/300
P.S. Avalonia — гениальный фреймворк, со своей нишей
23/300
.NET epeshk blog
Ночные заметки: GUI на .NET неуспешен по той причине, что десктоп мало кому нужен в 2023, а мобилки и веб поделены между кабанистыми фреймворками. P.S. Avalonia — гениальный фреймворк, со своей нишей 23/300
P.P.S. Желающие написать свой обзор GUI в .NET — welcome! Также ищу, кто сможет аргументировать, что semantic kernel и прочий ML.NET — бизнесс буллшит, или обратное
Облить редакцию за неправильное мнение про гуй .NETа и полное непонимание concurrency всегда можно в чате
https://t.me/+XF6RntNpSyJiMTdi
https://t.me/+XF6RntNpSyJiMTdi
Telegram
dotChat
You’ve been invited to join this group on Telegram.
ref readonly и in параметры
В C# 12 появилась возможность сделать параметр метода
В чём же разница?
Единственная разница, которую мне удалось обнаружить — при использовании
Возможо, я слишком тупой и не проникся философией иммутабельности. На мой взгляд,
Рекомендации
В своём коде я буду делать так:
- Использовать
- Делать структуры
- Использовать
- Если придётся где-то использовать
А какие ещё отличия есть у этих параметров?
@epeshkblog | Поддержать канал
█░░░░░░░░░░░░ 23 / 300
В C# 12 появилась возможность сделать параметр метода
ref readonly
— ref значит, что параметр передаётся по ссылке, а readonly — что метод не может менять значение. Казалось бы, тот же самый смысл у in параметров из C# 7.2В чём же разница?
Единственная разница, которую мне удалось обнаружить — при использовании
ref readonly
параметра генерируется warning, если в метод передаётся не ссылка на переменную, а просто значение.void UseIn(in int x) { }
void UseRefReadonly(ref readonly int x) { }
UseIn(2); // OK
UseRefReadonly(2); // Warning CS9193 : Argument 1 should be a variable because it is passed to a 'ref readonly' parameter
UseRefReadonly(in 2); // OK
Возможо, я слишком тупой и не проникся философией иммутабельности. На мой взгляд,
ref readonly
бесполезен для обычных людей и, скорее, появился из-за недоработок в реализации in
параметров.Рекомендации
В своём коде я буду делать так:
- Использовать
ref
, если структура — изменяемая, и надо передать её по ссылке- Делать структуры
readonly
, если они логически являются цельными, неизменяемыми значениями- Использовать
in
для больших readonly struct
- Если придётся где-то использовать
ref readonly
параметр — расскажу об этом отдельноА какие ещё отличия есть у этих параметров?
@epeshkblog | Поддержать канал
█░░░░░░░░░░░░ 23 / 300
Telegram
.NET epeshk blog
Канал с заметками о C# и .NET
Поддержать канал: https://t.me/blog_donate/2
Обратная связь: https://forms.gle/3uRz7FmzUA26Kw4y5
Поддержать канал: https://t.me/blog_donate/2
Обратная связь: https://forms.gle/3uRz7FmzUA26Kw4y5
Forwarded from Только не .NET
В одном из чатов проскользнула мысль, что у одного известного блоггера какая-то своя версия ООП, противоречащая объективной реальности.
Объективная реальность — это про естественные науки и технику, а не социальное.
Как оптимальнее двигать байты и жонглировать инструкциями процессора — это объективная реальность. Но код сейчас пишут не только для компьютера, но и для людей, которые будут его поддерживать.
И это норма, что разным людям удобно разное, соответственно про ООП, про ДДД, про девопс — у каждого своя секта
Объективная реальность — это про естественные науки и технику, а не социальное.
Как оптимальнее двигать байты и жонглировать инструкциями процессора — это объективная реальность. Но код сейчас пишут не только для компьютера, но и для людей, которые будут его поддерживать.
И это норма, что разным людям удобно разное, соответственно про ООП, про ДДД, про девопс — у каждого своя секта
SpinWait и Thread.Sleep(1)
Иногда возникает задача дождаться в цикле наступления какого-либо события. Примерно вот так:
Такой цикл имеет смысл делать только если под код выделено отдельное ядро процессора — иначе, он долго не будет отдавать свой квант времени CPU другим потокам, что приведёт к ухудшению производительности.
Для исправления этой ситуации в .NET есть примитив SpinWait. Используется он так:
Внутри — это лишь счётчик с числом итераций, который комбинирует разные способы ожидания:
-
-
-
И проблема в последнем способе — поток на самом деле приостанавливается не на 1 миллисекунду, а на 15 миллисекунд (размер тика в ОС — зависит от платформы и загруженности системы).
В нагруженном сервисе такая большая пауза может привести к лишним задержкам и переполнению буферов с данными. Сервис не сможет мгновенно реагировать на появившиеся данные.
Варианты решения проблемы:
- увеличить число итераций, после которых будет вызываться
- отключить
- делать логику не на ожидании через SpinWait, а через сигналы, например
@epeshkblog | Поддержать канал
█░░░░░░░░░░░░ 23 / 300
Иногда возникает задача дождаться в цикле наступления какого-либо события. Примерно вот так:
while (!condition()) { }
Такой цикл имеет смысл делать только если под код выделено отдельное ядро процессора — иначе, он долго не будет отдавать свой квант времени CPU другим потокам, что приведёт к ухудшению производительности.
Для исправления этой ситуации в .NET есть примитив SpinWait. Используется он так:
var spinWait = new SpinWait();
while (!condition())
spinWait.SpinOnce();
spinWait.Reset();
Внутри — это лишь счётчик с числом итераций, который комбинирует разные способы ожидания:
-
Thread.SpinWait(n)
— busy loop без отдачи процессорного времени другим потокам, но с сообщением процессору, что выполняется ожидание и можно снизить частоту (энергопотребление) ядра, например путём вставки инструкции pause
-
Thread.Yield()
и Thread.Sleep(0)
— отдают ядро другому потоку, если CPU-ресурсов в системе не хватает и образовалась очередь потоков на выполнение.-
Thread.Sleep(1)
— принудительно приостанавливает выполнение потока, даже если очереди нет. Используется спустя 20 итераций spinWait.SpinOnce
И проблема в последнем способе — поток на самом деле приостанавливается не на 1 миллисекунду, а на 15 миллисекунд (размер тика в ОС — зависит от платформы и загруженности системы).
В нагруженном сервисе такая большая пауза может привести к лишним задержкам и переполнению буферов с данными. Сервис не сможет мгновенно реагировать на появившиеся данные.
Варианты решения проблемы:
- увеличить число итераций, после которых будет вызываться
Thread.Sleep(1)
, передав параметр в метод SpinOnce
- отключить
Thread.Sleep(1)
: spinWait.SpinOnce(-1)
. Это приведёт к увеличению потребления CPU в простое- делать логику не на ожидании через SpinWait, а через сигналы, например
Monitor.Pulse
, ManualResetEventSlim
, TaskCompletionSource
@epeshkblog | Поддержать канал
█░░░░░░░░░░░░ 23 / 300
Monitor
Конструкция
Но класс
Happy path
Метод
В библиотеке Serilog.Sinks.RawConsole я использовал
В итоге, рендеринг лог-сообщений в текст/json выполняется параллельно, с дополнительным бонусом в виде отсутствия копирования, если конкуренции нет (повезло, или используется асинхронная обёртка Serilog.Sinks.Async, Serilog.Sinks.Background)
Также
Сигналы
Кроме блокировок, класс
Чтобы это сделать, вначале нужно захватить lock на объекте.
Затем, вызвав метод
Другой поток может вызвать
Пример с двумя потоками:
Результат:
Пример с N потоками и M сигналами остаётся на самостоятельное изучение
Такой способ передачи сигналов я использовал в библиотеке Serilog.Sinks.Background для того, чтобы сигнализировать background-поток о появлении новых лог-сообщений. Но, с дополнительными доработками:
- логирующие потоки не ждут на локе друг друга — они выбирают, какой пойдёт сигнализировать через
- один log event не активирует background-поток — нужно, чтобы в очереди набралось некоторое их количество
- на случай, если логов мало — background-поток активируется по таймауту. В итоге при отладке и выводе логов в консоль — они появляются на экране почти мгновенно, в 60 ФПС (дальше зависит от того, как быстро рендерит терминал)
Однако, такой способ очень рискованный — если никакой поток не вызвал
Метрики
Через свойство
@epeshkblog | Поддержать канал
█░░░░░░░░░░░░ 28 / 300
Конструкция
lock (obj) { /* do something */ }
в C# — синтаксический сахар для методов класса Monitor
:Monitor.Enter(obj);
try
{
// do something
}
finally
{
Monitor.Exit(obj);
}
Но класс
Monitor
— самодостаточный. Его использование напрямую, без конструкции lock
позволяет реализовать несколько интересных механик.Happy path
Метод
Monitor.TryEnter
позволяет зайти в критическую секцию, если она свободна от других потоков. Можно сделать разную логику в зависимости от того, есть ли конкуренция с другими потоками.В библиотеке Serilog.Sinks.RawConsole я использовал
TryEnter
, чтобы реализовать следующую логику — если конкуренции нет, то log event рендерится в текст/json под локом, сразу в общий буфер. Если не повезло и лок занят — рендеринг текста происходит в локальный для потока буфер, затем берётся лок и результат копируется в общий буфер.В итоге, рендеринг лог-сообщений в текст/json выполняется параллельно, с дополнительным бонусом в виде отсутствия копирования, если конкуренции нет (повезло, или используется асинхронная обёртка Serilog.Sinks.Async, Serilog.Sinks.Background)
Также
Monitor.TryEnter
позволяет задать таймаут ожидания блокировки.Сигналы
Кроме блокировок, класс
Monitor
можно использовать для передачи сигналов между потоками.Чтобы это сделать, вначале нужно захватить lock на объекте.
Затем, вызвав метод
Monitor.Wait
, можно заблокировать поток и освободить лок.Другой поток может вызвать
Pulse
или PulseAll
— тогда, при выходе из критической секции возобновится выполнение потоков, ждущих с помощью Wait
. PulseAll
сигнализирует все потоки, а Pulse
— один (при этом, Pulse
можно вызвать несколько раз чтобы сигнализировать нужное число потоков).Пример с двумя потоками:
var obj = new object();
var t1 = new Thread(() =>
{
lock (obj)
{
Console.WriteLine("entered lock on thread 1");
Monitor.Wait(obj);
Console.WriteLine("got signal on thread 1");
}
});
var t2 = new Thread(() =>
{
Thread.Sleep(1000);
lock (obj)
{
Console.WriteLine("entered lock on thread 2");
Monitor.Pulse(obj);
Console.WriteLine("exiting lock on thread 2"); // ещё не разбудили первый поток, это случится лишь по выходу из лока.
}
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Результат:
entered lock on thread 1
entered lock on thread 2
exiting lock on thread 2
got signal on thread 1
Пример с N потоками и M сигналами остаётся на самостоятельное изучение
Такой способ передачи сигналов я использовал в библиотеке Serilog.Sinks.Background для того, чтобы сигнализировать background-поток о появлении новых лог-сообщений. Но, с дополнительными доработками:
- логирующие потоки не ждут на локе друг друга — они выбирают, какой пойдёт сигнализировать через
Interlocked
- один log event не активирует background-поток — нужно, чтобы в очереди набралось некоторое их количество
- на случай, если логов мало — background-поток активируется по таймауту. В итоге при отладке и выводе логов в консоль — они появляются на экране почти мгновенно, в 60 ФПС (дальше зависит от того, как быстро рендерит терминал)
Однако, такой способ очень рискованный — если никакой поток не вызвал
Monitor.Wait
, то сигнал, отправленный через Monitor.Pulse
будет потерян, что может приводить к дедлокам. Избежать этого помогают более высокоуровневые примитивы синхронизации, но об этом будет уже следующий пост.Метрики
Через свойство
Monitor.LockContentionCount
можно узнать, сколько раз за время выполнения программы потоки останавливались на блокировках, реализованных через класс Monitor
. SpinLock, например, в эту статистику не входит, да и вообще не проявляется в метриках, чем уступает обычным локам в плане поддерживаемости.@epeshkblog | Поддержать канал
█░░░░░░░░░░░░ 28 / 300
GitHub
serilog-sinks-rawconsole/src/Serilog.Sinks.RawConsole/Sinks/RawStreamSink.cs at master · epeshk/serilog-sinks-rawconsole
Write logs to console efficienly. Contribute to epeshk/serilog-sinks-rawconsole development by creating an account on GitHub.
Чтобы разбавить тематику про многопоточность и производительность, ищу экспертов в .NET-хайпе
Особенно интересно разоблачить менеджеров Майкрософта про Blazor и AI&ML на .NET. Или наоборот — не разоблачить, а подтвердить то, что написано в пресс-релизах.
Если у вас есть пост по этим темам — присылайте, репостну в канал
Особенно интересно разоблачить менеджеров Майкрософта про Blazor и AI&ML на .NET. Или наоборот — не разоблачить, а подтвердить то, что написано в пресс-релизах.
Если у вас есть пост по этим темам — присылайте, репостну в канал
GUI на .NET: сравнение XAML-фреймворков
WPF — качественный GUI-фреймворк, но с двумя недостатками: работает только под Windows и больше не развивается.
В чат принесли статью со сравнением альтернатив WPF-у.
Краткие итоги сравнения:
- Avalonia — лучший выбор для кроссплатформенного десктопа
- UNO — для мобилок и веба
- MAUI годен только для мобилок
- WPF всё ещё можно спасти малой ценой (без переписывания), если использовать Wine для запуска на Linux или купить XPF
@epeshkblog | Поддержать канал
█░░░░░░░░░░░░ 29 / 300
WPF — качественный GUI-фреймворк, но с двумя недостатками: работает только под Windows и больше не развивается.
В чат принесли статью со сравнением альтернатив WPF-у.
Краткие итоги сравнения:
- Avalonia — лучший выбор для кроссплатформенного десктопа
- UNO — для мобилок и веба
- MAUI годен только для мобилок
- WPF всё ещё можно спасти малой ценой (без переписывания), если использовать Wine для запуска на Linux или купить XPF
@epeshkblog | Поддержать канал
█░░░░░░░░░░░░ 29 / 300
string.Intern
Сергей Тепляков написал статью о перформанс-ловушке при использовании метода
-
- в NativeAOT проблема исправлена — чтение из кэша происходит без блокировки, используется реализация кэша аналогичная старому
- самый простой способ сделать интернирование быстрее на всех платформах — использовать
====
Хэш-таблица, позволяющая чтение без блокировок также есть в моей библиотеке ConcurrencyToolkit, под названием
@epeshkblog | Поддержать канал
█░░░░░░░░░░░░ 29 / 300
Сергей Тепляков написал статью о перформанс-ловушке при использовании метода
string.Intern
.-
string.Intern
в .NET слишком медленный из-за блокировки, которая берётся при доступе к кэшу интернированных строк- в NativeAOT проблема исправлена — чтение из кэша происходит без блокировки, используется реализация кэша аналогичная старому
Hashtable
- самый простой способ сделать интернирование быстрее на всех платформах — использовать
ConcurrentDictionary
вместо string.Intern
====
Хэш-таблица, позволяющая чтение без блокировок также есть в моей библиотеке ConcurrencyToolkit, под названием
SingleWriterDictionary
.@epeshkblog | Поддержать канал
█░░░░░░░░░░░░ 29 / 300
Dissecting the Code
String Interning - To Use or Not to Use? A Performance Question
I recently join a new team and one of the projects was having a high memory footprint issues. There are a few mitigations put in place and one of them was to de-duplicate strings by using string interning.
С наступающим! За то, чтобы самым волнующим событием года стал релиз .NET 9
NUnit 4
Одна из библиотек для Unit-тестирования в .NET обновилась — вышла версия 4.
Changelog — https://docs.nunit.org/articles/nunit/release-notes/framework.html#nunit-400
Уже попробовали?
@epeshkblog | Поддержать канал
Одна из библиотек для Unit-тестирования в .NET обновилась — вышла версия 4.
Changelog — https://docs.nunit.org/articles/nunit/release-notes/framework.html#nunit-400
Уже попробовали?
@epeshkblog | Поддержать канал
Telegram
.NET epeshk blog
Канал с заметками о C# и .NET
Поддержать канал: https://t.me/blog_donate/2
Обратная связь: https://forms.gle/3uRz7FmzUA26Kw4y5
Поддержать канал: https://t.me/blog_donate/2
Обратная связь: https://forms.gle/3uRz7FmzUA26Kw4y5
Forwarded from George Drak
Всем привет. Может быть кому-то ещё пригодится. Иногда дебаггер в Rider не может показать значения некоторых переменных, особенно если проваливаться во внешние библиотеки. Выглядит это как ошибка Evaluation is not allowed: The thread is not at a GC-safe point. Оказывается, дебаггеру можно немного помочь, поотключав у приложения различные JIT-оптимизации при помощи переменных окружения.
Это можно сделать прямо в конфиге запуска. После добавления этих настроек у меня стало показывать все значения во внутрянке типа Microsoft.Extensions.DependencyInjection.
SET COMPLUS_ZapDisable=1 NGen off (CLR)
SET COMPLUS_JitMinOpts=1 Disable as much JIT optimizations as possible (CoreCLR)
SET COMPlus_TieredCompilation=0 No tiered JIT, only do one pass (CoreCLR)
SET COMPLUS_ReadyToRun=0 Don't do netcore's analog to NGen (CoreCLR)
Это можно сделать прямо в конфиге запуска. После добавления этих настроек у меня стало показывать все значения во внутрянке типа Microsoft.Extensions.DependencyInjection.
.NET epeshk blog
Зачем вообще это нужно? Раньше в .NET было принято обмазываться рефлексией, экспрешенами, и прочей кодогенерацией в рантайме. Изредка, если надо было сгенерировать много однотипного кода — использовались T4 шаблоны, вывод которых коммитился в репозиторий.…
PureDI https://habr.com/ru/articles/795809/
Вышла новая версия PureDI — легковесного DI-контейнера на основе source генераторов.
PureDI не реализует все фичи обычных контейнеров, собирающих граф объектов в рантайме, зато:
- В программе не остаётся зависимостей от DI-библиотек
- Не тратит время работы программы на сбор графа объектов, по скорости такой же, как создание объектов вручную через
- Проверяет корректность конфигурации при компиляции, а не в рантайме
Вышла новая версия PureDI — легковесного DI-контейнера на основе source генераторов.
PureDI не реализует все фичи обычных контейнеров, собирающих граф объектов в рантайме, зато:
- В программе не остаётся зависимостей от DI-библиотек
- Не тратит время работы программы на сбор графа объектов, по скорости такой же, как создание объектов вручную через
new
. Это полезно для короткоживущих программ, где полезная работа сравнима по времени с работой DI-контейнера, хорошо сочетается с NativeAOT- Проверяет корректность конфигурации при компиляции, а не в рантайме
Хабр
Pure.DI v2.1
С момента выхода генератора исходного кода Pure.DI версии 2.0 прошло уже больше, чем полгода. За это время появились отзывы по использованию, удалось добавить несколько полезных фич, улучшить...
🔓deadlock внутри lock
Считается, что lock — надёжная конструкция, а если пытаться использовать неблокирующую синхронизацию, то всюду будут баги и race condition. Например так написано в FAQ для разработчиков дотнета.
Однако правда в том, что программа зависнет в любом случае, независимо от вашего выбора.
Например, если ваша программа работает на Linux и периодически все managed-потоки в ней зависают, причиной может быть баг в glibc. В реализации критической секции в .NET (класс
Баг чаще воспроизводится при большом числе конкурирующих потоков на малое количество ядер. У меня воспроизвелось на 4 ядрах/48 потоках почти сразу. В реальности программа может проработать несколько дней и зависнуть неожиданно.
Баг занесён в glibc версии 2.27, и по прежнему не исправлен, несмотря на наличие фикса. Разработчики Linux-дистрибутивов сами решают, готовы ли они добавить исправление в свою сборку или нет. В Ubuntu фикс был включён в glibc версии 2.31-0ubuntu9.9 (требуется Ubuntu 20.04 или выше). Проверить версию glibc можно командой
Способ исправления — обновить glibc в системе, или перейти на образ, в котором используется glibc c исправлением. Если такой возможности нет — можно пересобрать glibc, пример есть в этом issue из dotnet/runtime
@epeshkblog | Поддержать канал
Считается, что lock — надёжная конструкция, а если пытаться использовать неблокирующую синхронизацию, то всюду будут баги и race condition. Например так написано в FAQ для разработчиков дотнета.
Однако правда в том, что программа зависнет в любом случае, независимо от вашего выбора.
Например, если ваша программа работает на Linux и периодически все managed-потоки в ней зависают, причиной может быть баг в glibc. В реализации критической секции в .NET (класс
Monitor
) на Linux используется функция pthread_cond_wait
. Из-за бага она может пропустить сигнал и никогда не разблокировать ждущий поток. Далее, когда случится stop-the-world GC, он заблокирует остальные потоки программы, а затем зависнет сам, не справившись с потоками, которые застряли внутри проблемной функции. Аналогичной проблеме подвержены и другие рантаймы, например Python.Баг чаще воспроизводится при большом числе конкурирующих потоков на малое количество ядер. У меня воспроизвелось на 4 ядрах/48 потоках почти сразу. В реальности программа может проработать несколько дней и зависнуть неожиданно.
Баг занесён в glibc версии 2.27, и по прежнему не исправлен, несмотря на наличие фикса. Разработчики Linux-дистрибутивов сами решают, готовы ли они добавить исправление в свою сборку или нет. В Ubuntu фикс был включён в glibc версии 2.31-0ubuntu9.9 (требуется Ubuntu 20.04 или выше). Проверить версию glibc можно командой
ldd --version
Способ исправления — обновить glibc в системе, или перейти на образ, в котором используется glibc c исправлением. Если такой возможности нет — можно пересобрать glibc, пример есть в этом issue из dotnet/runtime
@epeshkblog | Поддержать канал
GitHub
runtime/docs/coding-guidelines/clr-code-guide.md at main · dotnet/runtime
.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps. - dotnet/runtime
Forwarded from DotNext — конференция для .NET‑разработчиков
Начинаем постепенно открывать по пятницам #видеозаписи осеннего DotNext.
Первый доклад — от Евгения Пешкова, которого многие уже знают по предыдущим выступлениям.
Первый доклад — от Евгения Пешкова, которого многие уже знают по предыдущим выступлениям.