非同步調用
無需返回值即可進行操作的方法
asynchronous call(非同步調用)
一個可以無需等待被調用函數的返回值就讓操作繼續進行的方法
非同步調用就是你 喊 你朋友吃飯,你朋友說知道了,待會忙完去找你,你就去做別的了。
同步調用就是你 喊 你朋友吃飯,你朋友在忙,你就一直在那等,等你朋友忙完了,你們一起去。
操作系統發展到今天已經十分精巧,線程就是其中一個傑作。操作系統把 CPU 處理時間劃分成許多短暫時間片,在時間 T1 執行一個線程的指令,到時間 T2又執行下一線程的指令,各線程輪流執行,結果好象是所有線程在並肩前進。這樣,編程時可以創建多個線程,在同一期間執行,各線程可以“并行”完成不同的任務。
在單線程方式下,計算機是一台嚴格意義上的馮·諾依曼式機器,一段代碼調用另一段代碼時,只能採用 同步調用,必須等待這段代碼執行完返回結果后,調用方才能繼續往下執行。有了多線程的支持,可以採用非同步調用,調用方和被調方可以屬於兩個不同的線程,調用方啟動被調方線程后,不等對方返回結果就繼續執行後續代碼。被調方執行完畢后,通過某種手段通知調用方:結果已經出來,請酌情處理。
計算機中有些處理比較耗時。調用這種處理代碼時,調用方如果站在那裡苦苦等待,會嚴重影響程序性能。例如,某個程序啟動后如果需要打開文件讀出其中的數據,再根據這些數據進行一系列初始化處理,程序主窗口將遲遲不能顯示,讓用戶感到這個程序怎麼等半天也不出來,太差勁了。藉助非同步調用可以把問題輕鬆化解:把整個初始化處理放進一個單獨線程,主線程啟動此線程後接著往下走,讓主窗口瞬間顯示出來。等用戶盯著窗口犯呆時,初始化處理就在背後悄悄完成了。程序開始穩定運行以後,還可以繼續使用這種技巧改善人機交互的瞬時反應。用戶點擊滑鼠時,所激發的操作如果較費時,再點擊滑鼠將不會立即反應,整個程序顯得很沉重。藉助非同步調用處理費時的操作,讓主線程隨時恭候下一條消息,用戶點擊滑鼠時感到輕鬆快捷,肯定會對軟體產生好感。
非同步調用用來處理從外部輸入的數據特別有效。假如計算機需要從一台低速設備索取數據,然後是一段冗長的數據處理過程,採用 同步調用顯然很不合算:計算機先向外部設備發出請求,然後等待數據輸入;而外部設備向計算機發送數據后,也要等待計算機完成數據處理后再發出下一條數據請求。雙方都有一段等待期,拉長了整個處理過程。其實,計算機可以在處理數據之前先發出下一條數據請求,然後立即去處理數據。如果數據處理比數據採集快,要等待的只有計算機,外部設備可以連續不停地採集數據。如果計算機同時連接多台輸入設備,可以輪流向各台設備發出數據請求,並隨時處理每台設備發來的數據,整個系統可以保持連續高速運轉。編程的關鍵是把數據索取代碼和數據處理代碼分別歸屬兩個不同的線程。數據處理代碼調用一個數據請求非同步函數,然後徑自處理手頭的數據。待下一組數據到來后,數據處理線程將收到通知,結束 wait 狀態,發出下一條數據請求,然後繼續處理數據。
非同步調用時,調用方不等被調方返回結果就轉身離去,因此必須有一種機制讓被調方有了結果時能通知調用方。在同一進程中有很多手段可以利用,筆者常用的手段是回調、互斥對象和消息。
回調方式很簡單:調用非同步函數時在參數中放入一個函數地址,非同步函數保存此地址,待有了結果后回調此函數便可以向調用方發出通知。如果把非同步函數包裝進一個對象中,可以用事件取代回調函數地址,通過事件處理常式向調用方發通知。
mutex是 Windows系統提供的一個常用同步對象,以在非同步處理中對齊不同線程之間的步點。如果調用方暫時無事可做,可以調用 wait 函數等在那裡,此時 mutex處於 nonsignaled 狀態。當被調方出來結果之後,把 mutex對象置於 signaled 狀態,wait函數便自動結束等待,使調用方重新動作起來,從被調方取出處理結果。這種方式比回調方式要複雜一些,速度也相對較慢,但有很大的靈活性,可以搞出很多花樣以適應比較複雜的處理系統。
藉助 Windows消息發通知是個不錯的選擇,既簡單又安全。程序中定義一個用戶消息,並由調用方準備好消息處理常式。被調方出來結果之後立即向調用方發送此消息,並通過WParam 和 LParam 這兩個參數傳送結果。消息總是與窗口 handle關聯,因此調用方必須藉助一個窗口才能接收消息,這是其不方便之處。另外,通過消息聯絡會影響速度,需要高速處理時回調方式更有優勢。
如果調用方和被調方分屬兩個不同的進程,由於內存空間的隔閡,一般是採用 Windows消息發通知比較簡單可靠,被調方可以藉助消息本身向調用方傳送數據。event對象也可以通過名稱在不同進程間共享,但只能發通知,本身無法傳送數據,需要藉助 Windows 消息和 FileMapping等內存共享手段或藉助 MailSlot 和 Pipe 等通信手段。
非同步調用原理並不複雜,但實際使用時容易出莫名其妙的問題,特別是不同線程共享代碼或共享數據時容易出問題,編程時需要時時注意是否存在這樣的共享,並通過各種狀態標誌避免衝突。Windows 系統提供的 mutex 對象用在這裡特別方便。mutex同一時刻只能有一個管轄者。一個線程放棄管轄權后,另一線程才能接管。當某線程執行到敏感區之前先接管 mutex,使其他線程被 wait函數堵在身後;脫離敏感區之後立即放棄管轄權,使 wait函數結束等待,另一個線程便有機會光臨此敏感區。這樣就可以有效避免多個線程進入同一敏感區。
由於非同步調用容易出問題,要設計一個安全高效的編程方案需要比較多的設計經驗,所以最好不要濫用非同步調用。同步調用畢竟讓人更舒服些:不管程序走到哪裡,只要死盯著移動點就能心中有數,不至於象非同步調用那樣,總有一種四面受敵、惶惶不安的感覺。必要時甚至可以把非同步函數轉換為同步函數。方法很簡單:調用非同步函數后馬上調用 wait 函數等在那裡,待非同步函數返回結果后再繼續往下走。
四個示例全部使用同一個長期運行的測試方法 TestMethod。該方法顯示一個表明它已開始處理的控制台信息,休眠幾秒鐘,然後結束。TestMethod 有一個 out 參數(在 Visual Basic 中為 ByRef),它演示了如何將這些參數添加到 BeginInvoke 和 EndInvoke 的簽名中。您可以用類似的方式處理 ref 參數(在 Visual Basic 中為 ByRef)。
下面的代碼示例顯示 TestMethod 以及代表 TestMethod 的委託;若要使用任一示例,請將示例代碼追加到這段代碼中。
注意 為了簡化這些示例,TestMethod 在獨立於 Main() 的類中聲明。或者,TestMethod 可以是包含 Main() 的同一類中的 static 方法(在 Visual Basic 中為 Shared)。
非同步執行方法的最簡單方式是以 BeginInvoke 開始,對主線程執行一些操作,然後調用 EndInvoke。EndInvoke 直到C#非同步調用完成後才返回。這種技術非常適合文件或網路操作,但是由於它阻塞 EndInvoke,所以不要從用戶界面的服務線程中使用它。
等待 WaitHandle 是一項常用的線程同步技術。您可以使用由 BeginInvoke 返回的 IAsyncResult 的 AsyncWaitHandle 屬性來獲取 WaitHandle。C#非同步調用完成時會發出 WaitHandle 信號,而您可以通過調用它的 WaitOne 等待它。
如果您使用 WaitHandle,則在C#非同步調用完成之後,但在通過調用 EndInvoke 檢索結果之前,可以執行其他處理。
您可以使用由 BeginInvoke 返回的 IAsyncResult 的 IsCompleted 屬性來發現C#非同步調用何時完成。從用戶界面的服務線程中進行C#非同步調用時可以執行此操作。輪詢完成允許用戶界麵線程繼續處理用戶輸入。