非同步程式設計模式BeginInvoke和EndInvoke方法

兵工廠三劍客發表於2018-07-13

轉載自:非同步程式設計模式BeginInvoke與EndInvoke方法

為什麼要進行非同步回撥?眾所周知,普通方法執行,是單執行緒的,如果中途有大型操作(如:讀取大檔案,大批量運算元據庫,網路傳輸等),都會導致方法阻塞,表現在介面上就是,程式卡或者死掉,介面元素不動了,不響應了。非同步方法很好的解決了這些問題,非同步執行某個方法,程式立即開闢一個新執行緒去執行你的方法,主執行緒包括介面就不會死掉了。非同步呼叫並不是要減少執行緒的開銷, 它的主要目的是讓呼叫方法的主執行緒不需要同步等待在這個函式呼叫上, 從而可以讓主執行緒繼續執行它下面的程式碼.

      BeginInvoke方法可以使用執行緒非同步地執行委託所指向的方法。然後通過EndInvoke方法獲得方法的返回值(EndInvoke方法的返回值就是被呼叫方法的返回值),或是確定方法已經被成功呼叫。當使用BeginInvoke非同步呼叫方法時,如果方法未執行完,EndInvoke方法就會一直阻塞,直到被呼叫的方法執行完畢。


非同步呼叫通用模板


//……
//普通的程式碼:處於同步執行模式
IAsyncResultret=委託變數.BeginInvoke(……); //啟動非同步呼叫
//可以在這一部分幹其他一些事,程式處於非同步執行模式
用於儲存方法結果的變數=委託變數.EndInvoke(ret); //結束非同步呼叫
//普通的程式碼:處於同步執行模式
//……

對照上一篇文章中的計算指定資料夾的容量的例子(例2)
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using System.IO;
  5. namespace AsyncCalculateFolderSize1
  6. {
  7. class Program
  8. {
  9. //計算指定資料夾的總容量
  10. private static long CalculateFolderSize(string FolderName)
  11. {
  12. if (Directory.Exists(FolderName) == false)
  13. {
  14. throw new DirectoryNotFoundException("資料夾不存在");
  15. }
  16. DirectoryInfo RootDir = new DirectoryInfo(FolderName);
  17. //獲取所有的子資料夾
  18. DirectoryInfo[] ChildDirs = RootDir.GetDirectories();
  19. //獲取當前資料夾中的所有檔案
  20. FileInfo[] files = RootDir.GetFiles();
  21. long totalSize = 0;
  22. //累加每個檔案的大小
  23. foreach (FileInfo file in files)
  24. {
  25. totalSize += file.Length;
  26. }
  27. //對每個資料夾執行同樣的計算過程:累加其下每個檔案的大小
  28. //這是通過遞迴呼叫實現的
  29. foreach (DirectoryInfo dir in ChildDirs)
  30. {
  31. totalSize += CalculateFolderSize(dir.FullName);
  32. }
  33. //返回資料夾的總容量
  34. return totalSize;
  35. }
  36. //定義一個委託
  37. public delegate long CalculateFolderSizeDelegate(string FolderName);
  38. static void Main(string[] args)
  39. {
  40. //定義一個委託變數引用靜態方法CalculateFolderSize
  41. CalculateFolderSizeDelegate d = CalculateFolderSize;
  42. Console.WriteLine("請輸入資料夾名稱(例如:C:\\Windows):");
  43. string FolderName = Console.ReadLine();
  44. //通過委託非同步呼叫靜態方法CalculateFolderSize
  45. IAsyncResult ret=d.BeginInvoke(FolderName,null,null);
  46. Console.WriteLine("正在計算中,請耐心等待……");
  47. //阻塞,等到呼叫完成,取出結果
  48. long size = d.EndInvoke(ret);
  49. Console.WriteLine("\n計算完成。資料夾{0}的容量為:{1}位元組\n", FolderName, size);
  50. }
  51. }
  52. }

非同步呼叫的奧祕

非同步呼叫是通過委託來進行的,我們看看是如何定義委託的:
public delegate long CalculateFolderSizeDelegate(string FolderName);
通過Reflactor反編譯結果如下:                                    
  1. public sealed class CalculateFolderSizeDelegate: MulticastDelegate
  2. {
  3. public CalculateFolderSizeDelegate(Object target , intmethodPtr)
  4. { …… }
  5. public virtual long invoke(string FolderName)
  6. { …… }
  7. public virtual IAsyncResult BeginInvoke( string FolderName,
  8. AsyncCallbackcallback , object asyncState)
  9. { …… }
  10. public virtual long EndInvoke( IAsyncResultresult )
  11. { …… }
  12. }
由此我們發現,當我們定義一個委託的時候,實際上是定義了一個委託型別,這個型別有invoke、BeginInvoke()、EndInvoke()這樣幾個成員方法,而這幾個成員方法可以實現一步呼叫機制。我們看看這幾個方法格式怎麼定義的:

(1)BeginInvoke方法用於啟動非同步呼叫


BeginInvoke()的函式宣告:

public IAsyncResult BeginInvoke(

         <輸入和輸出變數>,回撥函式callback , 附加資訊AsyncState)

函式返回值型別:

public interface IAsyncResult

{

        object AsyncState{ get;}  //如果有回撥函式的話該引數用於儲存要傳遞給回撥函式的引數值

        WaitHandle AsyncWaitHandle{ get;}

        bool CompletedSynchronously{ get;}

        bool IsCompleted{ get;} //儲存方法是否執行結束,我們可以通過該屬性的值來判斷非同步方法是否執行結束

}

1.BeginInvoke返回IasyncResult,可用於監視呼叫進度。

2.結果物件IAsyncResult是從開始操作返回的,並且可用於獲取有關非同步開始操作是否已完成的狀態。

3.結果物件被傳遞到結束操作,該操作返回撥用的最終返回值。

4.在開始操作中可以提供可選的回撥。如果提供回撥,在呼叫結束後,將呼叫該回撥;並且回撥中的程式碼可以呼叫結束操作。

5.如果需要將一些額外的資訊傳送給回撥函式,就將其放入BeginInvoke()方法的第3個引數asyncState中。注意到這個引數的型別為Object,所以可以放置任意型別的資料。如果有多個資訊需要傳送給回撥函式,可以將所有要傳送的資訊封狀到一個Struct變數,或者乾脆再定義一個類,將資訊封裝到這個類所建立的物件中,再傳送給BeginInvoke()方法。

(2)EndInvoke方法用於檢索非同步呼叫結果。


方法宣告:

      public <方法返回值型別>EndInvoke(<宣告為ref或out的引數>, IAsyncResult result )

1.result引數由BeginInvoke()方法傳回。.NET藉此以瞭解方法呼叫是否完成。

2.當EndInvoke方法發現非同步呼叫完成時,它取出此非同步呼叫方法的返回值作為其返回值,如果非同步呼叫方法有宣告為ref和out的引數,它也負責填充它。

3.在呼叫BeginInvoke後可隨時呼叫EndInvoke方法,注意:始終在非同步呼叫完成後呼叫EndInvoke.
4.如果非同步呼叫未完成,EndInvoke將一直阻塞到非同步呼叫完成。
5.EndInvoke的引數包括需要非同步執行的方法的out和ref引數以及由BeginInvoke返回的IAsyncResult。


應用例項:


1.使用輪詢等待非同步呼叫完成:使用IAsyncResultIsCompleted屬性來判斷非同步呼叫是否完成

 雖然上面的方法可以很好地實現非同步呼叫,但是當呼叫EndInvoke方法獲得呼叫結果時,整個程式就象死了一樣,依然要等待非同步方法執行結束,這樣做使用者的感覺並不會太  好,因此,我們可以使用 asyncResult來判斷非同步呼叫是否完成,並顯示一些提示資訊。這樣做可以增加使用者體驗。程式碼如下:
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using System.IO;
  5. namespace AsyncCalculateFolderSize2
  6. {
  7. class Program
  8. {
  9. //計算指定資料夾的總容量
  10. private static long CalculateFolderSize(string FolderName)
  11. {
  12. if (Directory.Exists(FolderName) == false)
  13. {
  14. throw new DirectoryNotFoundException("資料夾不存在");
  15. }
  16. DirectoryInfo RootDir = new DirectoryInfo(FolderName);
  17. //獲取所有的子資料夾
  18. DirectoryInfo[] ChildDirs = RootDir.GetDirectories();
  19. //獲取當前資料夾中的所有檔案
  20. FileInfo[] files = RootDir.GetFiles();
  21. long totalSize = 0;
  22. //累加每個檔案的大小
  23. foreach (FileInfo file in files)
  24. {
  25. totalSize += file.Length;
  26. }
  27. //對每個資料夾執行同樣的計算過程:累加其下每個檔案的大小
  28. //這是通過遞迴呼叫實現的
  29. foreach (DirectoryInfo dir in ChildDirs)
  30. {
  31. totalSize += CalculateFolderSize(dir.FullName);
  32. }
  33. //返回資料夾的總容量
  34. return totalSize;
  35. }
  36. //定義一個委託
  37. public delegate long CalculateFolderSizeDelegate(string FolderName);
  38. static void Main(string[] args)
  39. {
  40. //定義一個委託變數引用靜態方法CalculateFolderSize
  41. CalculateFolderSizeDelegate d = CalculateFolderSize;
  42. Console.WriteLine("請輸入資料夾名稱(例如:C:\\Windows):");
  43. string FolderName = Console.ReadLine();
  44. //通過委託非同步呼叫靜態方法CalculateFolderSize
  45. IAsyncResult ret = d.BeginInvoke(FolderName, null, null);
  46. Console.Write ("正在計算中,請耐心等待");
  47. //每隔2秒檢查一次,輸出一個“."
  48. while (ret.IsCompleted == false)
  49. {
  50. Console.Write(".");
  51. System.Threading.Thread.Sleep(200);
  52. }
  53. //阻塞,等到呼叫完成,取出結果
  54. long size = d.EndInvoke(ret);
  55. Console.WriteLine("\n計算完成!\n資料夾{0}的容量為:{1}位元組", FolderName, size);
  56. }
  57. }
  58. }
這樣,當程式在執行CalculateFolderSize這個非同步方法的時候主執行緒並不是“假死”,而是每隔0.2毫秒輸出一個“.",這就是非同步呼叫的妙處!
這裡需要用到BeginInvoke的返回值IAsyncResult的IsCompleted這個屬性來判斷非同步執行緒是否執行結束。

2. 使用輪詢等待非同步呼叫完成:使用IAsyncResultAsyncWaitHandle.WaitOne

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.IO;
  6. namespace AsyncCalculateFolderSize3
  7. {
  8. class Program
  9. {
  10. //計算指定資料夾的總容量
  11. private static long CalculateFolderSize(string FolderName)
  12. {
  13. if (Directory.Exists(FolderName) == false)
  14. {
  15. throw new DirectoryNotFoundException("資料夾不存在");
  16. }
  17. DirectoryInfo RootDir = new DirectoryInfo(FolderName);
  18. //獲取所有的子資料夾
  19. DirectoryInfo[] ChildDirs = RootDir.GetDirectories();
  20. //獲取當前資料夾中的所有檔案
  21. FileInfo[] files = RootDir.GetFiles();
  22. long totalSize = 0;
  23. //累加每個檔案的大小
  24. foreach (FileInfo file in files)
  25. {
  26. totalSize += file.Length;
  27. }
  28. //對每個資料夾執行同樣的計算過程:累加其下每個檔案的大小
  29. //這是通過遞迴呼叫實現的
  30. foreach (DirectoryInfo dir in ChildDirs)
  31. {
  32. totalSize += CalculateFolderSize(dir.FullName);
  33. }
  34. //返回資料夾的總容量
  35. return totalSize;
  36. }
  37. //定義一個委託
  38. public delegate long CalculateFolderSizeDelegate(string FolderName);
  39. static void Main(string[] args)
  40. {
  41. //定義一個委託變數引用靜態方法CalculateFolderSize
  42. CalculateFolderSizeDelegate d = CalculateFolderSize;
  43. Console.WriteLine("請輸入資料夾名稱(例如:C:\\Windows):");
  44. string FolderName = Console.ReadLine();
  45. //通過委託非同步呼叫靜態方法CalculateFolderSize
  46. IAsyncResult ret = d.BeginInvoke(FolderName, null, null);
  47. Console.Write("正在計算中,請耐心等待");
  48. while(!ret.AsyncWaitHandle.WaitOne(2000))
  49. {
  50. //等待2秒鐘,輸出一個“.”
  51. Console.Write(".");
  52. }
  53. //阻塞,等到呼叫完成,取出結果
  54. long size = d.EndInvoke(ret);
  55. Console.WriteLine("\n計算完成。資料夾{0}的容量為:{1}位元組\n", FolderName, size);
  56. }
  57. }
  58. }

 WaitOne的第一個參數列示要等待的毫秒數,在指定時間之內,WaitOne方法將一直等待,直到非同步呼叫完成,併發出通知,WaitOne方法才返回true。當等待指定時間之後,非同步呼叫仍未完成,WaitOne方法返回false,如果指定時間為0,表示不等待,如果為-1,表示永遠等待,直到非同步呼叫完成。

3.使用非同步回撥函式
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using System.IO;
  5. namespace AsyncCalculateFolderSize4
  6. {
  7. class Program
  8. {
  9. //計算指定資料夾的總容量
  10. private static long CalculateFolderSize(string FolderName)
  11. {
  12. if (Directory.Exists(FolderName) == false)
  13. {
  14. throw new DirectoryNotFoundException("資料夾不存在");
  15. }
  16. DirectoryInfo RootDir = new DirectoryInfo(FolderName);
  17. //獲取所有的子資料夾
  18. DirectoryInfo[] ChildDirs = RootDir.GetDirectories();
  19. //獲取當前資料夾中的所有檔案
  20. FileInfo[] files = RootDir.GetFiles();
  21. long totalSize = 0;
  22. //累加每個檔案的大小
  23. foreach (FileInfo file in files)
  24. {
  25. totalSize += file.Length;
  26. }
  27. //對每個資料夾執行同樣的計算過程:累加其下每個檔案的大小
  28. //這是通過遞迴呼叫實現的
  29. foreach (DirectoryInfo dir in ChildDirs)
  30. {
  31. totalSize += CalculateFolderSize(dir.FullName);
  32. }
  33. //返回資料夾的總容量
  34. return totalSize;
  35. }
  36. public delegate long CalculateFolderSizeDelegate(string FolderName);
  37. private static CalculateFolderSizeDelegate task = CalculateFolderSize;
  38. //用於回撥的函式
  39. public static void ShowFolderSize(IAsyncResult result)
  40. {
  41. long size = task.EndInvoke(result);
  42. Console.WriteLine("\n資料夾{0}的容量為:{1}位元組\n", (String)result.AsyncState, size);
  43. }
  44. static void Main(string[] args)
  45. {
  46. string FolderName;
  47. while (true)
  48. {
  49. Console.WriteLine("請輸入資料夾名稱(例如:C:\\Windows),輸入quit結束程式");
  50. FolderName = Console.ReadLine();
  51. if (FolderName == "quit")
  52. break;
  53. task.BeginInvoke(FolderName, ShowFolderSize, FolderName);//第一個引數是非同步函式的引數,第二個引數是回撥函式,第三個引數是回撥函式的引數,回撥函式會在非同步函式執行結束之後被呼叫。
  54. }
  55. }
  56. }
  57. }
這個例子中通過迴圈的輸入資料夾名稱計算資料夾容量,計算的操作放在非同步呼叫函式中,因此我們在輸入下一個資料夾名稱時不必等待上一個計算結束,非同步函式執行完成之後會自動呼叫回撥函式ShowFolderSize進行結果處理。
(今天就學到這裡,下午去嘉禾看期待已久的3D《復仇者聯盟》,吼吼......)
2012/5/20 23:05補充

對於上面最後一個非同步回撥的例子有一個缺陷,就是當非同步呼叫的函式與主執行緒都需要訪問同一資源時,要注意解決資源共享的問題。如下圖:

修改程式如下:
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using System.IO;
  5. namespace AsyncCalculateFolderSize6
  6. {
  7. class Program
  8. {
  9. //計算指定資料夾的總容量
  10. private static long CalculateFolderSize(string FolderName)
  11. {
  12. if (Directory.Exists(FolderName) == false)
  13. {
  14. throw new DirectoryNotFoundException("資料夾不存在");
  15. }
  16. DirectoryInfo RootDir = new DirectoryInfo(FolderName);
  17. //獲取所有的子資料夾
  18. DirectoryInfo[] ChildDirs = RootDir.GetDirectories();
  19. //獲取當前資料夾中的所有檔案
  20. FileInfo[] files = RootDir.GetFiles();
  21. long totalSize = 0;
  22. //累加每個檔案的大小
  23. foreach (FileInfo file in files)
  24. {
  25. totalSize += file.Length;
  26. }
  27. //對每個資料夾執行同樣的計算過程:累加其下每個檔案的大小
  28. //這是通過遞迴呼叫實現的
  29. foreach (DirectoryInfo dir in ChildDirs)
  30. {
  31. totalSize += CalculateFolderSize(dir.FullName);
  32. }
  33. //返回資料夾的總容量
  34. return totalSize;
  35. }
  36. //定義一個委託
  37. public delegate long CalculateFolderSizeDelegate(string FolderName);
  38. private static CalculateFolderSizeDelegate d = new CalculateFolderSizeDelegate(CalculateFolderSize);
  39. //用於回撥的函式
  40. public static void ShowFolderSize(IAsyncResult result)
  41. {
  42. try
  43. {
  44. long size = d.EndInvoke(result);
  45. while (Console.CursorLeft != 0)//只有使用者不輸入,且游標位於第一列時,才輸出資訊。
  46. {
  47. //等待2秒
  48. System.Threading.Thread.Sleep(2000);
  49. }
  50. Console.WriteLine("\n資料夾{0}的容量為:{1}位元組\n", (String)result.AsyncState, size);
  51. }
  52. catch (DirectoryNotFoundException e)
  53. {
  54. Console.WriteLine("您輸入的資料夾不存在");
  55. }
  56. }
  57. static void Main(string[] args)
  58. {
  59. string FolderName;
  60. while (true)
  61. {
  62. Console.WriteLine("請輸入資料夾名稱(例如:C:\\Windows),輸入quit結束程式");
  63. FolderName = Console.ReadLine();
  64. if (FolderName == "quit")
  65. break;
  66. d.BeginInvoke(FolderName, ShowFolderSize, FolderName);
  67. }
  68. }
  69. }
  70. }

個人程式碼:

    class Program
    {
        public delegate string MyTestDelegate(string str);
        static void Main(string[] args)
        {
            string param = "123";
            MyTestDelegate d = Method;
            AsyncCallback callBkcak = MethodCallKback;
            IAsyncResult result = d.BeginInvoke(param, callBkcak, param);
            string returnValue = d.EndInvoke(result);
            Console.WriteLine(returnValue);
            Console.ReadKey();
        }

        private static string Method(string str)
        {
            Console.WriteLine("非同步操作開始了!" + "你傳入的引數是:" + str);
            return "非同步操作反回了一段話給你!";

        }

        private static void MethodCallKback(IAsyncResult result)
        {
            Console.WriteLine("回撥函式傳入引數:" + (string)result.AsyncState);
            Console.WriteLine("非同步操作結束了!");
        }

    }
以下是輸出內容:



相關文章