CodeGym /Các khóa học /C# SELF /Quản lý nhiều tác vụ

Quản lý nhiều tác vụ

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

1. Task.WhenAll: đợi tất cả cùng một lúc

Trong đời thực hiếm khi bạn chỉ làm một việc. Bạn bật dậy khỏi giường và trong khi đó nhìn đồng hồ, tắm và hát, mặc quần áo và nghe nhạc, vừa lúc đó pha cà phê. Hoặc khi làm việc trên máy tính, bạn tải cùng lúc vài tệp, gửi loạt request mạng, tính toán nhiều dữ liệu theo các hướng khác nhau. Tất cả những hành động này có thể (và nên) chạy song song để không lãng phí thời gian của người dùng. Chúng ta không muốn đợi từng task chạy xong lần lượt! Cách tổ chức "dàn nhạc" đó ra sao? Làm sao biết khi nào MỌI THỨ đã hoàn thành? Hoặc ngược lại, biết ai là người xong trước?

Task.WhenAll và Task.WhenAny — là công cụ cho các kịch bản này.

Phương thức WhenAll

Task.WhenAll là một phương thức tĩnh của lớp Task, nhận một tập hợp các task và trả về một task mới chỉ hoàn thành khi tất cả các task đã truyền vào đều hoàn thành. Giống như khi bạn đợi ở kỳ thi cho đến khi tất cả các bạn cùng khóa nộp bài xong trước khi rời phòng thi.

Chữ ký phương thức

Task Task.WhenAll(params Task[] tasks)
Task<TResult[]> Task.WhenAll<TResult>(params Task<TResult>[] tasks)

Có phiên bản cho các task trả về kết quả (Task<TResult>) và cho các task "rỗng" (Task).

Ví dụ đơn giản: đợi tải tất cả file

Ta thử tải ba file bất đồng bộ. Để đơn giản — giả lập tải bằng độ trễ (Task.Delay). Ví dụ thực tế, code chạy được.

using System;
using System.Threading.Tasks;

class Program
{
    // Simulate tải file với độ trễ, trả về tên
    static async Task<string> DownloadFileAsync(string fileName)
    {
        Console.WriteLine($"► Bắt đầu tải: {fileName}");
        await Task.Delay(2000); // Đợi 2 giây
        Console.WriteLine($"✓ Tải xong: {fileName}");
        return fileName;
    }

    static async Task Main()
    {
        var files = new[] { "fileA.txt", "fileB.txt", "fileC.txt" };

        // Khởi chạy tất cả các tải cùng lúc
        var downloadTasks = new Task<string>[files.Length];
        for (int i = 0; i < files.Length; i++)
        {
            downloadTasks[i] = DownloadFileAsync(files[i]);
        }

        // Đợi tất cả các tải hoàn thành
        string[] results = await Task.WhenAll(downloadTasks);

        Console.WriteLine($"Tất cả tải đã xong! Danh sách: {string.Join(", ", results)}");
    }
}

Chuyện gì đang xảy ra ở đây?

  • Chúng ta không đợi từng task theo thứ tự. Cả ba đều được khởi chạy song song (bất đồng bộ).
  • Task.WhenAll(downloadTasks) trả về một task sẽ hoàn thành chỉ khi tất cả các task hoàn thành.
  • Sau đó ta có thể làm việc với kết quả của tất cả các task — dùng chúng, in ra, gửi tiếp.

Tượng hình

Giống như khi bạn giao cho ba shipper giao ba kiện hàng và chỉ báo cáo cho sếp khi tất cả đều tới tay khách hàng thành công.

Sơ đồ hoạt động của Task.WhenAll

sequenceDiagram
    participant Program
    participant Task1
    participant Task2
    participant Task3

    Program->>Task1: Khởi chạy
    Program->>Task2: Khởi chạy
    Program->>Task3: Khởi chạy
    Task1-->>Program: Hoàn thành (có thể trước các task khác)
    Task2-->>Program: Hoàn thành
    Task3-->>Program: Hoàn thành
    Program->>Program: Task.WhenAll hoàn thành, mọi kết quả có sẵn

Nếu một trong các task ném ngoại lệ thì sao?

Task.WhenAll không dừng ngay khi có một task lỗi. Nó đợi tới khi mọi task kết thúc. Nếu ít nhất một task ném ngoại lệ — task kết quả sẽ ở trạng thái Faulted, và chứa tập hợp tất cả các ngoại lệ xảy ra.

Ví dụ

static async Task<string> MayThrowAsync(string fileName)
{
    await Task.Delay(500);
    if (fileName == "fileB.txt")
        throw new Exception("Lỗi tải fileB.txt");
    return fileName;
}

static async Task Main()
{
    var files = new[] { "fileA.txt", "fileB.txt", "fileC.txt" };
    var downloadTasks = files.Select(MayThrowAsync).ToArray();
    try
    {
        string[] results = await Task.WhenAll(downloadTasks);
        Console.WriteLine($"Mọi thứ ổn: {string.Join(", ", results)}");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Ít nhất một task kết thúc với lỗi: {ex.Message}");
    }
}

Trong trường hợp ngoại lệ, để xem tất cả lỗi nội bộ, dùng AggregateException.InnerExceptions.
Tài liệu chính thức: Task.WhenAll docs

2. Task.WhenAny: đợi task hoàn thành đầu tiên

Task.WhenAny cũng là phương thức tĩnh, nhưng nó hoàn thành ngay khi bất kỳ một trong các task truyền vào chuyển sang trạng thái "Hoàn thành" (bất kể là lỗi hay thành công). Nó trả về tham chiếu tới task hoàn thành đầu tiên.

Chữ ký

Task<Task> Task.WhenAny(params Task[] tasks)
Task<Task<TResult>> Task.WhenAny<TResult>(params Task<TResult>[] tasks)

Tượng hình

Ai nướng bánh đầu tiên — người đó thắng, có thể yêu cầu những người còn lại dừng lại. Đôi khi chỉ cần biết server nào trả lời nhanh nhất và dùng ngay kết quả đó.

Ví dụ: ai nhanh hơn?

using System;
using System.Threading.Tasks;

class Program
{
    // Giả lập request với tốc độ khác nhau
    static async Task<string> RequestAsync(string name, int delay)
    {
        await Task.Delay(delay);
        return $"{name} hoàn thành trong {delay} ms";
    }

    static async Task Main()
    {
        var taskA = RequestAsync("A", 1000);
        var taskB = RequestAsync("B", 700);    // Nhanh nhất
        var taskC = RequestAsync("C", 1500);

        // Đợi task hoàn thành đầu tiên
        Task<string> finished = await Task.WhenAny(taskA, taskB, taskC);

        Console.WriteLine($"Hoàn thành đầu tiên: {finished.Result}");
    }
}

Với WhenAny ta biết task nào về đích trước. Sau đó có thể hủy các task còn lại, ví dụ bằng CancellationToken (chủ đề các bài sau).

Tại sao WhenAny trả về chính task chứ không phải kết quả?

Vì phương thức không biết trước kiểu kết quả của các task: nó không thể biết task nào hoàn thành trước. Vì vậy nó trả về chính task — sau đó ta tự lấy kết quả bằng await hoặc thuộc tính Result của task đó.

WhenAll vs WhenAny: minh hoạ so sánh

Phương thức Khi nào kích hoạt Trả về gì Kịch bản điển hình
Task.WhenAll
Khi tất cả đã hoàn thành Một task (Task/Task<T[]>) Đợi tất cả phản hồi hoặc tất cả file, tổng hợp kết quả
Task.WhenAny
Khi bất kỳ một task nào hoàn thành Chính task hoàn thành đầu tiên Dùng kết quả đầu tiên, hủy các task còn lại

3. Kết hợp WhenAll và WhenAny — kịch bản thực tế

Đôi khi cần đợi một task bất kỳ trước, rồi mới đợi tất cả. Hoặc ngược lại.

Ví dụ: "Ping-pong" giữa hai server

Giả sử bạn gửi request tới hai server clone cùng lúc phòng trường hợp một server chậm. Nhưng bạn sẽ dùng kết quả của cái về trước.

static async Task Main()
{
    var fastServer = RequestAsync("Máy chủ nhanh", 400);   // 400 ms
    var slowServer = RequestAsync("Máy chủ chậm", 2000); // 2000 ms

    var completed = await Task.WhenAny(fastServer, slowServer);
    Console.WriteLine(await completed);

    // *Tùy chọn*: ở đây có thể hủy các task chưa xong bằng CancellationToken
}

Ví dụ: chỉ xử lý sau khi tất cả dữ liệu tải xong

Trong app của bạn có ba nguồn dữ liệu. Chỉ khi cả ba đều tải xong mới nên tiến hành xử lý:

static async Task Main()
{
    var task1 = DownloadFileAsync("a.txt");
    var task2 = DownloadFileAsync("b.txt");
    var task3 = DownloadFileAsync("c.txt");

    var all = await Task.WhenAll(task1, task2, task3);

    ProcessFiles(all[0], all[1], all[2]);
}

4. Lỗi thường gặp khi làm việc với Task.WhenAll và Task.WhenAny

Lỗi #1: nghĩ rằng Task.WhenAll sẽ hoàn thành khi task đầu tiên xong.
Người mới thường nghĩ Task.WhenAll sẽ xong khi task đầu tiên hoàn thành. Thực tế nó đợi tất cả task hoàn thành.

Lỗi #2: bỏ qua ngoại lệ trong WhenAll.
Nếu một trong các task ném ngoại lệ, task kết quả sẽ ở trạng thái Faulted. Nếu không xử lý AggregateException.InnerExceptions thì có thể bỏ sót lỗi quan trọng.

Lỗi #3: truy cập Result mà không kiểm tra trong WhenAny.
Nếu task hoàn thành đầu tiên bị lỗi, gọi Result sẽ ném ngoại lệ. Hãy kiểm tra Task.IsFaulted trước khi truy cập.

Lỗi #4: khởi chạy các task tuần tự thay vì song song.
Ví dụ đợi từng task bằng await trong vòng lặp thay vì dùng Task.WhenAll sẽ làm giảm hiệu năng rất nhiều.

2
Nhiệm vụ
C# SELF, mức độ, bài học
Đã khóa
Sử dụng Task.WhenAll để thực thi nhiều task cùng lúc
Sử dụng Task.WhenAll để thực thi nhiều task cùng lúc
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION