Design Pattern: Singleton và Lazy Loading – giải pháp cho đa luồng (phần 2)

Design Pattern: Singleton và Lazy Loading – giải pháp cho đa luồng (phần 2)

Trong bài viết trước về cách cài đặt theo design patern Singleton, phần code cài đặt sẽ chạy chính xác nếu chạy đơn luồng. Khi ta thực hiện chạy đa luồng, sẽ có những trường hợp không đảm bảo các tính chất mà design pattern Singleton quy định. Trong bài viết này, tôi sẽ sử dụng 2 kỹ thuật: object lockinglazy loading để khắc phục lỗi trên.
 

Nguyên nhân lỗi

Ta phân tích đoạn code cài đặt trong bài viết trước: (Design Pattern: Singleton – duy nhất một thể hiện)

 
class Singleton { 
	private static Singleton _instance; 
	
	protected Singleton() { } 
	
	public static Singleton Instance() { 
		if (_instance == null) 
			_instance = new Singleton(); 
		return _instance; 
	} 
} 

 
Bây giờ ta có 2 luồng cùng gọi đến phương thức Instance để lấy về thể hiện của lớp Singleton. Tôi sẽ viết lại trình tự gọi lệnh có thể xảy ra như sau:
1. Luồng 1: Vào phương thức Instance (dòng 6)
2. Luồng 2: Vào phương thức Instance (dòng 6)
3. Luồng 1: Kiểm tra xem _instance có bằng null không –> đúng là bằng null (dòng 7)
4. Luồng 2: Kiểm tra xem _instance có bằng null không –> đúng là bằng null vì _instance vẫn chưa được khởi tạo (dòng 7)
5. Luồng 1: Thực hiện khởi tạo thể hiện của lớp Singleton, gán thể hiện mới tạo ra vào _instance (dòng 8 )
6. Luồng 2: Thực hiện khởi tạo thể hiện của lớp Singleton !!! là thể hiện thứ 2 !!!, gán thể hiện mới tạo ra vào _instance (dòng 8 )
7. Luồng 1: Trả về thể hiện sinh ra ở luồng 2 (dòng 9)
8. Luồng 2: Trả về thể hiện sinh ra ở luồng 2 (dòng 9)

Như vậy là 2 luồng đều nhận được cùng thể hiện nhưng thực chất đã tạo ra 2 thể hiện rồi.
Nếu không may mắn hơn lệnh thứ 7 chạy trước lệnh thứ 6 (do lệnh 6 và 7 ở 2 luồng khác nhau) thì sẽ mỗi luồng trả về một thể hiện –> phá vỡ hoàn toàn định nghĩa về Singleton.

Nguyên nhân lỗi ở đây là kỹ thuật chặn khởi tạo nhiều thể hiện bằng lệnh kiểm tra _instance không có hiệu quả khi chạy đa luồng.
 

Sử dụng object locking

Trong xử lý đa luồng, khóa (lock, synchronized) một object là một thao tác cơ bản dùng để ngăn cản các luồng khác truy cập tới đối tượng (tham chiếu, thuộc tính và phương thức) cho đến khi đối tượng được mở khóa (hết khối lệnh lock). Áp dụng vào bài toán trên, lớp Singleton cần sở hữu một đối tượng làm khóa, khi kiểm tra thấy _instance chưa được khởi tạo thì ngay lập tức khóa lại để thực hiện việc khởi tạo và trả về đối tượng một cách trọn vẹn, không cho các luồng khác xem ngang.
Tôi cài đặt lại lớp Singleton như sau:

 
class Singleton { 
	private static readonly object _obj = new object();
	private static Singleton _instance; 
	
	protected Singleton() { } 
	
	public static Singleton Instance() { 
		if (_instance == null) {
			lock (obj) {
				if (_instance == null) 
					_instance = new Singleton(); 
			}
		}

		return _instance; 
	} 
} 

 

Sử dụng lazy loading

Riêng trong .net v4.0 có hỗ trợ kỹ thuật Lazy loading bằng cách sử dụng lớp System.Lazy. Về bản chất System.Lazy đảm bảo an toàn luồng là do nó cài đặt kỹ thuật object locking ở trên trong nó. Ta khai báo một biến tĩnh kiểu Lazy, truyền vào cho nó hàm sinh thể hiện, khi truy cập đến thể hiện duy nhất thì đơn giản chỉ cần truy cập đến Value của biến Lazy kia:

 
class Singleton { 
	private static readonly Lazy<Singleton> _instance = new Lazy(() => new Singleton()); 
	
	protected Singleton() { } 
	
	public static Singleton Instance() { 
		return _instance.Value; 
	} 
} 

 
Ở đây sử dụng biểu thức lambda: () => new Singleton() để thực hiện khởi tạo được đối tượng vì hàm khởi tạo của lớp Singleton là protected. Khi cần lấy về thể hiện thì chỉ việc truy cập đến Value của _instance, trong lần truy cập đầu tiên, thể hiện của lớp Singleton sẽ được tạo ra (đã được lock). Với Lazy loading, Singleton được cài đặt khá gọn.

Khi trích dẫn bài viết từ tek.eten.vn, xin vui lòng ghi rõ nguồn. Chúng tôi sẽ rất cảm ơn bạn!