TGViewer
C# 1001 notes C# 1001 notes @csharp_1001_notes · 6.64K subscribers
Post #909 855
# C# 14: хитрая задача на ArrayPool, IAsyncEnumerable и время жизни памяти

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

Наивная реализация:


static async IAsyncEnumerable<ReadOnlyMemory<byte>> ReadLinesAsync(
Stream stream,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);

try
{
while (true)
{
int read = await stream.ReadAsync(buffer, cancellationToken);

if (read == 0)
yield break;

yield return buffer.AsMemory(0, read);
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}


Использование:


await foreach (var data in ReadLinesAsync(stream))
{
queue.Add(data);
}


Код компилируется и выглядит эффективно.

Но в production данные внутри queue иногда внезапно меняются или повреждаются.

## Вопрос

Почему?

---

# Проблема

ReadOnlyMemory<byte> не владеет памятью.

Он всего лишь указывает на:


byte[] buffer


А этот массив взят из:


ArrayPool<byte>.Shared


После следующего:


ReadAsync(buffer)


содержимое массива перезаписывается.

А после:


ArrayPool<byte>.Shared.Return(buffer);


массив вообще может получить другой поток.

Получается:


yield memory

consumer сохраняет ссылку

buffer переиспользуется

старый ReadOnlyMemory показывает новые данные


Это логический аналог use-after-free, хотя runtime C# остаётся memory-safe.

---

# Задача

Исправьте API так, чтобы:

- использовался пул памяти;
- данные можно было безопасно хранить после yield;
- не происходило скрытого копирования каждого сообщения;
- consumer явно управлял временем жизни буфера;
- отмена работала через CancellationToken.

Подсказка: используйте


IMemoryOwner<byte>


---

# Один из вариантов решения


public sealed class Message : IDisposable
{
private IMemoryOwner<byte>? _owner;

public ReadOnlyMemory<byte> Data { get; }

public Message(IMemoryOwner<byte> owner, int length)
{
_owner = owner;
Data = owner.Memory[..length];
}

public void Dispose()
{
_owner?.Dispose();
_owner = null;
}
}


Чтение:


static async IAsyncEnumerable<Message> ReadAsync(
Stream stream,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
while (true)
{
IMemoryOwner<byte> owner =
MemoryPool<byte>.Shared.Rent(4096);

int read;

try
{
read = await stream.ReadAsync(
owner.Memory,
cancellationToken);
}
catch
{
owner.Dispose();
throw;
}

if (read == 0)
{
owner.Dispose();
yield break;
}

yield return new Message(owner, read);
}
}


Consumer:


await foreach (var message in ReadAsync(stream, ct))
{
using (message)
{
Process(message.Data);
}
}


Теперь память возвращается в pool только тогда, когда consumer закончил с сообщением.

---

# Главная ловушка

Даже такой код опасен:


ReadOnlyMemory<byte> saved;

await foreach (var message in ReadAsync(stream))
{
using (message)
{
saved = message.Data;
}
}

Console.WriteLine(saved.Length);


После Dispose() память больше не принадлежит consumer.

Сам ReadOnlyMemory<byte> технически существует, но использовать его содержимое уже нельзя.

---

# Вопрос уровня Senior

Как изменить API так, чтобы разработчику было сложнее случайно сохранить ReadOnlyMemory<byte> после Dispose()?

Дополнительно подумайте:


ArrayPool<T>
vs
MemoryPool<T>
vs
обычный byte[]


и ответьте, когда zero-copy действительно быстрее, а когда управление lifetime становится дороже простой копии.
More from @csharp_1001_notes
  1. Sep 20, 2026🧩 C# Channels: сообщение попало в очередь. Почему обработчик его не получил? Два воркера…
  2. Sep 16, 2026Эта задача проверяет понимание Memory<T>, pooling, ownership, IAsyncEnumerable, cancellati…
  3. Sep 14, 2026# C# 14: конкурентный кеш без race condition Реализуйте потокобезопасный кеш: public seale…
  4. Sep 14, 2026🔍Тестовое собеседование с Senior C# разработчиком уже завтра 15 сентября(уже завтра!) в 1…
  5. Sep 11, 2026🔧 Как передать многострочный PEM через Aspire в ASP.NET Core Сертификаты и ключи в PEM со…
  6. Sep 10, 2026Согласны ?
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →