1. Giới thiệu
Hãy tưởng tượng thread là một nhân viên chăm chỉ mà bạn giao việc. Nhân viên có thể ngủ (chưa bắt đầu làm), làm việc vất vả (đang thực thi phương thức của bạn), chờ khi bạn giao nhiệm vụ mới (idle), hoặc hoàn thành công việc (kết thúc).
Trong C# (và nói chung .NET) vòng đời của thread gồm vài trạng thái:
- Unstarted — thread được tạo nhưng chưa được start.
- Running — thread đang chạy.
- WaitSleepJoin — thread tạm thời không làm việc (ví dụ, đang chờ signal hoặc "ngủ").
- Stopped — thread đã hoàn thành nhiệm vụ và kết thúc.
Có thể tưởng tượng vòng đời này bằng sơ đồ như sau:
stateDiagram-v2
[*] --> Unstarted
Unstarted --> Running: Start()
Running --> WaitSleepJoin: Wait/Sleep/Join
WaitSleepJoin --> Running: Đã nhận signal/Thời gian hết
Running --> Stopped: Phương thức kết thúc
WaitSleepJoin --> Stopped: Phương thức kết thúc
Stopped --> [*]
Mọi thứ bắt đầu bằng việc tạo đối tượng Thread, nhưng nếu bạn chưa gọi Start(), thread "ngủ" ở trạng thái Unstarted. Sau khi gọi Start() — thread chuyển sang Running. Nếu thread gọi Thread.Sleep hoặc chờ thứ gì đó (ví dụ Monitor.Wait), nó vào trạng thái chờ. Khi phương thức truyền cho thread kết thúc, thread chết, không thể "hồi sinh". Một chiều thôi.
2. Thực hành: Vòng đời của một thread đơn giản
Hãy xem ví dụ kinh điển:
using System;
using System.Threading;
class Program
{
static void Main()
{
// Tạo thread — lúc này chỉ là lên kế hoạch làm việc
Thread worker = new Thread(DoWork);
Console.WriteLine($"Trạng thái thread sau khi tạo: {worker.ThreadState}");
// Khởi động thread
worker.Start();
Console.WriteLine($"Trạng thái thread sau khi start: {worker.ThreadState}");
// Cho main thread ngủ một chút để worker kịp làm việc
Thread.Sleep(100);
Console.WriteLine($"Trạng thái thread (sau đó): {worker.ThreadState}");
// Chờ cho worker hoàn thành (join)
worker.Join();
Console.WriteLine($"Trạng thái thread sau khi kết thúc: {worker.ThreadState}");
Console.WriteLine("Main thread kết thúc");
}
static void DoWork()
{
Console.WriteLine("Worker thread bắt đầu làm việc!");
Thread.Sleep(500);
Console.WriteLine("Worker thread đã hoàn thành công việc!");
}
}
Chương trình in gì?
- Sau khi tạo thread — trạng thái sẽ là Unstarted.
- Sau khi start — thường là Running (nhưng có thể là Running | Background).
- Trong khi chạy — trạng thái có thể là Running, hoặc WaitSleepJoin nếu thread đang "ngủ".
- Sau khi phương thức kết thúc — trạng thái thành Stopped.
Mã này là công cụ tuyệt vời để thấy thread đang ở trạng thái nào. Có thể thử thay đổi thời gian chờ để quan sát trạng thái thay đổi.
3. Quản lý thread: các phương thức chính
Start: Start()
Rõ ràng nhưng nhắc lại: tạo thread — gọi Start() để chạy. Và chỉ gọi một lần thôi: gọi Start() lần nữa sẽ ném exception ThreadStateException.
Thread t = new Thread(MyMethod);
t.Start(); // OK
t.Start(); // Lỗi!
Chờ kết thúc: Join()
Đôi khi cần chờ thread hoàn thành rồi mới tiếp tục. Dùng Join().
Thread t = new Thread(MyMethod);
t.Start();
t.Join(); // Khóa thread hiện tại cho đến khi t kết thúc
Nếu có nhiều thread, bạn có thể gọi Join() cho từng cái — main sẽ đợi cho đến khi tất cả "công nhân" xong việc.
Biến thể: có overload Join(int millisecondsTimeout) để chờ tối đa thời gian chỉ định rồi tiếp tục.
// Chờ không quá 2 giây
if (t.Join(2000))
Console.WriteLine("Thread kết thúc đúng hạn");
else
Console.WriteLine("Đợi cũng chán rồi...");
Dừng cưỡng bức: tại sao không nên
Trong các phiên bản cũ của .NET có method Thread.Abort() để "giết" thread. Hiện ít dùng vì nguy hiểm và có thể để ứng dụng ở trạng thái lạ. Triết lý .NET là: thread nên tự kết thúc một cách chủ động. Bạn không "giết" nhân viên — bạn lịch sự báo rằng hết giờ làm.
4. Cách "dừng" thread đúng cách
Cách an toàn nhất để dừng thread là dùng cờ hủy (cancellation flag) hoặc dấu hiệu kết thúc mà thread kiểm tra định kỳ.
class Worker
{
private volatile bool shouldStop = false;
public void DoWork()
{
while (!shouldStop)
{
Console.WriteLine("Đang làm việc!");
Thread.Sleep(300);
}
Console.WriteLine("Thread kết thúc theo lệnh.");
}
public void RequestStop()
{
shouldStop = true;
}
}
Sử dụng:
Worker w = new Worker();
Thread t = new Thread(w.DoWork);
t.Start();
// Chờ một chút
Thread.Sleep(1000);
// Yêu cầu thread dừng
w.RequestStop();
t.Join(); // Chờ thread kết thúc
Điểm quan trọng: volatile
Từ khóa volatile nói với compiler và CPU: "Đừng cache trường này, luôn lấy giá trị mới!" Điều này quan trọng để thread nhìn thấy cập nhật của cờ dừng. Nếu không có hoặc không dùng các cơ chế đồng bộ khác, thread có thể không nhận thấy thay đổi và loop vô hạn.
5. Thread vào trạng thái chờ và ngủ
Đôi khi thread tạm dừng làm việc — hoặc nó chờ, hoặc nó ngủ.
Ngủ: Thread.Sleep
Khi muốn cho thread nghỉ hoặc giảm tốc độ (ví dụ tránh tải CPU), dùng Thread.Sleep(milliseconds).
// Thread ngủ 2 giây
Thread.Sleep(2000);
Trong lúc ngủ thread không làm gì cả.
Chờ / Join
Khi main chờ child kết thúc (Join), main "tạm ngừng". Tương tự, nếu thread chờ tài nguyên (ví dụ qua monitor hoặc primitive đồng bộ khác), nó chuyển sang trạng thái chờ.
6. Quản lý background của thread
Trong .NET có hai loại thread: foreground (chạy nền chính) và background (chạy nền phụ). Khác biệt:
- Nếu chỉ còn background threads trong process, process sẽ kết thúc tự động.
- Main thread và tất cả foreground threads phải kết thúc để process dừng.
Bạn có thể đánh dấu thread là background:
Thread t = new Thread(SomeMethod);
t.IsBackground = true; // Đánh dấu thành background
t.Start();
Ví dụ thực tế — Demon vs. Normal thread
Thread t = new Thread(() =>
{
while (true)
{
Console.WriteLine("Tôi là phantom (background), tôi không bị dừng!");
Thread.Sleep(500);
}
});
t.IsBackground = true; // Làm thành background
t.Start();
Thread.Sleep(1200);
Console.WriteLine("Main thread kết thúc");
// Sau khi Main xong — process chết, và thread vĩnh viễn của ta cũng biến mất
Sau khi Main kết thúc — process dừng; các background threads bị terminate tự động.
7. Những chi tiết hữu ích
Những việc không nên làm với thread
- Không thể "khởi động lại" một thread. Đối tượng Thread chỉ sống một lần: khi phương thức hoàn thành — thread chết, và gọi Start() nữa sẽ lỗi.
- Không nên dừng thread khác cưỡng bức bằng Thread.Abort() hoặc Thread.Suspend() — đã lỗi thời và nguy hiểm.
- Không bỏ qua việc kết thúc tài nguyên của thread. Nếu thread làm việc với file hoặc resource, hãy giải phóng đúng cách trước khi kết thúc.
Kiểm tra trạng thái và quản lý vòng đời
if (t.IsAlive)
{
Console.WriteLine("Thread vẫn còn sống");
}
else
{
Console.WriteLine("Thread đã kết thúc");
}
IsAlive — true khi thread đang thực thi phương thức; sau khi xong — false.
Vòng đời của một thread đơn giản trong .NET
| Trạng thái | Cách vào | Có nghĩa là gì? | Cách thoát |
|---|---|---|---|
| Unstarted | |
Thread được tạo, chưa start | Gọi Start() |
| Running | |
Thread đang làm việc | Kết thúc phương thức |
| WaitSleepJoin | Sleep(), Join(), chờ | Thread tạm thời không hoạt động | Chờ kết thúc |
| Stopped | Phương thức thread đã kết thúc | Thread "chết" | Không thể — kết thúc |
FAQ về quản lý vòng đời thread
Câu hỏi: Có thể "giết" thread theo lệnh không?
Trả lời: Không và không nên, thread nên tự quản lý việc kết thúc. Dùng flags để hủy.
Câu hỏi: Có thể tái sử dụng đối tượng Thread không?
Trả lời: Không. Tạo đối tượng mới cho công việc mới.
Câu hỏi: Nếu main kết thúc mà child vẫn chạy thì sao?
Trả lời: Nếu child là background (IsBackground == true), ứng dụng sẽ kết thúc. Nếu không — process sẽ sống cho tới khi tất cả thread kết thúc.
Câu hỏi: Làm sao giải phóng tài nguyên nếu thread kết thúc do hủy?
Trả lời: Dùng khối try...finally bên trong method của thread để đảm bảo tài nguyên được giải phóng trong mọi trường hợp.
8. Lỗi phổ biến và cách tránh khi làm việc với thread
Lỗi #1: tái sử dụng cùng một đối tượng Thread.
Không thể start cùng một Thread object nhiều lần. Sau khi thread kết thúc, không thể restart — sẽ ném exception.
Lỗi #2: giải phóng resource sai trong thread.
Nếu thread làm việc với file, mạng hay resource khác, nhớ đóng và giải phóng đúng cách. Nên dùng finally hoặc cấu trúc using để tránh leak và deadlock.
Lỗi #3: tạo quá nhiều thread.
Quá nhiều thread làm debugging khó và có thể giảm hiệu năng. Thêm thread không phải lúc nào cũng tốt — cân nhắc dùng thread pool hoặc async khi phù hợp.
GO TO FULL VERSION