CodeGym /Các khóa học /C# SELF /Lớp Task

Lớp TaskTask<TResult>

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

1. Giới thiệu

Trong thế giới lập trình bất đồng bộ chúng ta sống theo nguyên tắc "ủy nhiệm kèm phản hồi". Hãy tưởng tượng: bạn cần tải một file khổng lồ, phân tích gigabyte log hoặc gửi request tới một server xa. Thay vì đứng im chờ đợi như tượng đá, ta bảo hệ thống: "Làm việc này giúp tôi, tôi sẽ làm việc khác. Khi xong — nhất định báo cho tôi biết!"

Đây là lúc Task xuất hiện — hiện thân tinh tế của cách tiếp cận đó. Đây không chỉ là một abstraction kỹ thuật, mà là một "trung gian thông minh" chịu trách nhiệm thực hiện công việc và đảm bảo kết quả không bị mất trong khoảng không kỹ thuật số.

Task hoạt động như một trợ lý cá nhân mà bạn giao công việc quan trọng. Nó gật đầu, ghi việc vào sổ tay và nói: "Bạn cứ yên tâm làm việc khác, tôi sẽ tìm bạn khi mọi thứ xong". Và đúng là nó sẽ tìm — với kết quả trong tay hoặc một lời giải thích thành thật vì sao có lỗi.

Nếu tìm một ẩn dụ đời thường hơn, Task giống hệ thống đặt lịch trước hiện đại: bạn đăng ký khám bác sĩ online, nhận xác nhận, và bạn không cần chờ cả ngày ở phòng đợi. Hệ thống sẽ nhắc khi tới lượt, còn bạn thì tiếp tục cuộc sống.

Lớp Task

Task là khối xây dựng cơ bản của lập trình bất đồng bộ trong .NET. Nó đại diện cho một thao tác đang chạy hoặc sẽ chạy, kết quả của nó sẽ có trong tương lai. Nếu một method không cần trả về gì, ta dùng Task.

public async Task BackupToCloudAsync()
{
    // Làm phép màu backup, không trả về gì
}

Lớp Task<TResult>

Nếu cần trả về kết quả (ví dụ, string, số, object…), dùng Task<TResult>:

public async Task<string> DownloadHtmlAsync(string url)
{
    // Tải trang và trả về HTML
    return "<html>...</html>";
}

Tại sao dùng Task chứ không phải Thread?

Thread quản lý chính luồng (nặng và dễ sai), còn Task là abstraction mức cao hơn: nó có thể chạy trong thread pool, có thể hoạt động bất đồng bộ mà không cần tạo luồng mới (ví dụ cho I/O) và bạn không phải lo các chi tiết thấp.

Lớp Task cho phép mô tả: "Mình muốn chạy hành động này", còn .NET quyết định cách thực thi thế nào!

2. Cấu tạo đối tượng Task

Properties và methods của Task cần biết

Property / Method Mô tả
Status
Trạng thái hiện tại của task
Result
Kết quả cho Task<TResult> (khóa luồng)
IsCompleted
Task đã hoàn thành chưa
IsFaulted
Task có ném exception không
IsCanceled
Task có bị huỷ không
Wait()
Khóa luồng hiện tại tới khi hoàn thành (nguy hiểm)
ContinueWith()
Chạy thêm một task sau khi hoàn thành
Exception
Truy cập exception nếu Task kết thúc với lỗi
Id
ID duy nhất của task

Cách method bất đồng bộ với Task hoạt động

sequenceDiagram
    participant Main as Main Luồng
    participant Task as Task (Task nền)
    Main->>Task: Khởi chạy Task.Run(() => ...)
    Note right of Task: Thực thi ở nền
(CPU hoặc I/O) alt Task hoàn thành Task->>Main: await hoàn thành, tiếp tục else Lỗi Task->>Main: await ném exception end

3. Tạo và chạy task: cách Task hoạt động

Method async với async

Trường hợp thường gặp — bạn khai báo method là async và trả về Task hoặc Task<TResult> (như phía trên).

Task.Run: chạy trong thread pool

Nếu cần chạy công việc nặng ở nền (ví dụ tính toán lớn hoặc encode video), dùng Task.Run:

Task work = Task.Run(() =>
{
    // Tính toán nặng — không block main thread!
    Console.WriteLine("Tính toán nền bắt đầu...");
    Thread.Sleep(2000); // Giả lập công việc lâu
    Console.WriteLine("Tính toán nền xong!");
});

Nếu cần kết quả:

Task<int> calculateTask = Task.Run(() =>
{
    // Ví dụ tính tổng 100 số đầu
    int sum = 0;
    for (int i = 1; i <= 100; i++) sum += i;
    return sum;
});

Task.Factory.StartNew

Đây là cách cấp thấp hơn và linh hoạt hơn, cho phép cấu hình kỹ hơn khi khởi chạy task (ví dụ chỉ định scheduler, truyền tham số, v.v.). Trong code hiện đại thường khuyên dùng Task.Run vì đơn giản và an toàn hơn.

4. Ứng dụng trong ngày: danh bạ sách của chúng ta

Giả sử ta có ứng dụng danh bạ sách và cần thêm chức năng tải sách từ nguồn "đám mây" — đó là I/O-bound (HTTP chậm hoặc đọc file).

Thêm method tải sách bất đồng bộ (giả lập độ trễ):

public class Book
{
    public string Title { get; set; }
    public string Author { get; set; }
}

public class BookCatalog
{
    public List<Book> Books { get; set; } = new();

    public async Task LoadBooksAsync()
    {
        Console.WriteLine("Đang tải sách...");
        await Task.Delay(2000); // Giả lập tải lâu (ví dụ HTTP hoặc file)
        Books = new List<Book>
        {
            new Book { Title = "CLR via C#", Author = "Jeffrey Richter" },
            new Book { Title = "C# in Depth", Author = "Jon Skeet" }
        };
        Console.WriteLine("Tải sách thành công.");
    }
}

Trong Main gọi tải bất đồng bộ (dùng await):

var catalog = new BookCatalog();
await catalog.LoadBooksAsync();
Console.WriteLine($"Trong catalog có {catalog.Books.Count} sách.");

Bảng: Các cách chính để tạo và chạy Task

Phương thức tạo Cách dùng Kết quả Ứng dụng
async-method
async Task / async Task<T>
Thao tác bất đồng bộ Thường I/O, tiện lợi
Task.Run
Task.Run(() => { ... })
Task nền CPU-bound (tính toán)
TaskCompletionSource<T>
Tạo và hoàn thành Task thủ công Toàn quyền điều khiển bởi dev Hiếm, cho thứ cấp thấp

5. Vòng đời của Task

Task có thể ở nhiều trạng thái khác nhau:

  • Created — task được tạo nhưng chưa chạy (với Task cần khởi chạy rõ ràng).
  • WaitingToRun — chờ vào hàng đợi trong pool.
  • Running — đang thực thi.
  • WaitingForActivation — chờ kích hoạt hoặc khởi chạy ngoài.
  • RanToCompletion — hoàn thành thành công.
  • Faulted — kết thúc với lỗi (exception).
  • Canceled — bị huỷ (nếu hỗ trợ huỷ).

Sơ đồ

flowchart LR
    Start -->|Khởi chạy task| Running
    Running -->|Thành công| Completed
    Running -->|Lỗi| Faulted
    Running -->|Huỷ| Canceled

Thử nghiệm thực tế

Task task = Task.Run(() =>
{
    Thread.Sleep(1000);
});
Console.WriteLine(task.Status); // Thường: Running hoặc WaitingToRun
await task;
Console.WriteLine(task.Status); // RanToCompletion sau khi xong

6. Làm sao lấy kết quả từ Task<TResult>?

Task<TResult> là wrapper cho kết quả sẽ xuất hiện trong tương lai. Khi cần chờ kết quả, dùng await:

Task<int> sumTask = Task.Run(() =>
{
    int sum = 0;
    for (int i = 1; i <= 5; i++) sum += i;
    return sum;
});

int result = await sumTask;
Console.WriteLine(result); // 15

Nếu quên viết await, bạn sẽ nhận được một Task (một lời hứa), chứ không phải kết quả. Đây là một "bẫy bất đồng bộ" phổ biến.

Thay thế: Lấy kết quả đồng bộ (KHÔNG NÊN DÙNG TRONG UI!)

Đôi khi (ví dụ trong tests) cần lấy kết quả không await. Có thể dùng property .Result:

int result = sumTask.Result;

Nhưng nếu Task chưa hoàn thành, đoạn code này sẽ khóa luồng, và nếu đó là UI thread thì app sẽ đứng! Vậy nên: luôn ưu tiên dùng await.

Lỗi thường gặp với TaskTask<TResult>

Quên trả về Task, method trở thành void. Nếu method không trả về giá trị — trả về Task, không dùng void, nếu không không bắt được lỗi.

Bỏ qua await. Chỉ gọi method mà không chờ, task chạy "fire and forget". Bạn sẽ không biết khi nào nó xong hoặc lỗi.

Chờ đồng bộ bằng .Result hoặc .Wait(). Dễ deadlock, đặc biệt trên UI hoặc ASP.NET. Dùng await thôi.

7. Tính năng nâng cao của Task

Chuỗi task: ContinueWith

Có thể "gắn" hành động chạy sau khi task hoàn thành bằng ContinueWith:

Task.Run(() => 10)
    .ContinueWith(t =>
    {
        Console.WriteLine($"Hoàn thành! Kết quả: {t.Result}");
    });

Nhưng trong C# hiện đại thường dùng async/await — đọc dễ hơn.

Ví dụ: Tải dữ liệu song song và tuần tự

Giả sử cần tải hai cuốn sách từ hai nguồn khác nhau. Bạn có thể khởi chạy cả hai Task song song và chờ cả hai:

public async Task LoadBooksFromMultipleSourcesAsync()
{
    Task<List<Book>> t1 = LoadFromCloudAsync();
    Task<List<Book>> t2 = LoadFromLocalAsync();

    // Chờ cả hai song song
    await Task.WhenAll(t1, t2);

    // Kết hợp kết quả
    Books = t1.Result.Concat(t2.Result).ToList();
}

private async Task<List<Book>> LoadFromCloudAsync()
{
    await Task.Delay(2000); // "Đám mây"
    return new List<Book> { new Book { Title = "Cloud Book", Author = "Cloud Author" } };
}

private async Task<List<Book>> LoadFromLocalAsync()
{
    await Task.Delay(1000); // "Đĩa local"
    return new List<Book> { new Book { Title = "Local Book", Author = "Local Author" } };
}

Lưu ý: với await Task.WhenAll(...) cả hai request bắt đầu cùng lúc và chạy song song (nếu có thể), và app chờ cho cả hai xong.

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

Task và Fire-and-forget

Đôi khi muốn chạy task mà không chờ nó xong (ví dụ gửi log lên cloud hoặc "rán bánh" còn người dùng làm việc):

async void LogToCloudAsync(string message)
{
    await Task.Run(() =>
    {
        // Gửi log lâu
        Thread.Sleep(1000);
        Console.WriteLine($"Log đã gửi: {message}");
    });
}

Nhưng nhớ: nếu task kiểu này ném lỗi — rất khó biết. Nếu có thể, hãy trả về Task và ít nhất catch/ghi log exception bên trong!

TaskTask<TResult> trong thực tế

  • Trong ứng dụng client UWP/WPF/WinForms đừng block UI — dùng Task cho thao tác dài (file, network).
  • Trong WebAPI/ASP.NET Task giúp không lãng phí thread để chờ network/DB, tăng throughput.
  • Tổ chức thực thi "song song": cùng lúc tải, xử lý và lưu.
  • Hầu hết method chậm đều có bản Async: File.ReadAllTextAsync, HttpClient.GetStringAsync v.v.

FAQ và những điểm bất ngờ

Câu hỏi: Tại sao Task đôi khi chạy đồng bộ?
Trả lời: Nếu thao tác đã hoàn tất (ví dụ kết quả được cache), compiler hoặc scheduler có thể hoàn thành method ngay trên cùng luồng đồng bộ. Điều này bình thường và tăng tốc các lần gọi lặp lại.

Câu hỏi: Tại sao không nên dùng async void?
Trả lời: Method kiểu này không thể "đợi", không bắt lỗi được và không theo dõi được khi hoàn thành. Dùng Task, còn async void chỉ dành cho EventHandler (ví dụ Button_Click).

Câu hỏi: Có thể khởi chạy nhiều task và chỉ chờ một trong số chúng không?
Trả lời: Có — dùng Task.WhenAny.

2
Nhiệm vụ
C# SELF, mức độ, bài học
Đã khóa
Sử dụng Task để trả về kết quả
Sử dụng Task để trả về kết quả
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION