QT 異步函數和同步函數交換問題

同步函數與異步函數

依據微軟的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 函數等在那裏,待異步函數返回結果後再繼續往下走。

假如回調函數中包含文件處理之類的低速處理,調用方等不得,需要把同步調用改爲異步調用,去啓動一個單獨的線程,然後馬上執行後續代碼,其餘的事讓線程慢慢去做。一個替×××法是借 API 函數 PostMessage 發送一個異步消息,然後立即執行後續代碼。這要比自己搞個線程省事許多,而且更安全。

 

如果你的服務端的客戶端數量多,你的服務端就採用異步的,但是你的客戶端可以用同步的,客戶端一般功能比較單一,收到數據後才能執行下面的工作,所以弄成同步的在那等。

一、舉個打電話的例子:

阻塞 block 是指,你撥通某人的電話,但是此人不在,於是你拿着電話等他回來,其間不能再用電話。同步大概和阻塞差不多。

非阻塞 nonblock 是指,你撥通某人的電話,但是此人不在,於是你掛斷電話,待會兒再打。至於到時候他回來沒有,只有打了電話才知道。即所謂的“輪詢 / poll”。

異步是指,你撥通某人的電話,但是此人不在,於是你叫接電話的人告訴那人(leave a message),回來後給你打電話(call back)。

二、同步異步與阻塞和非阻塞是兩種不同的概念來着

同步異步指的是通信模式,而阻塞和非阻塞指的是在接收和發送時是否等待動作完成才返回

首先是通信的同步主要是指客戶端在發送請求後,必須得在服務端有迴應後才發送下一個請求。所以這個時候的所有請求將會在服務端得到同步

其次是通信的異步指客戶端在發送請求後,不必等待服務端的迴應就可以發送下一個請求,這樣對於所有的請求動作來說將會在服務端得到異步,這條請求的鏈路就象是一個請求隊列,所有的動作在這裏不會得到同步的。

阻塞和非阻塞只是應用在請求的讀取和發送。

在實現過程中,如果服務端是異步的話,客戶端也是異步的話,通信效率會很高,但如果服務端在請求的返回時也是返回給請求的鏈路時,客戶端是可以同步的,這種情況下,服務端是兼容同步和異步的。相反,如果客戶端是異步而服務端是同步的也不會有問題,只是處理效率低了些。

 

設想你是一位體育老師,需要測驗100位同學的400米成績。你當然不會讓100位同學一起起跑,因爲當同學們返回終點時,你根本來不及掐表記錄各位同學的成績。

如果你每次讓一位同學起跑並等待他回到終點你記下成績後再讓下一位起跑,直到所有同學都跑完。恭喜你,你已經掌握了同步阻塞模式。你設計了一個函數,傳入參數是學生號和起跑時間,返回值是到達終點的時間。你調用該函數100次,就能完成這次測驗任務。這個函數是同步的,因爲只要你調用它,就能得到結果;這個函數也是阻塞的,因爲你一旦調用它,就必須等待,直到它給你結果,不能去幹其他事情。

如果你一邊每隔10秒讓一位同學起跑,直到所有同學出發完畢;另一邊每有一個同學回到終點就記錄成績,直到所有同學都跑完。恭喜你,你已經掌握了異步非阻塞模式。你設計了兩個函數,其中一個函數記錄起跑時間和學生號,該函數你會主動調用100次;另一個函數記錄到達時間和學生號,該函數是一個事件驅動的callback函數,當有同學到達終點時,你會被動調用。你主動調用的函數是異步的,因爲你調用它,它並不會告訴你結果;這個函數也是非阻塞的,因爲你一旦調用它,它就馬上返回,你不用等待就可以再次調用它。但僅僅將這個函數調用100次,你並沒有完成你的測驗任務,你還需要被動等待調用另一個函數100次。

當然,你馬上就會意識到,同步阻塞模式的效率明顯低於異步非阻塞模式。那麼,誰還會使用同步阻塞模式呢?不錯,異步模式效率高,但更麻煩,你一邊要記錄起跑同學的數據,一邊要記錄到達同學的數據,而且同學們回到終點的次序與起跑的次序並不相同,所以你還要不停地在你的成績冊上查找學生號。忙亂之中你往往會張冠李戴。你可能會想出更聰明的辦法:你帶了很多塊秒錶,讓同學們分組互相測驗。恭喜你!你已經掌握了多線程同步模式!

每個拿秒錶的同學都可以獨立調用你的同步函數,這樣既不容易出錯,效率也大大提高,只要秒錶足夠多,同步的效率也能達到甚至超過異步。

可以理解,你現的問題可能是:既然多線程同步既快又好,異步模式還有存在的必要嗎?

很遺憾,異步模式依然非常重要,因爲在很多情況下,你拿不出很多秒錶。你需要通信的對端系統可能只允許你建立一個SOCKET連接,很多金融、電信行業的大型業務系統都如此要求。

一、同步阻塞模式

在這個模式中,用戶空間的應用程序執行一個系統調用,並阻塞,直到系統調用完成爲止(數據傳輸完成或發生錯誤)。

二、同步非阻塞模式
同步阻塞 I/O 的一種效率稍低的。非阻塞的實現是 I/O 命令可能並不會立即滿足,需要應用程序調用許多次來等待操作完成。這可能效率不高,因爲在很多情況下,當內核執行這個命令時,應用程序必須要進行忙碌等待,直到數據可用爲止,或者試圖執行其他工作。因爲數據在內核中變爲可用到用戶調用 read 返回數據之間存在一定的間隔,這會導致整體數據吞吐量的降低。但異步非阻塞由於是多線程,效率還是高。

複製代碼

/* create the connection by socket 
* means that connect "sockfd" to "server_addr"
* 同步阻塞模式 
*/if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1)
{
perror("connect");
exit(1);
}/* 同步非阻塞模式 */while (send(sockfd, snd_buf, sizeof(snd_buf), MSG_DONTWAIT) == -1)
{
sleep(1);
printf("sleep\n");
}

複製代碼

 

網絡程序開發流程:

1. 需求分析

2. 根據需求, 進行數據包設計(一般分爲包頭和包數據兩部分, 包頭用來存儲包的必要信息, 如信息類型, 數據長度等)

3. 定義傳輸協議(如何傳輸).

4. 理解需求, 設計總體架構, 利用設計模式等方法, 進行問題分析和設計類圖.

5. 實現, 通常要配合多線程來實現通信問題. (一般有等待客戶請求線程, 接收數據線程, 發送數據線程, 資源清理線程).

6. 實現服務器端.

7. 實現客戶端.


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