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

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ểmNhược điểm
Đúc sẵnĐơn giản, an toànTốn nếu không dùng
Khóa cả cổngChắc chắnChậm
Kiểm tra képNhanh về sauKhó 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ó volatile cho biến thực thể, thiếu thì luồng khác có thể thấy đối tượng dựng dở.
  • Java: enum mộ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 static cụ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áiHành vi
fill() khi đang đầyNém std::logic_error
boil() khi rỗngNém std::logic_error
drain() khi chưa đunNé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:

  1. Eager: biến static khởi tạo sẵn.
  2. Lock toàn bộ: std::mutex khóa mỗi lần gọi.
  3. 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().

  1. Viết test cho OrderService và ghi lại lý do test khó: không thay được logger, trạng thái rò giữa các test.
  2. Refactor: OrderService nhận Logger& qua constructor. Viết FakeLogger ghi log vào vector.
  3. 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=thread không báo data race ở 3 biến thể đúng.
  • Bài 3 có test dùng FakeLogger pass.

❓ 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.

Bớt paste mù, hiểu sâu từng dòng mình viết.

180 Days Challenge. 180 ngày cai nghiện vibe code.