Singleton: Chocolate Factory và an toàn đa luồng
Định nghĩa Singleton, câu chuyện nồi sô cô la, ba biến thể đa luồng và tranh cãi khi nào không nên dùng.
Trung bìnhDesign pattern ~60 phút
Ôn lại trước khi học
Giới thiệu
Bài này học cách giữ đúng 1 thực thể duy nhất. Bạn tránh tạo 2 nồi sô cô la.
Kiến thức cần nhớ
Singleton là gì
- Đảm bảo lớp chỉ có 1 thực thể.
- Chặn đường tạo mới từ bên ngoài.
- Mở 1 cổng chung để mọi người lấy.
- Ví dụ đời thường: cả nhà dùng chung 1 nồi.
Nồi sô cô la và đa luồng
- Nồi có 3 trạng thái rỗng, đang đun, xong.
- 2 nồi cùng chạy thì mẻ hỏng.
- 2 luồng cùng tạo lúc chưa có thì ra 2 nồi.
- Ví dụ đời thường: 2 người cùng bật bếp khác nhau.
Ba cách chống 2 nồi
- Đúc sẵn khi nạp lớp, đơn giản nhất.
- Khóa cả cổng, chắc nhưng chậm.
- Kiểm tra kép, nhanh nhưng khó viết đúng.
- Ví dụ đời thường: khóa cửa kho chung.
| Cách | Ưu điểm | Nhược điểm |
|---|---|---|
| Đúc sẵn | Đơn giản, an toàn | Tốn nếu không dùng |
| Khóa cả cổng | Chắc chắn | Chậm |
| Kiểm tra kép | Nhanh về sau | Khó viết đúng |
Cách cài an toàn luồng trong từng ngôn ngữ
- Java: khóa kiểm tra kép phải có
volatilecho biến thực thể, thiếu thì luồng khác có thể thấy đối tượng dựng dở. - Java:
enummột phần tử là cách gọn nhất, JVM tự đảm bảo duy nhất và an toàn luồng. - C++11 trở lên: biến
staticcục bộ trong hàm (Meyers singleton) được khởi tạo an toàn luồng, không cần tự khóa. - Singleton về bản chất là biến toàn cục có kiểm soát. Nó làm khó kiểm thử, nên cân nhắc truyền phụ thuộc qua constructor.
Ví dụ
- Input: gọi cổng 2 lần liên tiếp. Output: cùng 1 nồi, đổ đầy lần 1 thì lần 2 thấy.
- Giải thích: cổng trả đúng thực thể duy nhất.
💻 Bài tập
Ngôn ngữ: C++17 (có std::thread, std::mutex). Nộp vào day-019/.
Bài 1: Nồi sô cô la
class ChocolateBoiler {
public:
static ChocolateBoiler &instance();
ChocolateBoiler(const ChocolateBoiler &) = delete;
ChocolateBoiler &operator=(const ChocolateBoiler &) = delete;
void fill(); // chỉ khi rỗng
void boil(); // chỉ khi đầy và chưa đun
void drain(); // chỉ khi đầy và đã đun
bool isEmpty() const; bool isBoiled() const;
private:
ChocolateBoiler();
};
| Thao tác sai trạng thái | Hành vi |
|---|---|
fill() khi đang đầy | Ném std::logic_error |
boil() khi rỗng | Ném std::logic_error |
drain() khi chưa đun | Ném std::logic_error |
&instance() == &instance() -> true
fill; boil; drain -> hợp lệ, cuối cùng isEmpty = true
fill; fill -> lần 2 ném logic_error
ChocolateBoiler b; -> KHÔNG biên dịch được
Bài 2: Ba biến thể và kiểm tra đa luồng
Viết 3 biến thể getInstance() cho lớp Registry:
- Eager: biến
statickhởi tạo sẵn. - Lock toàn bộ:
std::mutexkhóa mỗi lần gọi. - Meyers singleton:
static Registry r;trong hàm (C++11 đảm bảo an toàn luồng).
Constructor tăng một biến đếm std::atomic<int> created và sleep 50 ms. Tạo 16 luồng cùng gọi getInstance(). Kiểm tra created == 1 cho cả 3 biến thể.
Viết thêm biến thể sai cố ý: kiểm tra if (ptr == nullptr) ptr = new Registry(); không khóa. Chạy 100 lần, ghi lại số lần created > 1.
Đo thời gian 10^7 lần gọi getInstance() cho biến thể 2 và 3.
Bài 3: Bỏ Singleton để dễ test (nâng cao)
Cho lớp Logger dùng Singleton, được gọi trực tiếp trong OrderService::place().
- Viết test cho
OrderServicevà ghi lại lý do test khó: không thay được logger, trạng thái rò giữa các test. - Refactor:
OrderServicenhậnLogger&qua constructor. ViếtFakeLoggerghi log vào vector. - Viết test kiểm tra
place()ghi đúng 1 dòng log.
✅ Tiêu chí hoàn thành
- Test mẫu Bài 1 pass, dòng tạo trực tiếp không biên dịch được.
- Bài 2 có kết quả chạy 16 luồng và số lần biến thể sai tạo 2 thực thể.
- Build với
-fsanitize=threadkhông báo data race ở 3 biến thể đúng. - Bài 3 có test dùng
FakeLoggerpass.
❓ Câu hỏi ôn tập
- Singleton đảm bảo điều gì?
- Vì sao đa luồng tạo ra 2 nồi?
- Ba biến thể khác nhau thế nào?
- Khi nào không nên dùng Singleton?
🌕 Bạn đã hoàn thành ngày 19. Làm hết bài tập phía trên rồi sang ngày tiếp theo.