CodeGym /Các khóa học /C# SELF /Lợi ích của làm việc bất đồng bộ với file (

Lợi ích của làm việc bất đồng bộ với file ( async/ await)

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

1. Giới thiệu

Hãy tưởng tượng ứng dụng của bạn là một quán cà phê. Trong quán chỉ có một nhân viên phục vụ (đó là dòng chính của chương trình). Khi khách (người dùng) gọi cà phê (một hành động), phục vụ đi vào bếp, làm cà phê rồi sau đó mới quay lại bàn để nhận order tiếp theo.

Bây giờ tưởng tượng có người gọi... một nồi borsch. Và không chỉ nồi nhỏ, mà là một cái nồi khổng lồ phải nấu trong hai tiếng! Nhân viên phục vụ sẽ làm gì? Anh ta sẽ đứng canh nồi trong hai tiếng đó, không làm gì ngoài việc chờ borsch chín. Các khách khác sẽ ngồi, vẫy tay, giận dữ, và anh ta sẽ không nhìn thấy họ. Quán "treo" vì phục vụ bị block.

Trong lập trình, điều này gọi là thao tác blocking. Khi bạn gọi phương thức thông thường để đọc hoặc ghi file (ví dụ FileStream.Read() hoặc StreamReader.ReadLine()), luồng hiện tại của bạn bị khóa. Nó ngừng thực thi phần còn lại của code cho đến khi I/O hoàn tất.

Hãy xem ví dụ đơn giản:


// Tạo file "lớn" để minh họa
string largeFilePath = "LargeOrder.txt";
using (StreamWriter sw = new StreamWriter(largeFilePath))
{
    for (int i = 0; i < 1000000; i++) // 1 triệu dòng
        sw.WriteLine($"Dòng {i}: Thông tin rất quan trọng...");
} // File đóng ở đây để có thể đọc được

// !!! CHÚ Ý: Đây là thao tác blocking !!!
string content = File.ReadAllText(largeFilePath);

Chạy đoạn code này. Bạn sẽ thấy trong lúc ghi và đọc file chương trình "đứng im". Console không nhận input, không in thêm thông báo nào cho đến khi file được đọc xong. Chỉ sau đó các dòng tiếp theo mới được thực thi.

Điều này có thể không quá rõ trong các chương trình console nhỏ, nhưng hãy tưởng tượng:

  • Ứng dụng có giao diện đồ họa (UI): Bạn nhấn nút "Tải file", và cửa sổ chương trình "đơ". Không thể di chuyển cửa sổ, không thể nhấn nút khác, menu không phản hồi. Trải nghiệm người dùng rất tệ.
  • Web-server: Server xử lý request của người dùng. Nếu một request cần đọc file rất lớn, thì tất cả các request khác sẽ chờ trong hàng đợi cho đến khi thread đó được giải phóng. Dẫn tới độ trễ lớn và khả năng mở rộng kém.

Đó là lý do chúng ta cần bất đồng bộ!

2. Lợi ích của làm việc bất đồng bộ với file

Bất đồng bộ không làm cho ổ đĩa nhanh hơn. Ổ đĩa vẫn chạy ở cùng tốc độ. Bất đồng bộ là để không phải chờ thao tác chậm kết thúc, mà giải phóng luồng để làm việc khác.

Quay lại quán cà phê. Giờ phục vụ thông minh hơn và đa nhiệm. Khi khách order "NỒI BORSCH RẤT TO" (đọc file lớn), phục vụ không đứng canh nồi. Anh ta đặt nồi vào nấu, rồi quay lại sảnh tiếp nhận order khác, dọn dẹp chén bát, phục vụ khách khác. Khi borsch xong, bếp gọi, anh ta quay lại lấy và mang cho khách.

Khác biệt chính: phục vụ không bị khóa chờ borsch. Anh ta tận dụng thời gian đó để làm việc khác.

Phản hồi của ứng dụng (User Interface Responsiveness)

Đây có lẽ là lợi ích rõ nhất và quan trọng nhất với đa số ứng dụng desktop và di động. Nếu chương trình của bạn làm gì đó lâu (đọc file, tải dữ liệu từ internet, xử lý dữ liệu lớn), cách tiếp cận bất đồng bộ cho phép:

  • Giữ UI tương tác: Người dùng có thể tiếp tục nhấn nút, di chuyển cửa sổ, xem thông tin khác trong khi thao tác nặng chạy ở background.
  • Hiển thị chỉ báo tiến độ: Bạn có thể hiển thị animation loading hoặc progress bar, cho người dùng biết app không bị treo mà đang làm việc.

Hãy tưởng tượng thay vì console "đứng im" khi phục vụ đọc file, bạn thấy anh ta vẫn nhận order khác (ví dụ hiển thị thông báo khác hoặc phản ứng với input người dùng), còn việc đọc file diễn ra "ở nền". Tốt hơn nhiều!

Sử dụng tài nguyên hiệu quả và khả năng mở rộng

Điều này cực kỳ quan trọng cho ứng dụng server (ví dụ web services, API, backend) phải phục vụ nhiều người dùng cùng lúc.

  • Không lãng phí thread: Trong mô hình đồng bộ, mỗi request "dài" sẽ khóa một thread trên server. Nếu bạn có 1000 request như vậy, bạn cần 1000 thread. Mỗi thread tốn bộ nhớ và CPU. Hệ điều hành tốn thời gian switch giữa các thread. Bất đồng bộ cho phép cùng một thread, trong khi chờ I/O, bắt đầu xử lý request khác. Khi I/O xong, nó quay lại request ban đầu.
  • Giải phóng CPU: Trong khi dữ liệu được đọc từ đĩa hoặc truyền mạng, CPU nhàn rỗi. Bất đồng bộ cho phép CPU làm việc hữu ích với các tác vụ khác trong thời gian đó.
  • Ít tốn bộ nhớ hơn: Ít thread hoạt động đồng nghĩa với việc sử dụng RAM ít hơn cho server.

3. Đơn giản hóa code (trong C# với async/await)

Trước đây viết code bất đồng bộ khó, dài dòng và dễ sai. Cần quản lý thread thủ công, callbacks và đồng bộ hoá. Nó giống như cố gắng xây một trạm vũ trụ từ LEGO trong bóng tối.

Nhưng C# có các từ khoá asyncawait. Giống như cây đũa thần, cho phép viết code bất đồng bộ gần như dễ đọc như code đồng bộ. Bạn chỉ cần nói với compiler: "Ở đây có thể lâu, chờ, nhưng đừng block cả thế giới".


// Đây là VÍ DỤ, trông sẽ như thế này cho code bất đồng bộ (tạm chưa giải thích sâu)
// Trong các bài sau chúng ta sẽ đi chi tiết!

public static async Task Main(string[] args) // Đây là cách Main bất đồng bộ
{
    Console.WriteLine("Chương trình bắt đầu đọc FILE RẤT LỚN một cách bất đồng bộ...");

    Stopwatch stopwatch = Stopwatch.StartNew();
    
    // !!! CHÚ Ý: Thao tác bất đồng bộ !!!
    await File.ReadAllTextAsync(largeFilePath); // Điều này không block thread
    
    stopwatch.Stop();

    Console.WriteLine($"File đã được đọc! Thời gian tiêu tốn: {stopwatch.ElapsedMilliseconds} ms.");
    // Ở đây có thể làm việc khác trong lúc file được đọc!
}

Lưu ý: bất đồng bộ không làm cho I/O nhanh hơn ở mức ổ đĩa. Nếu đọc file 1 GB mất 15 giây, nó vẫn mất 15 giây. Nhưng điểm khác là CPU và các thread của bạn làm gì trong 15 giây đó. Ở mô hình đồng bộ chúng nhàn rỗi, còn ở bất đồng bộ — chúng làm việc với nhiệm vụ khác.

Hãy tóm tắt bằng một bảng nhỏ:

Đặc tính Thao tác đồng bộ (thường Read/Write) Thao tác bất đồng bộ (ReadAsync/WriteAsync)
Luồng thực thi Bị khóa cho tới khi thao tác kết thúc Không bị khóa, được giải phóng cho công việc khác
Phản hồi UI Ứng dụng "đơ" Ứng dụng vẫn tương tác được
Sử dụng CPU Nhàn rỗi trong lúc chờ I/O Có thể làm việc khác trong khi chờ I/O
Khả năng mở rộng Thấp (cần nhiều thread cho nhiều thao tác đồng thời) Cao (xử lý nhiều request bằng ít thread hơn)
Độ phức tạp khi viết Đơn giản Trước kia phức tạp, nhưng async/await làm dễ hơn nhiều
Tốc độ bản thân I/O Không nhanh hơn Không nhanh hơn (tốc độ phụ thuộc ổ đĩa)
Khi nào dùng? Cho các thao tác nhanh, ngắn Cho mọi thao tác có khả năng lâu (I/O, mạng, DB)

Về bản chất, bất đồng bộ là cách làm cho ứng dụng của bạn phản hồi tốt hơn và có khả năng mở rộng hơn, đặc biệt khi tương tác với tài nguyên chậm bên ngoài như filesystem hay mạng. Nó không thay thế buffering mà bổ sung cho nó. Buffering làm nhanh quá trình chuyển dữ liệu, còn bất đồng bộ đảm bảo chương trình bạn không đứng im trong khi dữ liệu được chuyển.

4. Đằng sau hậu trường

Ví dụ thực tế: video editors, games, websites

Hầu hết các chương trình hiện đại làm việc với file lớn dùng cách tiếp cận bất đồng bộ. Các media player không block giao diện khi load phim. Server không "đơ" khi một client tải file lớn. Ngay cả chương trình backup hay client cloud đều làm việc "ở nền" để user có thể tiếp tục dùng máy.

Cách nó hoạt động (ngôn ngữ đơn giản)

Các phương thức file bất đồng bộ trong .NET (ví dụ ReadAsync, WriteAsync) thực tế tận dụng khả năng của hệ điều hành để không block thread khi thao tác dài. Điều này có được nhờ các system call báo cho OS: "Đọc file này cho tớ, và khi xong — báo lại".

Yếu tố trực quan: cách đọc bất đồng bộ hoạt động (sơ đồ)

sequenceDiagram
    participant UserCode as Mã của bạn
    participant OS as Hệ điều hành
    participant Disk as Ổ đĩa

    UserCode->>OS: Gửi yêu cầu đọc file bất đồng bộ
    OS->>Disk: Đọc dữ liệu
    UserCode->>UserCode: Tiếp tục thực hiện công việc khác
    OS->>OS: Chờ hoàn thành đọc
    Disk-->>OS: Dữ liệu sẵn sàng
    OS-->>UserCode: Thông báo hoàn thành đọc
    UserCode->>UserCode: Xử lý dữ liệu

5. Nơi mà thao tác bất đồng bộ đem lại lợi ích lớn nhất

  • Ứng dụng có giao diện đồ họa (UI không bị block).
  • Server xử lý nhiều request đồng thời liên quan đến file.
  • Script xử lý khối lượng dữ liệu lớn tự động (ví dụ backup).
  • Công cụ làm việc với ổ chậm hoặc ổ mạng.

Dữ liệu càng lớn và thiết bị càng chậm — lợi ích của bất đồng bộ càng rõ. Dù bạn không làm app lớn, nên quen với các phương thức Async: chúng là tiêu chuẩn trong C# hiện đại.

Giờ bạn đã biết vì sao làm việc bất đồng bộ với file không chỉ là trend mà là kỹ thuật quan trọng để tạo ứng dụng nhanh và phản hồi tốt trên .NET 9. Trong các bài sau chúng ta sẽ đi sâu vào syntax, thực hành và các kịch bản điển hình dùng ReadAsync, WriteAsync và các phương thức liên quan!

6. Bất đồng bộ

Chúng ta sẽ tìm hiểu cách chính xác hoạt động của bất đồng bộ trong các level 55-62. Bây giờ mình chỉ muốn giới thiệu sơ. Nếu bạn không hiểu gì ở level này — không sao cả. Bỏ qua các bài và bài tập ở level này, rồi quay lại sau khi đã học về bất đồng bộ.

Song song (Parallelism) so với bất đồng bộ (Asynchrony)

Nếu bạn chạy vài ứng dụng Windows trên máy và chúng cùng làm việc, lập trình viên sẽ nói rằng nhiệm vụ được thực thi song song.

Còn nếu bạn thu nhỏ game trên điện thoại và chuyển sang app khác, trong khi app bị thu nhỏ tạm dừng, thì đó giống hơn với công việc bất đồng bộ. Bất đồng bộ không phải là chạy đồng thời, mà là về đồng thời chờ đợi.

Ví dụ

Giả sử bạn làm việc nhà. Bạn bật máy rửa bát, và trong khi nó chạy bạn xếp đồ vào máy giặt. Khi máy giặt đang giặt, bạn đặt bánh vào lò. Bạn làm một mình nhưng thực hiện nhiều việc cùng lúc, vì bạn chuyển sang việc khác thay vì chờ mỗi việc xong.

Khi máy giặt báo xong, nó gọi bạn và bạn quay lại xử lý đồ giặt. Từ góc nhìn của máy giặt, bạn có vẻ như đang chờ nó hoàn thành. Nhưng thực tế bạn vừa làm việc khác trong thời gian chờ.

Bạn không thể cùng lúc tải máy rửa bát, đặt bánh và lấy đồ khỏi máy giặt. Nhưng khi thứ gì đó đang chạy, bạn không phải đợi vô ích mà có thể dùng thời gian đó hiệu quả.

Lưu ý quan trọng

Nếu bạn chỉ có một tác vụ, bạn sẽ không thấy khác biệt giữa "chờ tác vụ xong" và "có thể làm việc khác trong khi chờ". Nhưng nếu có nhiều tác vụ, sự khác biệt sẽ rõ rệt.

7. Lỗi thường gặp và cạm bẫy

Hiểu lầm phổ biến: chỉ cần gọi phương thức bất đồng bộ là mọi thứ sẽ tự động nhanh hơn. Thực tế, nếu dùng sai (ví dụ quên await), code sẽ trở nên "không đoán trước được": kết quả chưa sẵn sàng mà chương trình đã dùng nó. Trong ứng dụng GUI, xử lý sự kiện hầu như luôn nên là async, nếu không UI sẽ "đơ".

Một điều nữa: bất đồng bộ không làm tăng tốc đọc/ghi, mà cho phép chương trình hiệu quả và phản hồi tốt trong khi thao tác chậm diễn ra.

Mở tiếp?

Bây giờ khi bạn đã hiểu tại sao cần bất đồng bộ, đã đến lúc học cách dùng nó. Ở bài tiếp theo chúng ta sẽ đi sâu vào cú pháp đọc và ghi file bất đồng bộ, làm quen với các phiên bản bất đồng bộ của phương thức (ví dụ ReadAsyncWriteAsync) và bắt đầu viết code bất đồng bộ thực sự! Sẽ rất thú vị, đừng chuyển kênh!

2
Nhiệm vụ
C# SELF, mức độ, bài học
Đã khóa
Đọc file một cách bất đồng bộ
Đọc file một cách bất đồng bộ
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION