1. Giới thiệu
Hãy nhớ ví dụ từ bài giảng trước: hai thread trong ứng dụng đơn giản của chúng ta tăng chung một bộ đếm, nhưng giá trị cuối cùng không luôn trùng với mong đợi. Để bảo vệ bộ đếm này chúng ta đã dùng keyword lock (hoặc chính xác hơn là Monitor), cái phù hợp để đồng bộ chỉ trong một process. Nhưng nếu chương trình của bạn không phải là duy nhất muốn dùng tài nguyên thì sao? Ví dụ, bạn viết một service chạy ở hai instance, và cả hai muốn ghi vào cùng một file… hoặc điều khiển thiết bị như cổng máy in? Lúc này mutex xưa cũ (từ mutual exclusion — "loại trừ lẫn nhau") sẽ giúp được.
Khái niệm
Mutex là một primitive đồng bộ hóa, không chỉ giới hạn truy cập giữa các thread trong cùng một process mà còn cho phép điều phối truy cập giữa các process khác nhau trên cùng một máy. Hãy tưởng tượng nó như một bảng "Đã có người" trước phòng họp, cả nhân viên và khách đều nhìn thấy.
Trong .NET lớp để làm việc này là System.Threading.Mutex.
Khi nào Mutex thực sự cần thiết:
- Khi bạn đồng bộ hóa truy cập giữa các thread ở các process khác nhau (ví dụ, hai ứng dụng riêng biệt cùng làm việc với một file).
- Khi tài nguyên quá quý giá và nguyên tử đến mức ngay cả context process không thể chia sẻ quyền truy cập.
Để đồng bộ chỉ giữa các thread trong một process thường dùng lock (Monitor). Mutex nên dùng cho đồng bộ giữa process vì nó nặng hơn và chậm hơn.
Cách Mutex hoạt động
flowchart TD
A(Quy trình 1) --|Yêu cầu|--> M(Mutex)
B(Quy trình 2) --|Yêu cầu|--> M
M --|Chỉ cho một|--> R(Tài nguyên chung)
A --|Giải phóng|--> M
B --|Sau khi giải phóng|--> M
2. Cú pháp cơ bản khi làm việc với Mutex
Tạo
Mutex được tạo đơn giản giống như hầu hết các lớp đồng bộ khác:
using System.Threading;
Mutex mutex = new Mutex();
Các phương thức chính
- WaitOne() — cố gắng chiếm mutex; nếu không được thì thread sẽ bị block cho đến khi mutex được người khác giải phóng.
- ReleaseMutex() — giải phóng mutex, cho phép các thread hoặc process khác vào critical section.
Ví dụ đơn giản: đồng bộ giữa các thread trong một process
using System;
using System.Threading;
class Program
{
static Mutex mutex = new Mutex();
static void Main()
{
Thread t1 = new Thread(PrintNumbers);
Thread t2 = new Thread(PrintNumbers);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
static void PrintNumbers()
{
for (int i = 0; i < 5; i++)
{
mutex.WaitOne(); // Vào critical section
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: {i}");
mutex.ReleaseMutex(); // Ra khỏi critical section
Thread.Sleep(100); // Cho dễ nhìn
}
}
}
Trong ví dụ này cả hai thread sẽ lần lượt có quyền ghi ra console.
3. Đồng bộ giữa process với named mutex
Cho "pháo nặng" — đồng bộ giữa process — dùng named mutex. Bạn đặt cho nó một tên, và tất cả process trên máy có thể truy cập cùng mutex đó.
Mutex mutex = new Mutex(false, "MyApp_Mutex");
Tham số constructor:
- Tham số đầu (bool initiallyOwned) — liệu thread có muốn chiếm mutex ngay sau khi tạo hay không. Thường là false.
- Tham số thứ hai — tên của mutex. Tất cả process dùng mutex với tên này sẽ tham chiếu cùng một đối tượng, ví dụ "MyApp_Mutex".
Ví dụ: hai ứng dụng dùng chung Mutex
Chạy cùng một chương trình ở hai cửa sổ khác nhau để thấy hiệu ứng.
using System;
using System.Threading;
class Program
{
static void Main()
{
using (Mutex mutex = new Mutex(false, "MySuperUniqueMutexName"))
{
Console.WriteLine("Đang cố vào critical section...");
mutex.WaitOne(); // Chờ tới khi process khác giải phóng mutex
try
{
Console.WriteLine("Critical section đang được process này giữ.");
Console.WriteLine("Nhấn Enter để thoát critical section.");
Console.ReadLine();
}
finally
{
mutex.ReleaseMutex();
Console.WriteLine("Critical section đã được giải phóng.");
}
}
}
}
Thử nghiệm:
- Mở hai cửa sổ với ứng dụng này.
- Chạy cả hai — tiến trình thứ hai sẽ chờ cho tới khi bạn nhấn Enter ở cái đầu tiên.
4. Giới hạn số instance của ứng dụng
Mutex thường dùng để giới hạn số bản chạy cùng lúc. Ví dụ: "Này người dùng, lần sau đừng mở hai bản calculator!".
using System;
using System.Threading;
class Program
{
static void Main()
{
bool createdNew;
using (Mutex mutex = new Mutex(true, "CalculatorAppInstanceMutex", out createdNew))
{
if (!createdNew)
{
Console.WriteLine("Ứng dụng đã được khởi chạy!");
return;
}
Console.WriteLine("Ứng dụng khởi động thành công. Nhấn Enter để thoát.");
Console.ReadLine();
}
}
}
Pattern này thường thấy trong ứng dụng desktop: instance đầu chạy, instance sau chỉ báo và thoát. Ví dụ chính thức của Microsoft — trong tài liệu.
5. Lỗi điển hình khi làm việc với Mutex
Lỗi #1: quên gọi ReleaseMutex().
Nếu thread chiếm mutex (WaitOne()) nhưng không gọi ReleaseMutex() (bị văng exception hoặc quên), thì không thread hay process nào khác có thể vào cho tới khi thread "quên" kết thúc. Điều này có thể gây deadlock. Thói quen tốt là luôn dùng try-finally:
mutex.WaitOne();
try
{
// critical section
}
finally
{
mutex.ReleaseMutex();
}
Lỗi #2: gọi ReleaseMutex() sai số lần.
Nếu gọi ReleaseMutex() nhiều hơn số lần đã gọi WaitOne(), sẽ ném ra exception ApplicationException.
Lỗi #3: cố giải phóng Mutex không phải của mình.
Mutex "gắn" với thread đã chiếm. Chỉ thread đó có quyền gọi ReleaseMutex(). Nếu thread khác cố giải phóng, .NET sẽ ném exception.
Lỗi #4: dùng Mutex trong khi lock đã đủ.
Mutex chậm hơn lock vì có thể hoạt động giữa process và cần syscall hệ thống. Khuyến nghị: nếu không cần đồng bộ giữa process — hãy dùng lock.
Lỗi #5: đặt tên mutex không tốt.
Nếu chọn tên quá đơn giản (ví dụ "MyMutex"), có thể vô tình "chung tên" với chương trình khác cũng dùng tên đó. Tốt hơn hãy dùng tên duy nhất (ví dụ có tên công ty hoặc ứng dụng).
6. Các mẹo hữu ích
WaitOne(timeout)
Có thể chỉ định timeout khi chờ mutex:
if (mutex.WaitOne(5000)) // chờ tới 5 giây
{
try { /* ... */ }
finally { mutex.ReleaseMutex(); }
}
else
{
Console.WriteLine("Không thể truy cập tài nguyên trong 5 giây!");
}
Mutex.TryOpenExisting
Nếu cần tham gia tới một mutex đã tồn tại, dùng method static:
if (Mutex.TryOpenExisting("DiaryFileWriteMutex", out Mutex existingMutex))
{
// bây giờ existingMutex — là tham chiếu tới mutex tồn tại
}
Phân quyền giữa người dùng
Mặc định named mutex có thể truy cập bởi tất cả user có quyền tạo system objects. Nếu cần kiểm soát chặt chẽ hơn — dùng constructor với MutexSecurity.
Bảng so sánh: lock/Monitor, Mutex, Semaphore
| Primitive | Đồng bộ giữa process? | Tốc độ | Nên dùng ở đâu |
|---|---|---|---|
|
Không | Rất nhanh | Giữa các thread trong cùng một process |
|
Có | Chậm hơn (đắt hơn) | Giữa các thread của các process khác nhau |
|
Có (với NamedSemaphore) | Tương đương với Mutex | Khi cần giới hạn số thread |
Chi tiết về semaphore — ở bài tiếp theo.
Các trạng thái của Mutex
stateDiagram-v2
[*] --> Unowned
Unowned --> Owned : WaitOne()
Owned --> Owned : WaitOne() (reentrant)
Owned --> Unowned : ReleaseMutex()
Owned --> Abandoned : thread "chết", không gọi ReleaseMutex()
Abandoned --> Unowned
- Unowned — không ai sở hữu mutex.
- Owned — mutex đang bị thread chiếm.
- Abandoned — thread bị kết thúc đột ngột mà không gọi ReleaseMutex(). Người tiếp theo lấy được mutex sẽ gặp exception AbandonedMutexException (điều này báo hiệu lỗi và khả năng trạng thái tài nguyên không nhất quán).
GO TO FULL VERSION