JavaRush /Курсы /C# SELF /Использование BufferedStre...

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

C# SELF
41 уровень , 2 лекция
Открыта

1. Введение

Прежде, чем начинать использовать новый класс, стоит понять, зачем он вообще нужен. Давайте разберёмся, что происходит, когда мы работаем с файлом «напрямую» с помощью FileStream.

Когда вы вызываете Read или Write для потока, созданного через FileStream, реально происходит обращение к дисковой подсистеме компьютера. Сам по себе этот процесс (особенно на старых жёстких дисках, но и на новых SSD тоже) гораздо медленнее, чем работа с оперативной памятью. Представьте, что когда вы заказываете в McDonalds картошку фри, то кассир каждый раз бежит за новым пакетом на склад. Представьте, какой длинной окажется очередь!

Если работать с небольшими кусочками данных, частые обращения к диску или сети приводят к потере производительности. Чем больше объём данных — тем заметнее эффект.

Краткая аналогия

Выходит, что потоки без буфера — это примерно как ездить за покупками в магазин 10 раз, чтобы купить по одному йогурту за раз. Буферизованный поток — это когда вы берёте сразу целую корзину йогуртов, сократив количество поездок до минимума.

2. Класс BufferedStream: первый взгляд

Для чего он нужен

BufferedStream — это обёртка над любым потоком (Stream), которая хранит промежуточный буфер в памяти. Когда вы записываете данные, они сначала попадают в буфер, и только когда буфер заполняется — сбрасываются на диск одной большой операцией. Аналогично с чтением: при первом чтении он загружает значительный кусок данных в память, а затем возвращает по кусочку из памяти, пока буфер не истощится.

Пример кода: создание BufferedStream

Давайте создадим простейший пример. Пусть у нас есть задача записать 100 000 строк в файл:


string filePath = "big_output.txt";
using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write);
using var bufferedStream = new BufferedStream(fileStream);
using var writer = new StreamWriter(bufferedStream);

for (int i = 0; i < 100_000; i++)
{
    writer.WriteLine($"Строка номер {i}");
}

Console.WriteLine("Запись завершена!");

Комментарий:

  • Мы открываем файл для записи через FileStream.
  • Затем оборачиваем его в BufferedStream, а уже затем — в StreamWriter (он пишет текстовые строки в поток).
  • Как только буфер заполняется — данные сбрасываются на диск одной порцией.

3. Как работает буферизация «изнутри»

Давайте разберём это на схеме:


[Ваш код] → [StreamWriter] → [BufferedStream] → [FileStream] → [Файл на диске]

Когда вы вызываете метод WriteLine() у StreamWriter, текст сначала записывается во внутренний буфер, потом через BufferedStream — ещё в один буфер, а уже потом, когда буфер заполнится или поток будет закрыт, данные сбрасываются на диск.

Сколько байт в одном ведре?

Размер буфера по умолчанию — 4096 байт (4 КБ), но его можно указать явно:


int myBufferSize = 16 * 1024; // 16 КБ
using var fileStream = new FileStream(filePath, FileMode.Create);
using var bufferedStream = new BufferedStream(fileStream, myBufferSize);
// ...

Практический лайфхак: На современных системах разумно использовать буферы 8–64 КБ. Для очень больших файловых операций — и больше. Но не увлекайтесь: если вы работаете на микроконтроллере с 128 КБ оперативки, то 64 КБ буфера — плохая мысль :)

4. Эксперимент: сравним скорость с буфером и без

Чтобы понять, насколько это важно, давайте напишем тест, который сравнивает запись данных через FileStream с буфером и без:


using System.Diagnostics;
using System.Text;

string data = new string('X', 1000); // 1 000 символов

void WriteWithoutBuffer()
{
    using var fs = new FileStream("no_buffer.txt", FileMode.Create, FileAccess.Write, FileShare.None, 4096, useAsync: false);
    for (int i = 0; i < 10_000; i++)
    {
        byte[] bytes = Encoding.UTF8.GetBytes(data);
        fs.Write(bytes, 0, bytes.Length); // Прямо в файл – каждый раз обращение к диску
    }
}

void WriteWithBuffer()
{
    using var fs = new FileStream("with_buffer.txt", FileMode.Create, FileAccess.Write, FileShare.None);
    using var bs = new BufferedStream(fs, 16 * 1024);
    for (int i = 0; i < 10_000; i++)
    {
        byte[] bytes = Encoding.UTF8.GetBytes(data);
        bs.Write(bytes, 0, bytes.Length);
    }
}

// Засекаем время
Stopwatch sw = Stopwatch.StartNew();
WriteWithoutBuffer();
sw.Stop();
Console.WriteLine("Без буфера: " + sw.ElapsedMilliseconds + " мс");

sw.Restart();
WriteWithBuffer();
sw.Stop();
Console.WriteLine("С буфером: " + sw.ElapsedMilliseconds + " мс");

Ожидаемый вывод:
В большинстве случаев с буфером вы увидите заметный прирост скорости! Особенно, если у вас HDD-диск. Если же это SSD — эффект всё равно заметен, но не так драматичен.

5. Какой буфер выбрать? Сравнение и практика

В .NET много классов для буферизации. Давайте попробуем внести ясность:

Класс Для чего используется Буфер встроен? Нужно использовать BufferedStream?
FileStream
Работа с файлами Да (с 4 КБ) Почти не нужно (но можно)
NetworkStream
Работа с сетью Нет Очень желательно
StreamReader/Writer
Чтение/запись текста Да (от 1 КБ) Обычно не нужно
GZipStream
Сжатие/распаковка Нет Можно/нужно для ускорения

Важно:
FileStream с параметром конструктора bufferSize — по сути уже буферизованный поток. Если вы самостоятельно указали достаточно большой буфер, то дополнительный BufferedStream большого выигрыша не даст. Но если вы пользуетесь другим потоком (например, сетевым), то BufferedStream — ваш друг.

6. Пример: Копирование файла с помощью BufferedStream


string source = "big_input.dat";
string dest = "big_output.dat";

int bufferSize = 64 * 1024; // 64 КБ

using var inputStream = new FileStream(source, FileMode.Open, FileAccess.Read);
using var outputStream = new FileStream(dest, FileMode.Create, FileAccess.Write);
using var bufferedInput = new BufferedStream(inputStream, bufferSize);
using var bufferedOutput = new BufferedStream(outputStream, bufferSize);

byte[] buffer = new byte[bufferSize];
int bytesRead;
while ((bytesRead = bufferedInput.Read(buffer, 0, buffer.Length)) > 0)
{
    bufferedOutput.Write(buffer, 0, bytesRead);
}
// Не забудьте flush – иначе последние байты не попадут на диск!
bufferedOutput.Flush();

Console.WriteLine("Копирование завершено!");

Комментарий:

  • Читаем данные большими блоками (64 КБ) из одного файла через BufferedStream.
  • Записываем их в другой файл, также используя буфер.
  • После окончания цикла обязательно вызываем Flush() для записи последних данных.

7. Полезные нюансы

Совет: когда действительно нужен BufferedStream

  • Если вы работаете с потоками, которые буферизации не имеют (например, сетевые потоки, или кастомные наследники Stream);
  • Если вы работаете с большими объемами бинарных данных (например, копирование файлов, преобразование форматов, резервное копирование);
  • Если вы оптимизируете уже существующий код и видите, что узким местом является большое количество мелких операций Write/Read.

Немного про асинхронность и буферизацию

С появлением асинхронных операций (ReadAsync/WriteAsync) буферизация осталась полезной, но стоит помнить: если вы используете асинхронные методы поверх буфера, обработка всё равно идёт внутри памяти, а физическое взаимодействие с диском ещё сильнее минимизируется.

В .NET 8+ и .NET 9 буферизация встроена всё глубже, и большинство классов уже по умолчанию имеют буферы. Но для совместимости с сетевыми потоками или вашими собственными реализациями всё ещё стоит использовать BufferedStream вручную.

Подробнее про асинхронность вы узнаете в уровне 58 :P

Визуальная схема работы буферизированных потоков

flowchart LR
    A[Ваш код] --> B[StreamReader/Writer]
    B --> C[BufferedStream]
    C --> D[FileStream]
    D --> E[Файл/Устройство]
  • A — Ваш код, который вызывает Write/Read.
  • B — Поток высокого уровня (работает с текстом или данными).
  • C — Буферизация (группирует данные для повышения скорости).
  • D — Конкретная реализация потока (файл, сеть).
  • E — Физическое устройство (жёсткий диск, SSD, сеть и т.д.).

Советы и трюки из практики

  1. Если вы пишете по одной строке в файл (например, логгирование), лучше явно указывать размер буфера больше, чем размер одной строки. Это позволит быстрее выгружать большие пачки данных.
  2. Если каждое действие должно быть мгновенно записано (например, критические логи), вызывайте Flush() после каждой записи. Но это уменьшает преимущество буферизации!
  3. Если вы создаёте временные файлы, которые после создания сразу же удаляются, может быть неважно, осталось ли что-то в буфере — но будьте осторожны, если важно, чтобы файл точно был записан.
  4. При работе с очень большими файлами (например, десятки гигабайт) не стесняйтесь повышать размер буфера до 1_048_576 байт (1 МБ) и выше — главное, чтобы хватало оперативки.

8. Типичные ошибки и нюансы использования

Если сейчас вы почувствовали азарт и желание «везде воткнуть буфер» — не спешите. Всё хорошо в меру!

Одна из частых ошибок — забывать вызвать Flush() или закрыть поток, когда это нужно. Если поток ещё не закрылся, а программа аварийно завершилась, последние байты могут остаться в буфере в оперативной памяти и не попасть на диск. Например, если вы пишете логгирование в файл и программа падает, последняя запись может исчезнуть.

BufferedStream сам по себе не «видит» конца ваших логических сообщений — он просто ждёт, пока накопится порция данных нужного размера. Поэтому для критически важных вещей (логирование, резервные копии и т.д.) лучше периодически принудительно вызывать Flush():

bufferedStream.Flush(); // Заставляет буфер выгрузить данные на диск

Если вы пользуетесь StreamWriter, то у него есть собственный буфер! То есть при вложенном использовании буферизация происходит дважды (и это не всегда хорошо). Часто достаточно одного уровня буфера, и если вы используете StreamWriter, то дополнительный BufferedStream может быть не нужен.

2
Задача
C# SELF, 41 уровень, 2 лекция
Недоступна
Копирование содержимого файла с использованием BufferedStream
Копирование содержимого файла с использованием BufferedStream
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ