多線程(轉)

  在當今的程序設計中,經常看到進程、線程這樣的名詞,其實它們屬於一個更廣大的範疇——多任務,現在讓我們先從進程和線程切入,來領略一下這個領域的廣闊風光。

  我瞭解不多,只能說說80x86上的Windows環境,其他如lpha,Mac或者Linux,我就一竅不通了,見笑見笑……

  現代操作系統都是多任務的操作系統,在這裏要澄清一個概念,Windows 3.x時代也有所謂的多任務,但是,那並不是現代意義的多任務--"搶佔式多任務"。Windows 3.x的多任務是非搶佔式的,即,一個應用程序,甚至系統,要等現在正在運行的程序主動放棄CPU(程序員把這個禮貌的行爲寫到程序中),才能獲得執行時間片。可以看出,在這種環境中,操作系統的主動性是很小的,只要某個已經獲得CPU的程序不主動放棄CPU,操作系統就得不到時間運行,亦無法實行其管理調度功能。如果一個應用程序因崩潰而掛起,將導致整個系統掛起。

  而搶佔式的多任務是指,將CPU時間片分配最需要它的應用程序,即便一個應用程序永遠不打算放棄CPU,操作系統也能保證隨時"搶佔"CPU時間,然後對當前所有的任務進行合理的調度。

  要實現多任務,首先要有CPU的支持,80x86 (Alpha?我不懂,別問我……) 支持多任務,通過一個TSS--任務描述符,80x86能夠描述一個任務。詳細我就不說了,和本文關係不大,只要說明搶佔式多任務不是操作系統在單幹就行了。

  作爲操作系統,要能夠利用CPU提供的功能才能實現多任務,Windows做到了這點(當然,Linux也做到了,只是我不懂……)。系統有一個核心調度程序,負責爲每一個任務分配CPU時間,允許其執行指定的一段時間,當這段時間用完後,控制權會重新交回到操作系統,操作系統可在此時重新分配CPU時間。至於CPU時間是如何分配的,就又不是本文的範圍了。Microsoft給出的文檔是說"系統保證CPU時間的分配是公平的",僅此而已……。

  在Windows中,一個任務就是一個線程,以前曾經說過,線程不能沒有存儲而存在,而其所依賴存在的存儲對象就是進程。而一個進程--以前也說過了--對應一個映象(可執行文件)。也就是說,在一個可執行文件運行時,可以有多個線程。

  好了,基本的概念大概說明了,來討論一下它們的用途。要這些多出來(除了原始線程外)的線程有什麼用?MSDN給出的答案是"等"。但我個人認爲稍微籠統了一點(偏激一點?)。不過,至少,有一點是肯定的--絕不要使多個線程同時進行繁重的操作--除非你是建立了CPU數目個線程(32路對等處理器系統?)。無論如何,實際工作的還是CPU,在這種情況下,CPU不僅不會少執行指令,還要執行很多的排班程序指令以及任務的切換指令--反而降低效率。

  在需要等待的地方,多線程確實能夠發揮很高的效能。(注意,並不一定是最高,以後將說到經常是更好的實現方法,這裏只是說明線程的用法)舉一個例子:一個網絡程序向遠程主機發送了一個請求,正在等待迴應,而在此期間,它還希望能夠與用戶進行交互。一種實現方法是:程序繼續與用戶交互,在交互的間歇檢查一下回應是否到達。而更好的方法是建立一個新的線程(稱爲工作線程)來等待迴應,原始線程繼續照常與用戶交互。如果您已經感覺到後一種方法確實比前一種方法好很多,那麼,您可以不讀下一段,不用聽我羅嗦了。

  後一種方法比前一種方法好在後者執行的指令更少,因而效率更高。如果您對Windows編程熟悉:在實現前者時,必須保證能夠及時檢測到迴應到達,因而就不能使用GetMessage()而要使用PeekMessage()。如果使用GetMessage(),而恰巧在很長的一段時間內都沒有消息到達,原始線程就不會從GetMessage()返回,也就不能檢測迴應是否到達。使用PeekMessage(),可以令其在沒有消息時也立即返回,因而可以檢測迴應是否到達。網絡部分的情況也一樣--程序不能等迴應一直等下去否則就無法與用戶交互。無論如何,在即沒
有消息、也沒有迴應到達的情況下,原始線程沒有有效的進入等待狀態,而是不停地"空轉",檢測二者中是否有到達的,這對系統資源顯然是極大的浪費。而對於後者,原始線程通過GetMessage()有效的進入了等待,工作線程也可以通過類似的方法進入等待,例如使用Socket的select()。

  但是,新的問題又出現了,工作線程發現迴應已經到達了,它可能要通知原始線程才行--它與原始線程是併發執行的。這就涉及到線程簡通信了,有一種最簡單的方法:Windows消息。工作線程通過向原始線程的窗口發送一個消息,然後終止;當原始線程的消息循環發現這個消息時,就知道迴應已經到達了。線程間通信的方法還有很多,將在以後專門介紹。

  最後要說明的是--很重要的一點:多線程經常不是程序員主動來使用的,而是在依賴操作系統時,已然是多線程了。即使用,也很有節制,濫用線程將適得其反。究竟什麼地方應該使用多線程,並沒有什麼規則,將多線程用在該用的地方而已,當然不會刻意的使用它。當綜合各種條件和各種可能的方案後,如發現多線程是最好的,就是使用多線程的最佳時機。我是否說的是廢話?呵呵,其實就像這篇文章一樣,我也很頭疼,線程是在遇到實際需要時纔想起用的,硬要設計一種情況來使用實在不易。有備無患。多線程不是殺手鐗,但是掌握它是向較高級編程邁進的必經之路。

在當今的程序設計中,經常看到進程、線程這樣的名詞,其實它們屬於一個更廣大的範疇——多任務,現在讓我們先從進程和線程切入,來領略一下這個領域的廣闊風光。

  上次說了線程的作用,顯然,要使得線程之間能夠通訊才能利用好線程,否則,每個線程都各幹個的,向一盤散沙,程序就什麼也做不了兒了。現在,我們來看看線程間如何通訊。
  線程間通訊的方法有很多,常用的有:變量、臨界段、Windows消息、事件。

  首先來討論變量。既然線程都處於同一個進程內,它們的地址空間就是相同的,對於完全依賴地址空間的變量來說,當然可以被同在一個進程中的任意一個線程訪問,因而就可以用來通訊。

  比如,有一個全局變量,兩個線程;我們希望第一個線程在工作,而第二個線程等待,當第一個線程檢測的某件事發生時,通知第二個線程,使第二個線程開始運行。可以將全局變量置爲0,然後讓第二個線程進入一個“死循環”,等待全局變量變爲1。而第一個線程執行他的任務,當事件發生時,第一個線程將全局變量賦值爲1,於是第二個線程便奇妙的結束了他的死循環,開始執行預定的工作。

  由此可見,變量確實可以進行線程間的通訊;但不是使用上面的方法(該方法被稱爲“循環鎖”),因爲它太拙劣了,第二個線程在等待中要消耗大量的CPU時間,卻不作任何事。而且,它不能處理複雜的情況,例如:

  還是上兩個線程,第一個線程分發任務,第二個線程執行任務;第一個線程每次給全局變量加上一個需執行的任務的數目,第二個線程每次從全局變量中減掉它完成的任務的數目。兩個運算都是“全局變量 op 任務數目 → 全局變量”,假設線程一先讀取全局變量,是n,然後加上新任務數目,結果是n+p;而就在同時,線程二也讀取了全局變量,由於線程一還沒來得及將結果寫回,線程二讀到的也是n,減去完成的任務數,結果是n-q;現在,兩個線程都要將結果寫回到全局變量,顯然這裏有問題,最終結果要麼是n+p、要麼是n-q,總之不是正確的n+p-q。

  問題就出在那個“同時”上,如果讓線程二等線程一把結果寫回全局變量再讀取、減去、寫回,錯誤就不會發生。

  Windows提供了一個專門針對變量通訊的同步方法——互鎖函數。這是一組形如Interlocked???()的函數。每個函數都可以對一個變量進行一種特定的操作,在操作進行中,能夠自動保持與其它線程同步——每個函數都執行一種“原子操作”——該操作不能與共用同一資源的其它操作同時進行。

  例如,上面的例子,通過使用InterlockedExchangeAdd()來執行加減操作,便可以保證對全局變量的操作不會同時發生。

  有時,線程間的通訊過程不像上述的那樣簡單、僅僅是做一次加減法,而是由許多步組成的。比如,線程一除了加上任務數外,還要填寫每個任務的具體參數,線程一進而希望在自己填寫完全部所需數據後,線程二再對它們修改,原因類似。這是可以使用臨界段。

  CRITICAL_SECTION類型聲明一個臨界段結構變量,然後用InitializeCriticalSection()初始化它。EnterCriticalSection()和LeaveCriticalSection()兩個API分別指示進入或退出臨界段,參數是一個已經定義的臨界段。在臨界段內的一組操作都是原子的,它們不能與共用同一臨界段的另外一組操作同時進行。

  與互鎖函數的不同是,互鎖函數僅涉及一個共享資源,執行一個操作,因而沒有該保護哪些資源、保護多長時間的問題;臨界段涉及多個資源,執行多個操作,因而需要一個變量來代表對一類資源和操作的保護。

  Windows的消息機制就是用來通訊的,它本身就支持線程間通訊。消息一般是基於窗口的,而窗口是屬於創建它的線程的。兩個屬於不同線程的窗口可以通過Windows消息來通訊。例如上面線程二等待的情況就可以讓線程二調用GetMessage()等待消息,當線程一使用PostMessage()將消息發送給線程二時,GetMessage()將返回,線程二可以執行相應的任務。消息的兩個參數可以用來攜帶對任務的描述。即便一個線程沒有窗口,其它線程也可以使用PostThreadMessage()將消息直接發給該線程。使用Windows消息來通訊,是一對一的進行的,在某些場合,可能需要一對多、多對一或多對多的通訊,這時,可以藉助事件(Event)來完成。CreateEvent()創建一個事件,事件有兩種狀態——已觸發和未觸發。SetEvent()用來觸發一個事件,ResetEvent()用來恢復事件到未觸發狀態。一組“等待函數”用來檢測事件的狀態,例如WaitForSingleObject()等待一個事件,直到事件的狀態爲已觸發時才返回。與上面Windows消息的例子類似,它也可以用於讓線程二等待,更方便的是,如果有另外的線程三做與線程二類似的工作,它可以等待同一個事件,線程一的一次SetEvent()將同時通知兩個線程執行任務;如果有線程零做與線程一類似的工作,它也可以觸發同一個事件,線程零或線程一任意一方觸發事件都將使兩個線程執行任務。

  除了上述的幾種方法以外,旗語等方法也是用來進行線程間通訊,這裏先不作介紹。

  當設計線程間的通訊時,一定要時刻記得各個線程之間是異步運行的,必須要使用某種機制才能進行通訊,在實際中,情況可能會非常複雜,如果考慮不全面,結果將是難以預料的。

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