Design Pattern: Singleton mở rộng và kỹ thuật tạo pool (phần 3)

Design Pattern: Singleton mở rộng và kỹ thuật tạo pool (phần 3)

Design pattern Singleton hướng dẫn ta cài đặt để chỉ cho phép tạo ra duy nhất một thể hiện của một lớp. Nhưng trong thực tế, đôi khi ta chỉ cần giới hạn số lượng thể hiện chứ không nhất thiết phải bằng một, mà có thể bằng 5, 10 hay 100 … Một ứng dụng tiêu biểu là cài đặt pool, cho phép pool quản lý một hữu hạn các object.
 

Singleton mở rộng: tạo hữu hạn thể hiện của một lớp

Để thực hiện việc tạo hữu hạn thể hiện, thay vì sử dụng một biến static để lưu trữ thể hiện duy nhất, ta dùng một mảng hoặc một danh sách để lưu trữ chúng. Mỗi khi tạo ra thể hiện thì lưu lại tham chiếu của chúng, trước đó kiểm tra xem số lượng thể hiện tạo ra đã đạt tới ngưỡng hay chưa. Việc xử lý khi đạt tới ngưỡng thì tùy thuộc vào từng bài toán cụ thể: có thể là phát ra một exception, trả về đối tượng được sinh ra đầu tiên hoặc trả về đối tượng đang không còn được sử dụng nữa…
Ở đây, để đơn giản tôi sẽ cài đặt mẫu với trường hợp tạo ra exception nếu quá ngưỡng cho phép. Bài toán: Có một hãng taxi có 4 chiếc xe mới và có một số khách hàng chỉ muốn đi taxi mới:

class MainApp {
	private static void Main(string[] args) {
		try {
			while (true) Taxi.GetNewTaxi().Serve();
		} catch (Exception ex) {
			Console.WriteLine(ex.Message);
		}

		Console.ReadLine();
	}
}

class Taxi {
	protected Taxi(string name) { Name = name; }

	public string Name { get; protected set; }

	public void Serve() {
		Console.WriteLine(string.Format("{0} dang don khach!", Name));
	}

	private static readonly List<Taxi> _taxis = new List<Taxi>(4);

	// phương thức cài đặt Singleton mở rộng
	public static Taxi GetNewTaxi() {
		lock (_taxis) {
			if (_taxis.Count == 4) throw new Exception("Het xe moi roi!");
			var car = new Taxi("Taxi so " + (_taxis.Count + 1));
			_taxis.Add(car);
			return car;
		}
	}
}

 

Taxi so 1 dang don khach!
Taxi so 2 dang don khach!
Taxi so 3 dang don khach!
Taxi so 4 dang don khach!
Het xe moi roi!

Nhận xét: với phương thức GetNewTaxi ở trên ta thấy nó có phần nào đó tương đối giống với phương thức sinh trong Factory.
Với ví dụ trên, việc lưu trữ lại tham chiếu của các thể hiện chưa thấy có nhiều ý nghĩa lắm vì ở trên mới chỉ cài đặt cho chức năng giới hạn số thể hiện. Một ví dụ khác cho việc sử dụng danh sách tham chiếu là việc xây dựng object pooling: cho phép giới hạn và quản lý trạng thái các thể hiện.

Kỹ thuật tạo pool

Giả sử bây giờ ta cần giải quyết bài toán thực tế như sau: Công ty taxi A chỉ có hữu hạn N chiếc taxi, công ty chịu trách nhiệm quản lý trạng thái các xe (đang rảnh hay đang chở khách), phân phối các xe đang rảnh đi đón khách, chăm sóc, kéo dài thời gian chờ đợi của khách hàng cho trong trường hợp tất cả các xe đều đang bận (để chờ một trong số các xe đó rảnh thì điều đi đón khách luôn), hủy khi việc chờ đợi của khách hàng là quá lâu.
Ta mô phỏng và thiết kế thành các lớp sau:

  • ITaxi: Là một interface định nghĩa các thuộc tính và phương thức của một taxi. Có thuộc tính Name là tên của taxi, IsAvailable cho biết đang rảnh hay đang chở khách, phương thức Serve mô phỏng việc đón, chở và trả khách, nhận tham số là tên khách hàng. Trong lúc đang chở khách thì IsAvailable là true, trái lại là false
  • Taxi: đại diện cho một chiếc taxi, là lớp implement interface ITaxi
  • TaxiPool: Đại diện cho công ty taxi, có phương thức tĩnh là GetTaxi để lấy về một thể hiện ITaxi đang ở trạng thái rảnh, có thể throw ra một exception nếu chờ lâu mà không lấy được thể hiện

Tôi sẽ cài đặt mô phỏng với TaxiPool quản lý được 4 taxi, cùng lúc có 8 cuộc gọi của khách hàng đến công ty để gọi xe, mỗi taxi chở khách trong khoảng thời gian từ 1000ms đến 1500ms (ngẫu nhiên), mỗi khách hàng chịu chờ tối đa 1400ms trước khi hủy:

class MainApp {
	static void Main(string[] args) {
		for (var i = 0; i < 8; i++) {
			new Thread(TakeATaxi).Start();
		}
		Console.ReadKey();
	}

	static Random rnd = new Random();

	static void TakeATaxi() {
		try {
			// nghỉ một ít cho 8 luồng nó lệch nhau
			Thread.Sleep(rnd.Next(100) + 1);
			// tên khách hàng là ThreadId hiện tại
			TaxiPool.GetTaxi().Serve(Thread.CurrentThread.ManagedThreadId.ToString());
		}
		catch (Exception ex) {
			Console.WriteLine(ex.Message);
		}
	}
}

public class TaxiPool {
	private const int PoolSize = 4; // quản lý 4 xe
	private static readonly List<Taxi> _taxis = new List<Taxi>(PoolSize);

	protected TaxiPool() { }

	public static ITaxi GetTaxi() {
		// tên khách hàng là ThreadId hiện tại
		var customerId = Thread.CurrentThread.ManagedThreadId;
		Console.WriteLine(string.Format("{0:HH:mm:ss.fff}: Customer_{1} take a taxi", DateTime.Now, customerId));

		var times = 0; // số lần thử gọi lại trong thời gian chờ xe
		while (true) {
			lock (_taxis) {
				if (_taxis.Count < PoolSize) {
					// nếu chưa khởi tạo hết thì khởi tạo và trả về
					var t = new Taxi();
					_taxis.Add(t); // thêm xe vào danh sách quản lý
					t.Name = "Taxi " + _taxis.Count;
					t.IsAvailable = false;
					return t;
				}

				// khởi tạo hết rồi thì tìm xem các xe đang rảnh
				foreach (var t in _taxis) {
					if (t.IsAvailable) {
						t.IsAvailable = false;
						return t;
					}
				}
			}
			// nếu hết xe rảnh thì chờ thôi
			Thread.Sleep(140);
			times++;
			// nếu chờ lâu quá thì hủy
			if (times == 10) throw new Exception(string.Format("{0:HH:mm:ss.fff}: Customer_{1} Timeout", DateTime.Now, customerId));
		}
	}

	public interface ITaxi {
		string Name { get; }
		bool IsAvailable { get; }
		void Serve(string customer);
	}

	class Taxi : ITaxi {
		public string Name { get; set; }
		public bool IsAvailable { get; set; }

		private static Random _rnd = new Random();

		public void Serve(string customer) {
			// tính thời gian chở khách
			var servingTime = _rnd.Next(500) + 1000;
			Console.WriteLine(string.Format("{0:HH:mm:ss.fff}: {1} is serving customer_{2}. Will free after {3}ms.", DateTime.Now, Name, customer, servingTime));
			Thread.Sleep(servingTime); // giả lập thời gian trở khách
			// trả khách xong thì đặt lại trạng thái là rảnh
			IsAvailable = true;
			Console.WriteLine("{0:HH:mm:ss.fff}: {1} is free", DateTime.Now, Name);
		}
	}
}

 
Kết quả khi chạy sẽ có dạng như sau:

15:21:04.935: Customer_13 take a taxi
15:21:04.926: Customer_15 take a taxi
15:21:04.939: Taxi 1 is serving customer_13. Will free after 1137ms.
15:21:04.939: Taxi 2 is serving customer_15. Will free after 1091ms.
15:21:04.946: Customer_12 take a taxi
15:21:04.946: Taxi 3 is serving customer_12. Will free after 1230ms.
15:21:04.968: Customer_16 take a taxi
15:21:04.968: Taxi 4 is serving customer_16. Will free after 1410ms.
15:21:04.969: Customer_10 take a taxi
15:21:05.000: Customer_14 take a taxi
15:21:05.011: Customer_11 take a taxi
15:21:05.013: Customer_17 take a taxi
15:21:06.030: Taxi 2 is free
15:21:06.076: Taxi 1 is free
15:21:06.089: Taxi 1 is serving customer_10. Will free after 1013ms.
15:21:06.120: Taxi 2 is serving customer_14. Will free after 1102ms.
15:21:06.176: Taxi 3 is free
15:21:06.271: Taxi 3 is serving customer_11. Will free after 1208ms.
15:21:06.378: Taxi 4 is free
15:21:06.413: Customer_17 Timeout
15:21:07.102: Taxi 1 is free
15:21:07.222: Taxi 2 is free
15:21:07.479: Taxi 3 is free

 
Nhận xét:
Ưu điểm của việc cài đặt Pool là việc tận dụng được các tài nguyên đã được cấp phát. Với ví dụ về taxi ở trên với 4 taxi, trong nhiều trường hợp vẫn có thể đáp ứng được nhiều hơn 4 yêu cầu cùng một lúc. Nó làm tăng hiệu năng hệ thống ở điểm không cần phải khởi tạo quá nhiều thể hiện (trong nhiều trường hợp việc khởi tạo này mấy nhiều thời gian), tận dụng được các tài nguyên đã được khởi tạo (tiết kiệm bộ nhớ, không mất thời gian hủy đối tượng).
Việc cài đặt Pool có thể linh động hơn nữa bằng cách đặt ra 2 giá trị N và M trong đó N là số lượng thể hiện tối thiểu (trong những lúc rảnh rỗi), M là số thể hiện tối đa (lúc cần huy động nhiều thể hiện nhất mà phần cứng đáp ứng được). Sau khi qua trạng thái cần nhiều thể hiện, Pool có thể giải phóng bớt một số thể hiện không cần thiết.

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!