C# cơ bản .NET Core
Lập trình C# (C Sharp)
DI Dependency Injection (Bài trước)
(Bài tiếp) Thư viện lớp

Thi hành song song tác vụ với Parallel.For

Lớp Parallel thuộc namespace System.Threading.Tasks, nó trừu tượng hóa các thread, lớp này có phương thức tĩnh Parallel.For, Parallel.ForEach để thực hiện vòng lặp forforeach để chạy song song các tác vụ. Nếu với forforeach trong C#, thì vòng lặp đó chạy trong một thread, nhưng với Parallel nó sử dụng đa tác vụ, đa tiến trình để thực hiện nội dung lặp. Ngoài ra là Parallel.Invoke để thực hiện một Action có khả năng chạy song song.

Parallel.For có nhiều quá tải, cú pháp bản đơn giản như sau:


ParallelLoopResult result = Parallel.For(i1, i2, task);

Vòng lặp chạy (biến chạy) từ số nguyên i1 đến i2, mỗi lần lặp nó sẽ thực hiện Action task

task là một delegate, kiểu Action<int> có nghĩa nó làm phương thức trả về void, có một tham số kiểu int, tham số này là biến chạy. Ví dụ đây là một action phù hợp cho Parallel.For


Action<int> action = (int x) => {
    // Doing somthing here ...
};

result đối tượng lớp ParallelLoopResult trả về từ Paralell.For, thuộc tính ParallelLoopResult.IsCompleted cho biết vòng lặp đã được duyệt qua hết, tất cả các task đã khởi chạy.


Ví dụ: Source Code

class Program
{
    //In thông tin, Task ID và thread ID đang chạy
    public static void PintInfo(string info) =>
            Console.WriteLine($"{info, 10}    task:{Task.CurrentId,3}    " +
                              $"thread: {Thread.CurrentThread.ManagedThreadId}");

    // Phương thức phù hợp với Action<int>, được làm tham số action của Parallel.For
    public static void RunTask(int i)  {
        PintInfo($"Start {i,3}");
        Task.Delay(1000).Wait();          // Task dừng 1s - rồi mới chạy tiếp
        PintInfo($"Finish {i,3}");
    }

    public static void ParallelFor() {
        ParallelLoopResult result = Parallel.For(1, 20, RunTask);   // Vòng lặp tạo ra 20 lần chạy RunTask
        Console.WriteLine($"All task start and finish: {result.IsCompleted}");
    }
    static void Main(string[] args)
    {
        ParallelFor();

        Console.WriteLine("Press any key ..."); Console.ReadKey();
    }
}

Từ kết quả trên bạn thấy:

  • Lệnh Parallel.For khởi chạy song song nhiều tác vụ (thời điểm bắt đầu của mỗi tác vụ không giống nhau, có những tác vụ đã kết thúc thì tác vụ sau mới chạy, nó có thể phụ thuộc vào tài nguyên hệ thống RAM, CPU ...
  • Một task nó có chạy trên một thread nào đó (chứ không phải mỗi task một thread), một thread có thể sử dụng bởi nhiều task. Ví dụ nhìn vào kết quả, task với i = 1, và task với id = 15 chạy trên một thread
  • Khi tất vòng lặp duyệt qua hết, có nghĩa là đã khởi chạy hết các tác vụ, nếu tất cả các tác vụ trả về (khi chạy xong hàm Action, hoặc ngay khi gọi Action nếu Action là async - hãy xem kỹ phần async - await ở phần trước) thì kết quả lưu vào result với result.IsCompletedtrue

Parallel.For khi Action là async

Để ý, bản thân vòng lặp Parallel.For, khi các Action chạy, mặc dù chúng chạy trên những Task và Thread, nhưng khi tất cả các Action hoàn hành thì vòng lặp mới hoàn thành. Ở ví dụ trên, bạn thấy tất cả đều Finish mới trả về kết quả result (All task start and finish: True)

Điều này, lại dẫn đến Parallel.For khóa(block) thread gọi nó. Để không bị khóa, có thể chuyển các Action là async

Source Code
public static async void RunTask(int i)  {
    PintInfo($"Start {i,3}");
    // Task.Delay(1000).Wait();          // Task dừng 1s - rồi mới chạy tiếp
    await Task.Delay(1);         // Task.Delay là một async nên có thể await, RunTask chuyển điểm gọi nó tại đây
    PintInfo($"Finish {i,3}");
}
    

Kết quả khi chạy, vòng lặp Parallet.For nó trả về ngay khi tất cả các Task đã khởi chạy - (chứ không cần tất cả các task chạy và kết thúc như trường hợp trước). Khi Parallet.For hoàn thành, có một số Task đã kết thúc có những Task vẫn đang chạy.

Parallel.ForEach chạy song song tác vụ

Với Parallet.ForEach cũng là vòng lặp để chạy nhiều tác vụ, nhưng nó duyệt qua các Collection như Mảng, List ... tương tự như vòng lặp foreach. Cú pháp cơ bản như sau:

ParallelLoopResult result = Parallel.ForEach(source, RunTask);

Trong đó source là một Collection như mảng, List. RunTaskAction, có 1 tham số có kiểu giống kiểu phần tử trong source, giá trị tham số này là giá trị phần tử trong source trong mỗi vòng lặp. Ví dụ:

Source Code
public static async void RunTask(string s)  {
    PintInfo($"Start {s,10}");
    await Task.Delay(1);                 // Task.Delay là một async nên có thể await, RunTask chuyển điểm gọi nó tại đây
    PintInfo($"Finish {s,10}");
}

public static void ParallelFor() {

    string[] source = new string[] {"xuanthulab1","xuanthulab2","xuanthulab3",
                                    "xuanthulab4","xuanthulab5","xuanthulab6",
                                    "xuanthulab7","xuanthulab8","xuanthulab9"};
    // Dùng List thì khởi tạo
    // List<string> source = new List<string>();
    // source.Add("xuanthulab1");

    ParallelLoopResult result = Parallel.ForEach(
        source, RunTask
    );

    Console.WriteLine($"All task started: {result.IsCompleted}");
}
static void Main(string[] args)
{
    ParallelFor();
    Console.WriteLine("Press any key ...");
    Console.ReadKey();
}

Parallel.Invoke chạy song song nhiều loại tác vụ (phương thức)

Với các vòng lặp ở trên, thì các tác vụ định nghĩa trọng một Action, nhưng nếu muốn chạy song song nhiều loại Action (phương thức) một lúc thì dùng Paralell.Invoke

Parallel.Invoke(action1, action2, action3);

Trong đó tham số là các Action

Source Code
public static void PintInfo(string info) =>
Console.WriteLine($"{info, 10}    task:{Task.CurrentId,3}    "
                + $"thread: {Thread.CurrentThread.ManagedThreadId}");

public static async void RunTask(string s)  {
    PintInfo($"Start {s,10}");
    await Task.Delay(1);
    PintInfo($"Finish {s,10}");
}

public static void actionA() {
    PintInfo($"Finish {"ActionA",10}");
}

public static void actionB() {
    PintInfo($"Finish {"ActionB",10}");
}


public static void ParallelInvoke() {
    Action action1  = () => {
        RunTask("Action1");
    };

    Parallel.Invoke(action1, actionA, actionB);
}
static void Main(string[] args)
{
    ParallelInvoke();
    Console.WriteLine("Press any key ...");
    Console.ReadKey();
}

Tham khảo các mã nguồn CS028_Parallelhoặc tải về ex028


Đăng ký nhận bài viết mới
DI Dependency Injection (Bài trước)
(Bài tiếp) Thư viện lớp