CodeGym /Các khóa học /C# SELF /Làm việc bất đồng bộ với các tập tin văn bản

Làm việc bất đồng bộ với các tập tin văn bản

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

1. Giới thiệu

Hãy nhớ ví dụ về tải một ảnh lớn. Trong lúc nó đang tải, app "treo". Cùng chuyện đó xảy ra với tập tin văn bản, nhất là khi chúng lớn (log chục GB, CSV khổng lồ, backup DB ở dạng text).

Giả sử bạn viết một app mà:

Phân tích một file log khổng lồ để tìm lỗi. Nếu đọc nó một cách đồng bộ, UI của bạn sẽ "đứng im" vài giây hoặc thậm chí vài phút cho tới khi xong. Người dùng sẽ nghĩ chương trình bị hỏng.

Ghi dữ liệu vào file báo cáo khi dữ liệu được tạo. Nếu ghi block luồng chính thì cả việc tạo dữ liệu lẫn tương tác với UI đều sẽ bị ảnh hưởng.

Web server phải phục vụ hàng nghìn request. Mỗi request có thể cần đọc hoặc ghi tập tin. Nếu mỗi IO file là đồng bộ, các thread server sẽ chờ đĩa và server nhanh chóng "nghẹt" vì quá tải.

Trong các kịch bản như vậy IO bất đồng bộ không chỉ là "tính năng hay ho" mà là điều cần thiết. Nó cho phép app của bạn không bị đứng khi đĩa "suy nghĩ", mà làm việc hữu ích (ví dụ update UI, xử lý request khác hoặc tính toán).

Khái niệm cơ bản: async/await và tasks

  • Từ khoá async báo rằng method có thể chứa "điểm chờ" (await).
  • Toán tử await tạm thời trả quyền điều khiển trong khi task bất đồng bộ chưa hoàn thành (ví dụ đọc file).
  • Method bất đồng bộ thực hiện IO mà không block luồng hiện tại: khi chưa có dữ liệu — luồng được giải phóng.

Tất cả những điều này là nền tảng cho "phép thuật" bất đồng bộ khi làm việc với file.

2. Các method bất đồng bộ cho file

Trong các phiên bản .NET hiện đại hầu hết các lớp làm việc với file đều có bản bất đồng bộ. Với file văn bản thường dùng:

  • StreamReader.ReadLineAsync()
  • StreamReader.ReadToEndAsync()
  • StreamWriter.WriteLineAsync()
  • StreamWriter.WriteAsync()
  • Còn có các phương thức static: File.ReadAllTextAsync(), File.WriteAllTextAsync() v.v.
Đọc Ghi
ReadLineAsync()
WriteLineAsync()
ReadToEndAsync()
WriteAsync()
File.ReadAllXAsync()
File.WriteAllXAsync()

3. Đọc toàn bộ file văn bản bất đồng bộ

Ta hãy đọc toàn bộ file vào một chuỗi. Cách này hợp với file nhỏ: config, log nhỏ.

using System;
using System.IO;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        string path = "input.txt";
        
        // Asynchronously read the whole file
        string fileContents = await File.ReadAllTextAsync(path);
        
        Console.WriteLine("Nội dung file:");
        Console.WriteLine(fileContents);
    }
}

Chú ý: method Main giờ được gắn async Task Main(). Có thể làm thế từ C# 7.1. Một await — là chạy bất đồng bộ!

4. Đọc file lớn theo dòng bất đồng bộ

Khi file thực sự lớn, nạp toàn bộ vào RAM không phải ý hay. Tốt hơn là đọc theo dòng:

using System;
using System.IO;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        string path = "biglog.txt";

        // Mở StreamReader để đọc bất đồng bộ
        using StreamReader reader = new StreamReader(path);

        string? line;
        while ((line = await reader.ReadLineAsync()) != null)
        {
            // Xử lý dòng ở đây (ví dụ: tìm lỗi)
            Console.WriteLine(line);
        }
    }
}

Hoạt động thế nào?

Mỗi lần gọi await reader.ReadLineAsync() giải phóng luồng — rất hữu ích nếu file ở network drive hoặc cloud. Xử lý bất đồng bộ quan trọng khi có hàng chục nghìn dòng và có người dùng song song (ví dụ API server).

5. Ghi các dòng vào file bất đồng bộ

Tương tự, bạn có thể ghi dữ liệu vào file bất đồng bộ (ví dụ khi tạo báo cáo):

using System;
using System.IO;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        string path = "output.txt";

        using StreamWriter writer = new StreamWriter(path);

        for (int i = 0; i < 5; i++)
        {
            await writer.WriteLineAsync($"Dòng số {i + 1}");
        }
        
        // Có thể gọi FlushAsync để đảm bảo dữ liệu được ghi
        await writer.FlushAsync();

        Console.WriteLine("Dữ liệu đã được ghi bất đồng bộ!");
    }
}

Gọi FlushAsync() không phải lúc nào cũng bắt buộc — khi đóng StreamWriter buffer sẽ được flush. Nhưng nếu cần đảm bảo "ngay bây giờ", thì dùng.

6. Tương tác giữa nhiều thao tác file bất đồng bộ

Giả sử cần đọc một file văn bản và đồng thời ghi bản đã biến đổi sang file khác:

using System;
using System.IO;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        string sourcePath = "even_biggerlog.txt";
        string destinationPath = "copy_biggerlog.txt";

        using StreamReader reader = new StreamReader(sourcePath);
        using StreamWriter writer = new StreamWriter(destinationPath);

        string? line;
        int linesProcessed = 0;

        while ((line = await reader.ReadLineAsync()) != null)
        {
            // Một chút "ma thuật": biến tất cả ký tự thành chữ hoa
            string processed = line.ToUpperInvariant();

            await writer.WriteLineAsync(processed);
            linesProcessed++;
        }

        Console.WriteLine($"Đã xử lý dòng: {linesProcessed}");
    }
}

Ở đây cả đọc và ghi đều chạy bất đồng bộ. Mỗi await giải phóng quyền điều khiển, cho phép app làm việc khác.

7. Ứng dụng thực tế: dùng ở đâu?

  • Web development (ASP.NET Core): upload/download file không block xử lý request khác; server vẫn responsive.
  • Desktop apps (WPF, WinForms): khi mở log hoặc lưu báo cáo UI không "treo".
  • Game engines: load tài nguyên bất đồng bộ (texture, model) để không làm gián đoạn animation và gameplay.
  • Xử lý dữ liệu lớn: parse CSV/JSON/XML khổng lồ theo dòng, xử lý "on the fly" mà không tiêu tốn bộ nhớ thừa.
  • Background services và daemons: logging, caching, xử lý queue với dùng thread và disk hiệu quả.

Kết luận: bất đồng bộ giúp tạo app hiện đại, phản hồi tốt và dễ scale. "Blocking" — xấu, bất đồng bộ — tốt!

8. Lưu ý và best practices

Đừng quên await! Nếu gọi method có hậu tố Async mà không await thì bạn sẽ nhận một Task, nhưng code sẽ tiếp tục chạy, dẫn tới lỗi về thứ tự thực thi.

// XẤU: quên await
FileManager.ReadTextFileAsync("nonexistent.txt"); // sẽ khởi chạy, nhưng Main tiếp tục
Console.WriteLine("Tôi chạy ngay, mặc dù file còn đang đọc (hoặc đã ném lỗi)! Điều này tệ!");

Compiler thường cảnh báo khi quên await, nhưng không ngăn build.

using cho mọi thứ implement IDisposable: mọi stream (FileStream, StreamReader, StreamWriter) cần được giải phóng đúng. Dùng khối using hoặc declaration using (C# 8+) để đảm bảo đóng và flush buffer.

Kích thước buffer (bufferSize): StreamReader/StreamWriter đã được tối ưu sẵn, nhưng khi có yêu cầu đặc biệt bạn có thể thử nghiệm. Mặc định là giá trị hợp lý (trong FileStream thường dùng 4096 bytes).

Xử lý lỗi: method bất đồng bộ cũng throw exception. Bao các thao tác trong try-catch. Exception sẽ "nổi lên" khi bạn await trên Task tương ứng.

ConfigureAwait: trong thư viện và kịch bản web, khi không cần synchronization context (GUI), dùng await SomeAsync().ConfigureAwait(false). Điều này giảm overhead khi chuyển context. Trong console và app UI thường có thể bỏ qua.

Thực hành nhiều — và sớm thôi async Task sẽ trở nên quen như Console.WriteLine.

9. Lỗi thường gặp và những điểm cần chú ý khi làm việc file bất đồng bộ

Nếu không dùng await (mà chỉ gọi method có hậu tố Async), bạn sẽ nhận một object Task, nhưng kết quả sẽ không được chờ đợi tự động. Bạn phải chờ nó bằng await hoặc chờ thủ công (thường không mong muốn).

Không thể chờ method bất đồng bộ từ code đồng bộ mà không "nâng" async lên stack. Dùng .Result hoặc .GetAwaiter().GetResult() có thể gây deadlock — tốt hơn là chuyển các caller thành async.

Đừng đọc và viết cùng một file đồng thời (dù là bất đồng bộ). Điều này có thể gây race và hỏng dữ liệu.

Bất đồng bộ giải phóng luồng gọi nhưng không làm cho thao tác nhanh hơn: nếu đĩa hay network chậm, chạy bất đồng bộ vẫn chậm — chỉ khác là không block UI hay worker thread.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION