CodeGym /Các khóa học /C# SELF /Khởi chạy luồng bằng lớp T...

Khởi chạy luồng bằng lớp Thread

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

1. Giới thiệu

Nếu tưởng tượng một process như siêu thị, thì các luồng (thread) — là các thu ngân ở các quầy khác nhau, họ phục vụ khách cùng lúc. Tất cả thu ngân làm việc trong cùng một cửa hàng, nhưng mỗi người thực hiện công việc của mình song song, nhờ đó công việc nhanh hơn và hiệu quả hơn. Các luồng trong một process cho phép chạy nhiều nhiệm vụ đồng thời, chia sẻ tài nguyên chung và phối hợp công việc trong cùng một ứng dụng. Lợi thế chính — các luồng thực sự có thể chạy song song nếu CPU hỗ trợ đa nhiệm.

Nhu cầu thực tế của multithreading

Tại sao chúng ta cần chạy nhiều luồng? Đây là vài tình huống thực tế:

  • Bạn viết ứng dụng có giao diện GUI và không muốn nó "bị treo" trong khi thực hiện tác vụ lâu.
  • Bạn cần tải nhiều file cùng lúc.
  • Trong game, kẻ địch cần "suy nghĩ" độc lập với nhau.

Ngạc nhiên là nhiều chương trình vẫn bị "treo" vì dùng luồng chưa đúng. Hôm nay chúng ta học cách tránh mấy rắc rối đó.

Lớp Thread: nền tảng của multithreading thủ công

Lớp Thread — là "cổ điển" trong lập trình đa luồng trên .NET. Dù có các công cụ hiện đại hơn (Task, async/await), làm việc với Thread vẫn có ý nghĩa, đặc biệt nếu bạn muốn cảm nhận việc điều khiển luồng "từ đầu".

Sơ đồ tạo luồng

  1. Tạo một đối tượng của lớp Thread, truyền cho nó phương thức sẽ chạy trong luồng.
  2. Khởi chạy luồng bằng phương thức Start().
  3. (Không bắt buộc) Quan sát xem chuyện gì xảy ra — vì mọi thứ có thể chạy song song!

2. Khởi chạy luồng bằng Thread

Thử làm để cảm nhận sức mạnh song song. Thêm vào ứng dụng giả định một lớp nhỏ, nó sẽ đếm đến một số nhất định và in tiến trình. Chúng ta sẽ học cách chạy công việc đó trong một luồng riêng.

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        Console.WriteLine("Luồng chính đã bắt đầu!");

        // Tạo đối tượng luồng, chỉ định phương thức sẽ thực thi
        Thread workerThread = new Thread(CountToTen);

        // Khởi chạy luồng mới
        workerThread.Start();

        // Luồng chính cũng làm gì đó: in các chấm...
        for (int i = 0; i < 5; i++)
        {
            Console.Write(".");
            Thread.Sleep(500); // Tạm dừng để dễ quan sát
        }

        Console.WriteLine("\nLuồng chính đã kết thúc!");
    }

    static void CountToTen()
    {
        for (int i = 1; i <= 10; i++)
        {
            Console.WriteLine($"[Luồng] Đếm: {i}");
            Thread.Sleep(400);
        }
        Console.WriteLine("[Luồng] Xong!");
    }
}

Có gì xảy ra?

Bạn sẽ thấy trên console rằng các dấu chấm và "Đếm: X" xuất hiện đan xen. Đó là dấu hiệu đầu tiên của multithreading! Luồng chính in chấm, luồng mới đếm tới 10. Chúng không cản trở nhau, giống hai nhạc công trong ban nhạc: người chơi trống, người kia chơi piano. Mỗi người có tiếng riêng, và chơi cùng nhau tạo thành giai điệu.

3. Làm sao truyền dữ liệu vào luồng?

Đôi khi luồng không chỉ biết làm gì mà còn cần biết với cái gì làm việc. Nếu phương thức chạy trong luồng có tham số, làm sao truyền vào?

Cách 1: Dùng lambda (anonymous method)

int bounds = 7;
Thread t = new Thread(() => CountToNumber(bounds));
t.Start();
static void CountToNumber(int n)
{
    for (int i = 1; i <= n; i++)
    {
        Console.WriteLine($"[Luồng] {i} / {n}");
        Thread.Sleep(300);
    }
}

Ở đây ta bọc lời gọi phương thức cần thiết vào lambda để truyền tham số. Đây là cách rất phổ biến, vì Thread mong đợi một phương thức không có tham số (ThreadStart).

Cách 2: Dùng ParameterizedThreadStart

Có thể dùng delegate đặc biệt ParameterizedThreadStart, nhận một tham số kiểu object.

Thread t = new Thread(CountToNumberObject);
t.Start(12);

static void CountToNumberObject(object? n)
{
    int max = (int)n!;
    for (int i = 1; i <= max; i++)
    {
        Console.WriteLine($"[Luồng] {i} / {max}");
        Thread.Sleep(200);
    }
}

Đúng là tham số kiểu object, nên cần ép kiểu. Không lý tưởng, nhưng hoạt động! Còn C# hiện đại thường ưu tiên lambda hơn.

4. Quản lý vòng đời luồng

Ta xem các tiện ích hữu ích mà lớp Thread cung cấp.

Thuộc tính / Phương thức Mục đích
Thread.Start()
Khởi chạy luồng (phương thức được chỉ định khi tạo)
Thread.Join()
Chờ luồng kết thúc (khóa luồng gọi cho tới khi luồng kia hoàn thành)
Thread.IsAlive
Cho biết luồng đang chạy hay không (true/false)
Thread.Name
Cho phép gán tên cho luồng (hữu ích để debug)
Thread.CurrentThread
Lấy đối tượng biểu diễn luồng hiện tại
Thread.Sleep(ms)
Tạm dừng luồng hiện tại trong ms mili-giây

Ví dụ: Chờ luồng kết thúc

Đôi khi cần luồng chính đợi cho luồng phụ hoàn thành công việc.

Thread t = new Thread(() =>
{
    for (int i = 0; i < 5; i++)
    {
        Console.WriteLine($"[Luồng thứ hai] {i}");
        Thread.Sleep(300);
    }
});
t.Start();

Console.WriteLine("[Luồng chính] Đang chờ luồng thứ hai hoàn thành...");
t.Join(); // Luồng chính đợi ở đây

Console.WriteLine("[Luồng chính] Luồng thứ hai đã kết thúc!");

Không có Join() chương trình có thể kết thúc ngay cả khi luồng còn chạy. Với Join(), luồng chính sẽ kiên nhẫn đợi tới khi mọi việc xong.

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

Đặt tên luồng: để khỏi rối

Đặt tên luồng để debug dễ hơn:

Thread t = new Thread(() =>
{
    Console.WriteLine($"Đang chạy trong luồng: {Thread.CurrentThread.Name}");
});
t.Name = "Luồng-Đếm";
t.Start();

Điều này hữu ích khi có nhiều luồng và mỗi luồng làm việc khác nhau.

Hạn chế và tương lai thực tế

Cần nói rõ: trong ứng dụng hiện đại, quản lý luồng thủ công bằng Thread ít khi được dùng. Thực tế thường dùng công cụ mạnh hơn và "thông minh" hơn (Task, async/await), chúng ta sẽ xem sau. Nhưng hiểu các nguyên tắc cơ bản về luồng quan trọng cho:

  • Hiểu "bên trong" của C# và .NET.
  • Phỏng vấn (đôi khi họ sẽ hỏi khác biệt giữa ThreadTask).
  • Chẩn đoán và sửa lỗi phức tạp trong các hệ thống lớn, "kế thừa".

Sơ đồ tổng kết: vòng đời của luồng

stateDiagram-v2
    [*] --> New: Thread được tạo
    New --> Running: Start()
    Running --> Stopped: Phương thức hoàn thành
    Stopped --> [*]

Giờ bạn có thể tự tạo và khởi chạy luồng trong C#. Bạn không còn chỉ là hành khách trên tàu nữa, mà là lái tàu, điều khiển nhiều toa cùng lúc! Tiếp theo — vòng đời luồng, quản lý, đồng bộ và những chân trời mới của song song.

6. Lỗi thường gặp và mẹo khi làm việc với Thread

Lỗi #1: không khởi chạy luồng.
Thường thì người ta tạo đối tượng Thread nhưng quên gọi Start(). Kết quả là luồng không chạy, và lý do này khó nhận ra ngay.

Lỗi #2: thay đổi dữ liệu chung mà không đồng bộ.
Nếu nhiều luồng làm việc với cùng một biến mà không bảo vệ, chờ rắc rối! Giống như hai thu ngân cùng rút tiền từ một hộp — nhanh chóng sẽ có hỗn loạn và lỗi.

Lỗi #3: dùng các phương thức lỗi thời và nguy hiểm.
Đừng dùng Thread.Suspend(), Thread.Resume() và mấy thứ tương tự — chúng nguy hiểm và lỗi thời. Hãy quản lý vòng đời luồng bằng cách khác.

Lỗi #4: ngoại lệ không được bắt trong luồng.
Nếu trong luồng xảy ra ngoại lệ mà không bắt, luồng sẽ kết thúc, và luồng chính có thể không biết điều đó! Bọc code trong luồng bằng khối try-catch để bắt lỗi và log.

Thread t = new Thread(() =>
{
    try
    {
        // ... code của bạn
    }
    catch (Exception ex)
    {
        // Ghi log lỗi
        Console.WriteLine($"[Luồng] Lỗi: {ex.Message}");
    }
});
t.Start();
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION