Introduction

Trong phần 2 của series bài viết này, tôi sẽ trình bày sâu hơn về cách sử dụng delegate thật linh hoạt, mang lại hiệu quả tối ưu. Nếu bạn chưa đọc bài viết đầu tiên trong series này, tôi khuyên bạn hãy làm một việc – đọc bài viết đó tại đây.

Ở phần này, tôi muốn thảo luận về những cách khác có thể để gọi một function thông qua delegate. Nội dung chính mà tôi muốn xoáy vào đó là làm thế nào để gọi một delegate không đồng thời. Việc này đòi hỏi một vài kĩ thuật nâng cao, bởi vì nó sẽ tạo ra một thread khác. Tôi hi vọng sau khi đã hiểu kĩ về nội dung của bài viết này, bạn có thể tự tin sử dụng nó trong các ứng dụng của bạn.


Life Problem

Trong bài viết này, tôi muốn đưa những vấn đề thực sự trong cuộc sống vào làm ví dụ, vì theo tôi nghĩ nó sẽ dễ hiểu hơn. Vấn đề của chúng ta hôm nay như sau:

Tối hôm nay, tôi sẽ mời một vài người bạn của tôi đến tham dự một bữa tiệc, và tôi sẽ cần phải chuẩn bị một số thứ để bữa tiệc có thể diễn ra. Sau đây là danh sách những công việc cần thiết:

  1. Dọn phòng
  2. Làm một ít thức ăn
  3. Chuẩn bị bàn
  4. Tắm rửa, thay quần áo…

Những việc này cũng không nặng nhọc lắm, nhưng khổ một nỗi là tôi không biết nấu ăn. Vì vậy tôi cần phải tìm một cách nào đó thôi😀. Một giải pháp đó là order cho một nhà hàng nào đó qua điện thoại. Nhà hàng mà tôi chọn có một vài điểm đặc biệt. Nếu muốn, họ có thể chuẩn bị thức ăn ngay tại nhà của bạn. Họ sẽ chuyển nguyên liệu tới nhà bạn và nấu ăn tại đó. Vậy là bạn có thể xem người ta nấu ăn trực tiếp rồi. Một lựa chọn khác là bạn có thể yêu cầu họ làm sẵn tại nhà hàng, sau đó chuyển thức ăn đã làm xong tới cho bạn.

Program Model

Bây giờ, chúng ta sẽ giả lập tình huống này thành một chương trình.

Đầu tiên, tôi tạo class Restaurant với một phương thức tĩnh MakeFood, nó sẽ nhận order dưới dạng string, đồng thời return một chuỗi khi công việc đã xong. Quá trình này có thể tốn nhiều thời gian, do đó tôi sử dụng thêm phương thức Thread.Sleep:

public class Restaurant
{
	/// <summary>
	/// Static function excepts an order and delivers some food
	/// </summary>
	/// <param name="order">
	/// <returns>
	public static string MakeFood(string order)
	{
		//register start:
		Console.WriteLine("Making {0} started at {1}", order, 
				DateTime.Now.ToLongTimeString());   //food preparation:
		Thread.Sleep(4000);   //register finish
		Console.WriteLine("Making {0} finished at {1}", order, 
				DateTime.Now.ToLongTimeString());   //deliver:
		return order.ToUpper() + " made";
	}
}

Tiếp theo, class PartyHost bao gồm một vài phương thức public, đó là các công việc phải làm trước khi buổi tiệc bắt đầu. Tất cả các function đều được xác định rõ thời điểm bắt đầu và kết thúc. Tôi cũng thêm một vài khoảng thời gian chờ đợi nhằm giải lập thời gian thực mà chúng ta cần để hoàn thành công việc. Class này có function MakeFood, thay vào đó, một delegate có tên là Restaurant sẽ gọi function MakeFood. Trong thực tế, delegate chính là cái điện thoại tôi dùng để order thức ăn.

public class PartyHost
{
	/// <summary>
	/// Person Name
	/// </summary>
	public string Name { get; private set; }   /// <summary>
	/// constructor
	/// </summary>
	/// <param name="name">
	public PartyHost(string name)
	{
		this.Name = name;
	}   /// <summary>
	/// Clean up place for party
	/// </summary>
	public void CleanUpPlace()
	{
		//register start:
		Console.WriteLine("Cleaning  started at {0}", 
				DateTime.Now.ToLongTimeString());   //cleaning:
		Thread.Sleep(3000);   //register end:
		Console.WriteLine("Cleaning  finished at {0}", 
				DateTime.Now.ToLongTimeString());
	}   /// <summary>
	/// Set up furniture for the party
	/// </summary>
	public void SetupFurniture()
	{
		//register start:
		Console.WriteLine("Furniture setup  started at {0}" , 
				DateTime.Now.ToLongTimeString());   //setting up:
		Thread.Sleep(2000);   //register end:
		Console.WriteLine("Furniture setup  finished at {0}", 
				DateTime.Now.ToLongTimeString());
	}   /// <summary>
	/// Take your time
	/// </summary>
	public void TakeBathAndDressUp()
	{
		//register start:
		Console.WriteLine("TakeBathAndDressUp  started at {0}", 
				DateTime.Now.ToLongTimeString());   //having fun:
		Thread.Sleep(2000);   //register end:
		Console.WriteLine("TakeBathAndDressUp  finished at {0}", 
				DateTime.Now.ToLongTimeString());
	}   /// <summary>
	/// Get restaurant's phone number
	/// </summary>
	public OrderHandle GetRestaurantPhoneNumber()
	{
		//find the restaurant's phone number in the phone book:
		OrderHandle phone = new OrderHandle(Restaurant.MakeFood);   return phone;
	}
}

Bốn class tiếp theo là chương trình chính. Tất cả đều có hàm Main riêng. Nếu bạn để các class này vào cùng một project, bạn cần phải xác định hàm Main nào bạn muốn gọi trước khi chạy chương trình. Dưới đây là đoạn chương trình gọi delegate trực tiếp bằng phương thức Invoke. Cách gọi này chính là synchronous call.

public class Sync
{
	/// <summary>
	/// Invoking directly
	/// </summary>
	/// <param name="args">
	static void Main(string[] args)
	{
		//mark start time:
		long start = DateTime.Now.Ticks;   //initialize host
		PartyHost host = new PartyHost("Ed");   //find a restaurant phone number in phone book;
		//it is your delegate:
		OrderHandle restaurantPhone = host.GetRestaurantPhoneNumber();   //clean the place:
		host.CleanUpPlace();   //set up furniture
		host.SetupFurniture();   //call food chef into your apartment to prepare food
		//the chef will come and will make the food at your place
		string getFood = restaurantPhone.Invoke("sushi");   //register when food is done:
		Console.WriteLine(getFood + " at " + DateTime.Now.ToLongTimeString());   //prepare yourself:
		host.TakeBathAndDressUp();   //mark end time:
		long end = DateTime.Now.Ticks;   //register total time:
		Console.WriteLine("Total time {0} seconds", (end - start) / 10000000);   Console.Read();
	}
}

Hình ảnh dưới đây mô tả quá trình đoạn chương trình trên:

Khi bạn sử dụng lời gọi hàm đồng thời (synchronous call), bạn ra lệnh cho compiler thực thi hàm mà delegate trỏ tới ngay trên thread của hàm Main. Nó tương tự như việc tôi mời một đầu bếp tới nhà tôi để làm đồ ăn vậy. Mọi công việc diễn ra theo một trình tự. Công việc tiếp theo chỉ được thực hiện sau khi công việc trước đó kết thúc. Ở đây, chương trình mất 11s để hoàn thành công việc.

Trường hợp thứ hai đó là ta sẽ thực hiện asynchronous call. Thay vì gọi phương thức Invoke, chương trình sẽ gọi phương thức BeginInvoke. Có một chút khác biệt trong việc gọi hàm:

IAsyncResult asyncResult = restaurantPhone.BeginInvoke("sushi", null, null); 
public class AsyncNoCallBackBadTiming
{
	/// <summary>
	/// Invoking Async No Call Back Bad Timing
	/// </summary>
	/// <param name="args">
	static void Main(string[] args)
	{
		//mark start:
		long start = DateTime.Now.Ticks;   //initialize host:
		PartyHost host = new PartyHost("Ed");   //find a restaurant phone number in phone book;
		//it is your delegate:
		OrderHandle restaurantPhone = host.GetRestaurantPhoneNumber();   //clean up:
		host.CleanUpPlace();   //set furniture:
		host.SetupFurniture();   //call the restaurant to order food and get a token 
		//from them identifying 
		//your order. they immediately start preparing food:
		IAsyncResult asyncResult = restaurantPhone.BeginInvoke
					("sushi", null, null);   string getFood;   //call the restaurant, provide the token, get the food, 
		//don't be surprised that
		//you have to wait until the food is ready.
		getFood = restaurantPhone.EndInvoke(asyncResult);   Console.WriteLine(getFood + " at " + DateTime.Now.ToLongTimeString());   //take care of yourself:
		host.TakeBathAndDressUp();   //mark the end:
		long end = DateTime.Now.Ticks;   //register total time:
		Console.WriteLine("Total time {0} seconds", (end - start) / 10000000);   Console.Read();
	}
}

Hình ảnh dưới đây sẽ thể hiện những gì diễn ra trong đoạn chương trình trên:

Khi sử dụng asynchronous call, bạn ra lệnh cho compiler lấy một thread từ thread pool (hoặc tạo một thread mới). Thay vì chuẩn bị thức ăn tại nhà tôi, tôi yêu cầu nhà hàng làm thức ăn tại chỗ của họ, sau đó mang đến cho tôi. Công việc lúc này được thực hiện ở một nơi khác. Và nhờ đó, main thread không cần phải chờ cho đến khi function hoàn thành. Nó có thể tiếp túc thực hiện các chức năng khác.

Tuy nhiên, ở đây tôi lại gọi phương thức EndInvoke ngay sau khi gọi phương thức BeginInvoke. Vì vậy, main thread sẽ đợi cho đến khi EndInvoke hoàn thành. Phương thức này chỉ kết thúc khi function MakeFood kết thúc. Việc này sẽ block main thread lại và tất cả những lợi ích của việc sử dụng asynchronous call trở nên vô nghĩa. Thời gian tổng cộng vẫn là 11 giây.

Trong trường hợp thứ 3, tôi sẽ sửa lại lỗi này. Bây giờ, tôi sẽ order trước. Trong khi thức ăn được chuẩn bị, tôi có thể làm một vài việc khác, ví dụ như dọn nhà, kê bàn ghế… Như vậy, phương thức BeginInvoke được gọi, main thread lúc này quay trở lại thực thi tiếp công việc của nó. Cho đến khi phương thức EndInvoke được gọi tức là thức ăn đã sẵn sàng. Bây giờ, tổng thời gian chỉ là 7s:

 

public class AsyncNoCallBackGoodTiming
{
    /// <summary>
	/// Invoking Async No Call Back Good Timing
	/// </summary>
	/// <param name="args">
	static void Main(string[] args)
	{
		//mark start:
		long start = DateTime.Now.Ticks;   //initialize host:
		PartyHost host = new PartyHost("Ed");   //find a restaurant phone number in phone book;
		//it is your delegate:
		OrderHandle restaurantPhone = host.GetRestaurantPhoneNumber();   //call the restaurant to order food and get a token from them 
		//identifying your order. they immediately start preparing food:
		IAsyncResult asyncResult = restaurantPhone.BeginInvoke
					("sushi", null, null);   //clean up:
		host.CleanUpPlace();   //set furniture:
		host.SetupFurniture();   string getFood;   //call the restaurant, provide the token, get the food
		getFood = restaurantPhone.EndInvoke(asyncResult);   Console.WriteLine(getFood + " at " + DateTime.Now.ToLongTimeString());   //take care of yourself:
		host.TakeBathAndDressUp();   //mark the end:
		long end = DateTime.Now.Ticks;   //register total time:
		Console.WriteLine("Total time {0} seconds", (end - start) / 10000000);   Console.Read();
	}
}

Đây là hình ảnh mô tả quá trình diễn ra trong đoạn chương trình trên:

Trường hợp cuối cùng sẽ giải thích cho bạn cách sử dụng CallBack function. Như tôi đã đề cập từ trước, phương thức BeginInvoke có thêm 2 tham số phía sau mà trong các trường hợp trên chúng ta chưa dùng tới. Đầu tiên đó là delegate CallBackFunction. Ở đây tôi sử dụng một static function:

private static void FoodDeliveryNotification(IAsyncResult result){ }

Phương thức này nhận tham số có kiểu IAsyncResult và return void. Bởi vì tham số truyền vào phương thức BeginInvoke là một delegate, do đó tôi sử dụng từ khóa new để tạo một thể hiện mới cho delegate, đồng thời trỏ delegate này tới phương thức FoodDeliveryNotification:

restaurantPhone.BeginInvoke("sushi", 
	new AsyncCallback(FoodDeliveryNotification), restaurantPhone);  

Tham số thứ hai trong phương thức BeginInvoke là một object và tôi có thể truyền bất kì object nào mà tôi muốn vào đây. Trong trường hợp này, tôi gọi lại chính delegate restaurantPhone. Như chúng ta đã biết, phương thức MakeFood được thực thi trên một thread khác. Khi function này kết thúc, nó sẽ kích hoạt hàm callback FoodDeliveryNotification. Một vài công viecj sẽ được thực thi bên trong hàm này:

  1. Nhận việc gọi delegate
  2. Gọi phương thức EndInvoke và truyền tham số IasyncResult vào.
  3. Xử lí kết quả trả về.

Trong trường hợp này, đoạn chương trình bên trong hàm main không trực tiếp gọi phương thức EndInvoke.

public class AsyncCallBack
{
        /// <summary>
	/// Invoking Async Call Back
	/// </summary>
	/// <param name="args">
	static void Main(string[] args)
	{
		//mark start:
		long start = DateTime.Now.Ticks;   //initialize host:
		PartyHost host = new PartyHost("Ed");   //find a restaurant phone number in phone book;
		//it is your delegate:
		OrderHandle restaurantPhone = host.GetRestaurantPhoneNumber();   //call the restaurant to order food and get a token 
		//from them identifying your order. 
		//They immediately start preparing food:
		restaurantPhone.BeginInvoke("sushi", new AsyncCallback
			(FoodDeliveryNotification), restaurantPhone);   //clean up:
		host.CleanUpPlace();   //set furniture:
		host.SetupFurniture();   //take care of yourself:
		host.TakeBathAndDressUp();   //mark the end:
		long end = DateTime.Now.Ticks;   //register total time:
		Console.WriteLine("Total time {0} seconds", (end - start) / 10000000);   Console.Read();
	}   private static void FoodDeliveryNotification(IAsyncResult result)
	{
		//get the delegate:
		OrderHandle handle = (OrderHandle)result.AsyncState;   //call end invoke:
		string getFood = handle.EndInvoke(result);   Console.WriteLine(getFood + " at " + DateTime.Now.ToLongTimeString());
	}
}
}

Summary

Ở bài viết này, bạn đã biết cách gọi một delegate không cùng lúc. Mặc dù bạn biết cách sử dụng nhưng bạn cũng không cần thiết phải sử dụng nó. Thông thường, nó hay được sử dụng khi delegate call một chức năng I/O nào đó. Tuy nhiên, dựa vào những đánh giá về cách đặt các lời gọi hàm trong chương trình của bạn để có thể sử dụng chức năng này đúng đắn. Nếu sử dụng không đúng, bạn có thể làm chậm chương trình của mình và tất nhiên nó sẽ chẳng mang lại lợi ích nào cả.

Nguyên bản: http://www.codeproject.com/KB/cs/delegatespart2.aspx