C# Ready | Unity
10.6K subscribers
1.52K photos
85 videos
757 links
Авторский канал по разработке на C# и Unity.
Ресурсы, гайды, задачи, шпаргалки.
Информация ежедневно пополняется!

Cотрудничество: @energy_c

Реклама на бирже: https://telega.in/channels/csharp_ready/card

РКН: https://clck.ru/3SBaT3
Download Telegram
Почему Task.Run не делает синхронный ввод-вывод асинхронным?

Оборачивать блокирующее чтение файла в Task.Run кажется способом освободить текущий поток, но ожидание всё равно занимает поток из пула:
await Task.Run(() => File.ReadAllText(path));


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

Для чтения файла используем API с настоящей асинхронной операцией:
string text = await File.ReadAllTextAsync(
    path, cancellationToken);


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

Токен отмены передаём и в сам метод чтения, и дальше по цепочке:
await using var stream = File.OpenRead(path);
using var reader = new StreamReader(stream);
string text = await reader.ReadToEndAsync(token);


Для CPU-вычислений Task.Run может быть уместен, например чтобы не блокировать UI-поток:
var result = await Task.Run(
    () => Calculate(input, token), token);


Синхронная функция должна сама проверять отмену, потому что токен не прерывает уже запущенный код:
foreach (var item in items)
{
    token.ThrowIfCancellationRequested();
    Process(item);
}


Независимые операции с асинхронным API запускаем напрямую и ожидаем вместе:
Task<string> config = File.ReadAllTextAsync(
    configPath);
Task<string> data = File.ReadAllTextAsync(
    dataPath);
await Task.WhenAll(config, data);


Так ожидание I/O не расходует потоки на блокировку. В серверном коде Task.Run вокруг обычного ввода-вывода чаще добавляет переключение потоков, а не пропускную способность.

➡️ C# Ready | #совет
Please open Telegram to view this post
VIEW IN TELEGRAM
❤5👍4🔥2
Строим порядок сборки из зависимостей!

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

В этой задаче:
• строим граф зависимостей и считаем входящие связи
• обрабатываем готовые задачи через очередь
• проверяем, что в графе не осталось цикла


Алгоритм Кана проходит каждую задачу и каждую связь один раз.

➡️ C# Ready | #задача
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6❤3🔥3
Строим ограниченную очередь фоновых задач через Channel<T>!

Если задания поступают быстрее, чем обрабатываются, неограниченная очередь продолжает расти и расходует память. Bounded channel задаёт верхний предел, а производитель асинхронно ждёт свободное место.

Создаём очередь на 32 задания:
var queue = Channel.CreateBounded<Job>(
    new BoundedChannelOptions(32)
    { FullMode = BoundedChannelFullMode.Wait });


Режим Wait не отбрасывает новые элементы при заполнении. Вместо этого WriteAsync приостанавливает производителя до появления свободного слота.

Передаём задания с поддержкой отмены:
await queue.Writer.WriteAsync(job, token);
await queue.Writer.WriteAsync(nextJob, token);


Токен позволяет отменить ожидание записи, если приложение завершает операцию. В реальном producer-коде задания обычно записываются в цикле, а ошибки производства обрабатываются отдельно.

Потребитель читает элементы по мере появления:
await foreach (var job in queue.Reader
    .ReadAllAsync(token))
{
    await HandleAsync(job, token);
}


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

Для нескольких производителей можно завершить запись после их окончания:
try { await ProduceAsync(queue.Writer, token); }
finally { queue.Writer.TryComplete(); }


TryComplete закрывает очередь для новых записей, но позволяет обработать уже добавленные задания. Если producer завершился с исключением, его можно передать в TryComplete(error), чтобы consumer увидел причину остановки.

Запускаем производство и чтение вместе:
var consumer = ConsumeAsync(queue.Reader, token);
Exception? error = null;
try { await ProduceAsync(queue.Writer, token); }
catch (Exception ex) { error = ex; }
queue.Writer.TryComplete(error);
await consumer;


Ёмкость 32 ограничивает только число ожидающих элементов, но не время обработки.

➡️ C# Ready | #практика
Please open Telegram to view this post
VIEW IN TELEGRAM
❤6👍4🔥2
Шпаргалка по class, record и struct в C#!

Например, class и record class являются ссылочными типами, а struct и record struct передают значения как отдельные данные.

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

Сохрани, чтобы не потерять!

➡️ C# Ready | #шпора
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥17👍8❤4🤝4
❤6👍4🔥4
Почему ValueTask не стоит использовать ради ускорения?

ValueTask часто воспринимают как более быстрый Task. На практике он полезен не всегда и может сделать код сложнее.

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

Такой метод может вернуть готовое значение без лишней аллокации:
if (cache.TryGetValue(id, out var user))
{
return new ValueTask(user);
}


А если значения нет, можно перейти к обычной async-операции:
return new ValueTask(LoadAsync(id));


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

Плохой пример выглядит так:
var result = GetUserAsync(id);
await result;
await result;


С Task такое обычно безопасно, а с ValueTask можно получить неожиданное поведение, особенно если внутри используется IValueTaskSource.

Если метод почти всегда делает настоящий async I/O, лучше оставить Task:
Task LoadUserAsync(int id)
{
return db.Users.FindAsync(id).AsTask();
}


ValueTask используйте только там, где на то есть причина и польза.

➡️ C# Ready | #совет
Please open Telegram to view this post
VIEW IN TELEGRAM
❤4👍2🔥2
Media is too big
VIEW IN TELEGRAM
CSharpGuide по C# и .NET!

Каталог разбит на шесть этапов, основы языка, управление потоком, методы и коллекции, ООП, продвинутый C# и практический .NET. Внутри этапов собраны отдельные уроки по переменным, LINQ, async/await, JSON, тестированию и другим темам.

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

Оставляю ссылочку на CSharpGuide


➡️ C# Ready | #ресурс
Please open Telegram to view this post
VIEW IN TELEGRAM
❤10👍9🔥4
Почему арендованный ArrayPool буфер нельзя использовать как обычный массив?

ArrayPool<T> позволяет повторно использовать массивы и уменьшать число выделений памяти. Но Rent может вернуть массив больше запрошенного, а его содержимое может остаться от предыдущего владельца.

Запрашиваем буфер для нужной операции:
byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);


Размер массива может быть больше 4096. Поэтому передавай дальше только диапазон, в который действительно записаны данные:
int written = await stream.ReadAsync(buffer);
await destination.WriteAsync(buffer.AsMemory(0, written));


Если отправить весь массив, в файл или ответ попадут лишние байты. В них может оказаться старое содержимое, а не только нули.

Возвращем массив в пул даже при исключении:
try { await ProcessAsync(buffer.AsMemory(0, written)); }
finally { ArrayPool<byte>.Shared.Return(buffer); }


После Return считаем буфер чужим и не сохраняй на него ссылки. Следующий Rent может получить этот же массив другой операции, пока прежний код продолжает его читать.

Для паролей, токенов и других секретов очищаем массив при возврате:
ArrayPool<byte>.Shared.Return(buffer, clearArray: true);


➡️ C# Ready | #совет
Please open Telegram to view this post
VIEW IN TELEGRAM
❤4👍2🔥2