同步函數與異步函數

文章出處:http://blog.163.com/lyzaily@126/blog/static/42438837200952751954922/

 

1. 同步函數與異步函數

        什麼是同步函數?

        什麼是異步函數?

        它們在線程中執行時會對線程有何影響?

        一個線程如何與一個異步執行的函數進行同步?

依據微軟的MSDN上的解說:

(1)   同步函數:當一個函數是同步執行時,那麼當該函數被調用時不會立即返回,直到該函數所要做的事情全都做完了才返回。

(2)   異步函數:如果一個異步函數被調用時,該函數會立即返回儘管該函數規定的操作任務還沒有完成。

(3) 在一個線程中分別調用上述兩種函數會對調用線程有何影響呢?

        當一個線程調用一個同步函數時(例如:該函數用於完成寫文件任務),如果該函數沒有立即完成規定的操作,則該操作會導致該調用線程的掛起將CPU的使用權交給系統,讓系統分配給其他線程使用),直到該同步函數規定的操作完成才返回,最終才能導致該調用線程被重新調度。

         當一個線程調用的是一個異步函數(例如:該函數用於完成寫文件任務),該函數會立即返回儘管其規定的任務還沒有完成,這樣線程就會執行異步函數的下一條語句,而不會被掛起。那麼該異步函數所規定的工作是如何被完成的呢?當然是通過另外一個線程完成的了啊;那麼新的線程是哪裏來的呢?可能是在異步函數中新創建的一個線程也可能是系統中已經準備好的線程

(4)一個調用了異步函數的線程如何與異步函數的執行結果同步呢?

        爲了解決該問題,調用線程需要使用“等待函數”來確定該異步函數何時完成了規定的任務。因此在線程調用異步函數之後立即調用一個“等待函數”掛起調用線程,一直等到異步函數執行完其所有的操作之後,再執行線程中的下一條指令。

我們是否已經發現了一個有趣的地方呢?!就是我們可以使用等待函數將一個異步執行的函數封裝成一個同步函數。

 

2.同步調用與異步調用

        操作系統發展到今天已經十分精巧,線程就是其中一個傑作。操作系統把 CPU 處理時間劃分成許多短暫時間片,在時間 T1 執行一個線程的指令,到時間 T2 又執行下一線程的指令,各線程輪流執行,結果好象是所有線程在並肩前進。這樣,編程時可以創建多個線程,在同一期間執行,各線程可以“並行”完成不同的任務。
        在單線程方式下,計算機是一臺嚴格意義上的馮·諾依曼式機器,一段代碼調用另一段代碼時,只能採用同步調用,必須等待這段代碼執行完返回結果後,調用方纔能繼續往下執行。有了多線程的支持,可以採用異步調用,調用方和被調方可以屬於兩個不同的線程,調用方啓動被調方線程後,不等對方返回結果就繼續執行後續代碼。被調方執行完畢後,通過某種手段通知調用方:結果已經出來,請酌情處理。
        計算機中有些處理比較耗時。調用這種處理代碼時,調用方如果站在那裏苦苦等待,會嚴重影響程序性能。例如,某個程序啓動後如果需要打開文件讀出其中的數據,再根據這些數據進行一系列初始化處理,程序主窗口將遲遲不能顯示,讓用戶感到這個程序怎麼等半天也不出來,太差勁了。藉助異步調用可以把問題輕鬆化解:把整個初始化處理放進一個單獨線程,主線程啓動此線程後接着往下走,讓主窗口瞬間顯示出來。等用戶盯着窗口犯呆時,初始化處理就在背後悄悄完成了。程序開始穩定運行以後,還可以繼續使用這種技巧改善人機交互的瞬時反應。用戶點擊鼠標時,所激發的操作如果較費時,再點擊鼠標將不會立即反應,整個程序顯得很沉重。藉助異步調用處理費時的操作,讓主線程隨時恭候下一條消息,用戶點擊鼠標時感到輕鬆快捷,肯定會對軟件產生好感。
        異步調用用來處理從外部輸入的數據特別有效。假如計算機需要從一臺低速設備索取數據,然後是一段冗長的數據處理過程,採用同步調用顯然很不合算:計算機先向外部設備發出請求,然後等待數據輸入;而外部設備向計算機發送數據後,也要等待計算機完成數據處理後再發出下一條數據請求。雙方都有一段等待期,拉長了整個處理過程。其實,計算機可以在處理數據之前先發出下一條數據請求,然後立即去處理數據。如果數據處理比數據採集快,要等待的只有計算機,外部設備可以連續不停地採集數據。如果計算機同時連接多臺輸入設備,可以輪流向各臺設備發出數據請求,並隨時處理每臺設備發來的數據,整個系統可以保持連續高速運轉。編程的關鍵是把數據索取代碼和數據處理代碼分別歸屬兩個不同的線程。數據處理代碼調用一個數據請求異步函數,然後徑自處理手頭的數據。待下一組數據到來後,數據處理線程將收到通知,結束 wait 狀態,發出下一條數據請求,然後繼續處理數據。
        異步調用時,調用方不等被調方返回結果就轉身離去,因此必須有一種機制讓被調方有了結果時能通知調用方。在同一進程中有很多手段可以利用,筆者常用的手段是回調、event 對象和消息。
        回調:
回調方式很簡單:調用異步函數時在參數中放入一個函數地址,異步函數保存此地址,待有了結果後回調此函數便可以向調用方發出通知。如果把異步函數包裝進一個對象中,可以用事件取代回調函數地址,通過事件處理例程向調用方發通知。
  event : event 是 Windows 系統提供的一個常用同步對象,以在異步處理中對齊不同線程之間的步點。如果調用方暫時無事可做,可以調用 wait 函數等在那裏,此時 event 處於 nonsignaled 狀態。當被調方出來結果之後,把 event 對象置於 signaled 狀態,wait 函數便自動結束等待,使調用方重新動作起來,從被調方取出處理結果。這種方式比回調方式要複雜一些,速度也相對較慢,但有很大的靈活性,可以搞出很多花樣以適應比較複雜的處理系統。

        消息:藉助 Windows 消息發通知是個不錯的選擇,既簡單又安全。程序中定義一個用戶消息,並由調用方準備好消息處理例程。被調方出來結果之後立即向調用方發送此消息,並通過 WParam 和 LParam 這兩個參數傳送結果。消息總是與窗口 handle 關聯,因此調用方必須藉助一個窗口才能接收消息,這是其不方便之處。另外,通過消息聯絡會影響速度,需要高速處理時回調方式更有優勢。
        如果調用方和被調方分屬兩個不同的進程,由於內存空間的隔閡,一般是採用 Windows 消息發通知比較簡單可靠,被調方可以藉助消息本身向調用方傳送數據。event 對象也可以通過名稱在不同進程間共享,但只能發通知,本身無法傳送數據,需要藉助 Windows 消息和 FileMapping 等內存共享手段或藉助 MailSlot 和 Pipe 等通信手段。
        異步調用原理並不複雜,但實際使用時容易出莫名其妙的問題,特別是不同線程共享代碼或共享數據時容易出問題,編程時需要時時注意是否存在這樣的共享,並通過各種狀態標誌避免衝突。Windows 系統提供的 mutex 對象用在這裏特別方便。mutex 同一時刻只能有一個管轄者。一個線程放棄管轄權後,另一線程才能接管。當某線程執行到敏感區之前先接管 mutex,使其他線程被 wait 函數堵在身後;脫離敏感區之後立即放棄管轄權,使 wait 函數結束等待,另一個線程便有機會光臨此敏感區。這樣就可以有效避免多個線程進入同一敏感區。
        由於異步調用容易出問題,要設計一個安全高效的編程方案需要比較多的設計經驗,所以最好不要濫用異步調用。同步調用畢竟讓人更舒服些:不管程序走到哪裏,只要死盯着移動點就能心中有數,不至於象異步調用那樣,總有一種四面受敵、惶惶不安的感覺。必要時甚至可以把異步函數轉換爲同步函數。方法很簡單:調用異步函數後馬上調用 wait 函數等在那裏,待異步函數返回結果後再繼續往下走。

 

3.如何寫一個異步函數

        我們平常編程寫的函數 幾乎都是同步調用函數,那麼我們如何寫一個異步執行的函數呢?!我想這個問題也許是哪些比較喜歡專研的程序員或者具有專研精神的人士回提出的問題吧!我們很多人已經習慣了windows系統提供的一些異步機制,使用這些異步機制我們很快的就能實現一些異步操作甚至可以很容易的實現一個異步執行的函數;但是我們研究過實現一個“異步函數”的本質嗎?!

        在單線程的系統中,所以的指令執行都是順序執行的,這就暗示瞭如果一個函數A中調用了函數B,則A必須等到B執行後才能繼續執行A中剩下的代碼。

        在多線程中,如果我們有一個threadA線程,在該線程中調用了一個函數C,而該C函數我們想將它實現成異步執行的,而異步執行必須要有多線程支持;如果我們在Windows中編寫程序,創建一個線程是很簡單隻要使用

HANDLE WINAPI CreateThread(

                                                  __in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes,

                                                  __in SIZE_T dwStackSize,

                                                  __in LPTHREAD_START_ROUTINE lpStartAddress,

                                                  __in_opt LPVOID lpParameter,

                                                  __in DWORD dwCreationFlags,

                                                  __out_opt LPDWORD lpThreadId );

函數就可以創建一個線程。

 

那麼我們按如下方式可以實現一個異步的FuncC函數:

(1)先把你要異步完成的工作單獨寫成要給函數,如

DWORD   WINAPI  AsyncronousThread(

LPVOID   lpParameter       //   thread   data   
        )

{

.....

}

(2)在函數FuncC中使用CreateThtread函數將(1)中的函數創建一成一個線程,然後直接返回

void FuncC(void)

{

 .....

CreateThread(....,AsyncronousThread,...);

return;

}

當然,寫一個異步函數的方法很多,但是一個本質不會變,就是必須要依據多線程才能實現。

 

4.回調與異步機制

        回調函數是應用程序提供給Windows系統DLL或其它DLL調用的函數,一般用於截獲消息、獲取系統信息或處理異步事件。應用程序把回調函數的地址指針告訴DLL,而DLL在適當的時候會調用該函數。回調函數必須遵守事先規定好的參數格式和傳遞方式,否則DLL一調用它就會引起程序或系統的崩潰。通常情況下,回調函數採用標準WindowsAPI的調用方式,即__stdcall,當然,DLL編制者可以自已定義調用方式,但客戶程序也必須遵守相同的規定。在__stdcall方式下,函數的參數按從右到左的順序壓入堆棧,除了明確指明是指針或引用外,參數都按值傳遞,函數返回之前自己負責把參數從堆棧中彈出。

程序在調用一個函數(function)時(通常指api).相當於程序(program)呼叫(Call)了一個函數(function)關係表示如下:
                call(調用)
   program  --------------------→    dll 

程序在調用一個函數時,將自己的函數的地址作爲參數傳遞給程序調用的函數時(那麼這個自己的函數稱回調函數).需要回調函數的 DLL 函數往往是一些必須重複執行某些操作的函數.關係表示如下:

               call(調用)
   program  --------------------→    dll
      ↑                               ¦
      ¦_______________________________¦
              callback(回調)

        當你調用的函數在傳遞返回值給回調函數時,你就可以利用回調函數來處理或完成一定的操作。至於如何定義自己的回調函數,跟具體使用的API函數有關,很多不同類別的回調函數有各種各樣的參數,有關這些參數的描述一般在幫助中有說明回調函數的參數和返回值等.其實簡單說回調函數就是你所寫的函數滿足一定條件後,被DLL調用!

        Windows 系統還包含着另一種更爲廣泛的回調機制,即消息機制。消息本是 Windows 的基本控制手段,是一種變相的函數調用。發送消息的目的是通知收方運行一段預先準備好的代碼,相當於調用一個函數。消息所附帶的 WParam 和 LParam 相當於函數的參數,應用程序可以主動發送消息,更多情況下是坐等 Windows 發送消息。一旦消息進入所屬消息隊列,便檢感興趣的那些,跳轉去執行相應的消息處理代碼。操作系統本是爲應用程序服務,由應用程序來調用。而應用程序一旦啓動,卻要反過來等待操作系統的調用。這分明也是一種回調,或者說是一種廣義回調。其實,應用程序之間也可以形成這種回調。假如進程 B 收到進程 A 發來的消息,啓動了一段代碼,其中又向進程 A 發送消息,這就形成了回調。這種回調比較隱蔽,弄不好會搞成遞歸調用,若缺少終止條件,將會循環不已,直至把程序搞垮。利用消息也可以構成狹義回調。把回調函數地址換成窗口 handle。如此,當需要比較數據大小時,不是去調用回調函數,而是借 API 函數 SendMessage 向指定窗口發送消息。收到消息方負責比較數據大小,把比較結果通過消息本身的返回值傳給消息發送方。所實現的功能與回調函數並無不同。當然,此例中改爲消息純屬畫蛇添腳,反倒把程序搞得很慢。但其他情況下並非總是如此,特別是需要異步調用時,發送消息是一種不錯的選擇。假如回調函數中包含文件處理之類的低速處理,調用方等不得,需要把同步調用改爲異步調用,去啓動一個單獨的線程,然後馬上執行後續代碼,其餘的事讓線程慢慢去做。一個替代辦法是借 API 函數 PostMessage 發送一個異步消息,然後立即執行後續代碼。這要比自己搞個線程省事許多,而且更安全。

        回調用於層間協作,上層將本層函數安裝在下層,這個函數就是回調,而下層在一定條件下觸發回調,例如作爲一個驅動,是一個底層,他在收到一個數據時,除了完成本層的處理工作外,還將進行回調,將這個數據交給上層應用層來做進一步處理,這在分層的數據通信中很普遍。其實回調和API非常接近,他們的共性都是跨層調用的函數。但區別是API是低層提供給高層的調用,一般這個函數對高層都是已知的;而回調正好相反,他是高層提供給底層的調用,對於低層他是未知的,必須由高層進行安裝,這個安裝函數其實就是一個低層提供的API,安裝後低層不知道這個回調的名字,但它通過一個函數指針來保存這個回調,在需要調用時,只需引用這個函數指針和相關的參數指針。    其實:回調就是該函數寫在高層,低層通過一個函數指針保存這個函數,在某個事件的觸發下,低層通過該函數指針調用高層那個函數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章