CodeGym /Các khóa học /C# SELF /Sử dụng BufferedStream

Sử dụng BufferedStream

C# SELF
Mức độ , Bài học
Có sẵn

1. Giới thiệu

Trước khi bắt đầu dùng một lớp mới, nên hiểu nó cần để làm gì. Hãy tìm hiểu điều gì xảy ra khi ta làm việc với file "trực tiếp" bằng FileStream.

Khi bạn gọi Read hoặc Write trên một stream được tạo bằng FileStream, thực tế là có truy cập đến subsystem đĩa của máy tính. Quá trình này (đặc biệt trên HDD cũ, nhưng cả trên SSD mới cũng vậy) chậm hơn nhiều so với làm việc với RAM. Hãy tưởng tượng khi bạn gọi món khoai tây chiên ở McDonalds, nhân viên thu ngân chạy ra kho mỗi lần để lấy một gói mới. Hàng đợi sẽ rất dài!

Nếu làm việc với các mẩu dữ liệu nhỏ, truy cập thường xuyên tới đĩa hoặc mạng sẽ làm mất hiệu năng. Dữ liệu càng lớn — hiệu ứng càng rõ.

Tương tự ngắn gọn

Stream không có bộ đệm giống như đi siêu thị 10 lần để mua mỗi lần một hộp sữa chua. BufferedStream là khi bạn lấy cả giỏ sữa chua một lần, giảm số chuyến đi xuống tối thiểu.

2. Lớp BufferedStream: cái nhìn đầu tiên

Dùng để làm gì

BufferedStream là một wrapper quanh bất kỳ stream nào (Stream), giữ một buffer trung gian trong bộ nhớ. Khi bạn ghi dữ liệu, nó đầu tiên vào buffer, và chỉ khi buffer đầy — mới flush xuống đĩa bằng một thao tác lớn. Tương tự với đọc: lần đọc đầu tiên nạp một đoạn lớn vào bộ nhớ, rồi trả từng phần từ bộ nhớ cho đến khi buffer cạn.

Ví dụ mã: tạo BufferedStream

Hãy tạo ví dụ đơn giản. Giả sử cần ghi 100 000 dòng vào file:


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($"Stroka nomer {i}");
}

Console.WriteLine("Zapic zavershena!");

Chú thích:

  • Chúng ta mở file để ghi qua FileStream.
  • Sau đó bọc nó bằng BufferedStream, rồi mới tới StreamWriter (nó ghi chuỗi text vào stream).
  • Khi buffer đầy — dữ liệu được flush xuống đĩa một lần.

3. Buffer hoạt động "bên trong" thế nào

Hãy phân tích theo sơ đồ:


[Vash kod] → [StreamWriter] → [BufferedStream] → [FileStream] → [Fail na diske]

Khi bạn gọi WriteLine() trên StreamWriter, text đầu tiên vào buffer nội bộ, rồi qua BufferedStream — vào một buffer khác, và khi buffer đầy hoặc stream đóng, dữ liệu được flush xuống đĩa.

Có bao nhiêu byte trong một xô?

Kích thước buffer mặc định là 4096 byte (4 KB), nhưng bạn có thể chỉ định rõ:


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

Mẹo thực tế: Trên hệ thống hiện đại nên dùng buffer khoảng 8–64 KB. Với các thao tác file rất lớn — có thể dùng lớn hơn. Nhưng đừng lạm dụng: nếu bạn đang chạy trên microcontroller có 128 KB RAM, thì buffer 64 KB không phải ý hay :)

4. Thí nghiệm: so tốc độ có buffer và không

Để thấy tầm quan trọng, viết test so sánh ghi qua FileStream có buffer và không:


using System.Diagnostics;
using System.Text;

string data = new string('X', 1000); // 1 000 ký tự

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); // Trực tiếp xuống file – mỗi lần truy cập đĩa
    }
}

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);
    }
}

// Đo thời gian
Stopwatch sw = Stopwatch.StartNew();
WriteWithoutBuffer();
sw.Stop();
Console.WriteLine("Bez bufera: " + sw.ElapsedMilliseconds + " ms");

sw.Restart();
WriteWithBuffer();
sw.Stop();
Console.WriteLine("S buferom: " + sw.ElapsedMilliseconds + " ms");

Dự kiến đầu ra:
Trong hầu hết trường hợp có buffer sẽ thấy tăng tốc rõ! Đặc biệt với HDD. Trên SSD hiệu ứng vẫn có nhưng ít kịch tính hơn.

5. Chọn buffer nào? So sánh và thực tế

Trong .NET có nhiều lớp để buffer. Hãy làm rõ:

Lớp Dùng cho Buffer có sẵn? Có cần dùng BufferedStream?
FileStream
Làm việc với file Có (từ 4 KB) Hầu như không cần (nhưng có thể dùng)
NetworkStream
Làm việc với mạng Không Rất nên dùng
StreamReader/Writer
Đọc/ghi text Có (từ 1 KB) Thường không cần
GZipStream
Nén/giải nén Không Có thể/cần để tăng tốc

Quan trọng:
FileStream với tham số constructor bufferSize — thực chất đã là stream có buffer. Nếu bạn đã tự chỉ định buffer đủ lớn thì thêm BufferedStream nữa không mang lại lợi nhiều. Nhưng nếu bạn dùng stream khác (ví dụ network), thì BufferedStream là người bạn tốt.

6. Ví dụ: Sao chép file bằng BufferedStream


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

int bufferSize = 64 * 1024; // 64 KB

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);
}
// Đừng quên flush – nếu không những byte cuối có thể không xuống đĩa!
bufferedOutput.Flush();

Console.WriteLine("Kopirovanie zaversheno!");

Chú thích:

  • Đọc dữ liệu theo block lớn (64 KB) từ file qua BufferedStream.
  • Ghi vào file khác, cũng dùng buffer.
  • Sau khi vòng lặp kết thúc nhớ gọi Flush() để ghi phần cuối.

7. Những chi tiết hữu ích

Mẹo: khi nào thực sự cần BufferedStream

  • Nếu bạn làm việc với stream không có buffer (ví dụ network stream, hoặc các lớp kế thừa Stream tùy chỉnh);
  • Nếu bạn xử lý lượng lớn dữ liệu nhị phân (ví dụ sao chép file, chuyển đổi định dạng, backup);
  • Nếu bạn tối ưu code hiện có và thấy điểm nghẽn là số lượng thao tác nhỏ liên tục Write/Read.

Vài điều về async và buffer

Khi có các phép bất đồng bộ (ReadAsync/WriteAsync), buffering vẫn hữu ích, nhưng chú ý: nếu bạn dùng phương thức async trên buffer, xử lý vẫn diễn ra trong RAM, và tương tác vật lý với đĩa càng được giảm thiểu hơn.

Trong .NET 8+ và .NET 9 buffering được tích hợp sâu hơn, và nhiều lớp đã có buffer mặc định. Nhưng để tương thích với network stream hoặc implement của bạn, vẫn nên dùng BufferedStream khi cần.

Bạn sẽ học sâu về async ở level 58 :P

Sơ đồ trực quan hoạt động của buffered streams

flowchart LR
    A[Vash kod] --> B[StreamReader/Writer]
    B --> C[BufferedStream]
    C --> D[FileStream]
    D --> E[Fail/Ustroistvo]
  • A — Code của bạn gọi Write/Read.
  • B — Stream cấp cao (làm việc với text hoặc dữ liệu).
  • C — Buffer (gom nhóm dữ liệu để tăng tốc).
  • D — Implement cụ thể của stream (file, network).
  • E — Thiết bị vật lý (HDD, SSD, network, ...).

Mẹo & thủ thuật thực tế

  1. Nếu bạn ghi từng dòng vào file (ví dụ logging), tốt hơn là chỉ định buffer lớn hơn một dòng. Điều này giúp flush thành các mảng lớn nhanh hơn.
  2. Nếu mỗi thao tác phải được ghi ngay lập tức (ví dụ log quan trọng), gọi Flush() sau mỗi ghi. Nhưng điều này làm mất lợi ích của buffering!
  3. Nếu bạn tạo file tạm và xóa ngay sau khi tạo, có thể buffer còn sót không quan trọng — nhưng cẩn thận nếu cần chắc chắn file đã được ghi.
  4. Khi làm việc với file rất lớn (ví dụ vài chục GB) đừng ngại tăng buffer lên tới 1_048_576 byte (1 MB) hoặc hơn — miễn là RAM đủ.

8. Lỗi điển hình và lưu ý khi dùng

Nếu bạn thấy hứng và muốn "cắm buffer vào mọi chỗ" — chậm lại. Cái gì cũng phải vừa phải!

Một lỗi thường gặp là quên gọi Flush() hoặc đóng stream khi cần. Nếu stream chưa đóng mà chương trình crash, những byte cuối có thể còn nằm trong RAM và không được ghi xuống đĩa. Ví dụ khi ghi log và chương trình rơi, dòng cuối có thể biến mất.

BufferedStream không "biết" ranh giới logic của thông điệp — nó chỉ chờ đủ lượng dữ liệu theo kích thước buffer. Vì vậy với những thứ quan trọng (logging, backup...) nên thỉnh thoảng ép gọi Flush():

bufferedStream.Flush(); // Ép buffer flush xuống đĩa

Nếu bạn dùng StreamWriter, nó cũng có buffer riêng! Tức là khi xếp chồng sẽ có hai lớp buffer (và không phải lúc nào cũng tốt). Thường chỉ cần một lớp buffer, và nếu dùng StreamWriter thì có thể không cần thêm BufferedStream.

2
Nhiệm vụ
C# SELF, mức độ, bài học
Đã khóa
Sao chép nội dung file sử dụng BufferedStream
Sao chép nội dung file sử dụng BufferedStream
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION