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? |
|---|---|---|---|
|
Работа с файлами | Да (с 4 КБ) | Почти не нужно (но можно) |
|
Работа с сетью | Нет | Очень желательно |
|
Чтение/запись текста | Да (от 1 КБ) | Обычно не нужно |
|
Сжатие/распаковка | Нет | Можно/нужно для ускорения |
Важно:
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, сеть и т.д.).
Советы и трюки из практики
- Если вы пишете по одной строке в файл (например, логгирование), лучше явно указывать размер буфера больше, чем размер одной строки. Это позволит быстрее выгружать большие пачки данных.
- Если каждое действие должно быть мгновенно записано (например, критические логи), вызывайте Flush() после каждой записи. Но это уменьшает преимущество буферизации!
- Если вы создаёте временные файлы, которые после создания сразу же удаляются, может быть неважно, осталось ли что-то в буфере — но будьте осторожны, если важно, чтобы файл точно был записан.
- При работе с очень большими файлами (например, десятки гигабайт) не стесняйтесь повышать размер буфера до 1_048_576 байт (1 МБ) и выше — главное, чтобы хватало оперативки.
8. Типичные ошибки и нюансы использования
Если сейчас вы почувствовали азарт и желание «везде воткнуть буфер» — не спешите. Всё хорошо в меру!
Одна из частых ошибок — забывать вызвать Flush() или закрыть поток, когда это нужно. Если поток ещё не закрылся, а программа аварийно завершилась, последние байты могут остаться в буфере в оперативной памяти и не попасть на диск. Например, если вы пишете логгирование в файл и программа падает, последняя запись может исчезнуть.
BufferedStream сам по себе не «видит» конца ваших логических сообщений — он просто ждёт, пока накопится порция данных нужного размера. Поэтому для критически важных вещей (логирование, резервные копии и т.д.) лучше периодически принудительно вызывать Flush():
bufferedStream.Flush(); // Заставляет буфер выгрузить данные на диск
Если вы пользуетесь StreamWriter, то у него есть собственный буфер! То есть при вложенном использовании буферизация происходит дважды (и это не всегда хорошо). Часто достаточно одного уровня буфера, и если вы используете StreamWriter, то дополнительный BufferedStream может быть не нужен.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ