開篇
1、背景
之前的很長一段時間里,隨着加工工藝的發展,cpu的處理速度一直在提升,基本上每18個月就會翻倍。直到04年cpu主頻達到了4.0GH以來,這種規律似乎已經失效,原因是人們在制造cpu的工藝方面已經達到了物理極限。除非技術有本質突破,才能進一步提高cpu的處理速度。然而需要處理的數據量並沒有因此而停止增長,其中的一個方法就是采用多核、並行處理技術。這會成為並且正在成為未來發展的趨勢。要理解並行技術,對線程有一定的了解是很必要的。這篇博客主要說一下自己對線程的看法,這只是從簡單的角度來看問題,入門級文章,筆者認知有限,有不足之處還望不吝指正。
2、我的想法
關於並發編程,我覺得如果能有一種專門為並行而設計的語言,將是最好的解決方案。因為現有的語言大多的針對單核處理器的,現在的並發多數是由操作系統完成,用現行語言來編寫並發程序的技術還顯得不是很成熟。如果能有一種專門為並發而設計的語言,將會大幅度的提高程序的運行效率。當然這只是我的小想法而已。
3、內容一覽:
1)程序和進程的概念區別
2)線程的概念
3)多線程的調度問題
4)線程安全問題(當多個線程同時訪問一個變量時)
程序和進程
一般意義上的程序可以認為是在特定操作系統上的可執行文件。也就是源碼經過編譯、鏈接而形成的可執行文件,它依賴於執行的操作系統,因為正是操作系統提供了該程序運行的環境(運行庫等)。它是一個靜態的概念。
而進程,是一個動態的概念,它有自己的地址空間,能執行一些操作。程序的執行都會伴隨着進程的生成,一個程序的執行會產生一個或多個進程。
所以可認為進程是程序的動態概念。
總結下程序和進程的區別
1)進程是動態的,而程序是靜態的
2)進程有一個生命周期 ,而程序是指令集合,本身無“運動”含義。沒有建立進程的程序不能作為一個單獨的單位得到操作系認可
3)1個程序可產生多個進程,一個進程只對應一個程序
什么是線程
線程有時候又被稱為輕量級進程,是程序執行的最小單元。和上文中一樣的,一個進程可對應多個線程,而一個線程只屬於一個進程。
進程的執行是以線程為單位進行得,比如說一個簡單“hello world”程序只有一個線程,就是main()函數對應的線程。
線程的構成:
1)線程ID。用於標實線程
2)當前指令指針PC。標明下一指令執行點
3)寄存器集合和堆棧。該線程的可用空間
多線程
大多數軟件應用中,線程的數量都不止一個。多線程可用並發的執行,並共享進程的全局便來那個和堆的數據。
多線程優勢:
1)某個操作可能會陷入長時間的等待。采用多線程,當一個線程等待的時候,可執行其他線程,充分利用cpu。
2)某操作(如計算)可能會消耗大量時間,而導致和用戶之間的交互中斷。多線程可讓一個線程負責計算,另一線程負責交互。
3)軟件本身就要求並發操作,如多端下載軟件
4)多核cpu,本身就具備同時執行多個線程的能力
線程訪問權限
一般來說線程能訪問進程內存中的所有數據,但實際應用中線程也有自己的空間
1)棧(可能被其他進程訪問,但仍可認為是私有數據)
2)線程局部存儲,一般只有很小容量
3)寄存器(包括PC寄存器)
線程調度
最好的情況是:當處理器數量大於要處理的線程數目的時候,所有線程都可以同時執行。實現真正意義上的並發。
而這種情況在現實中基本不可能。現實中的並發只是一種模擬出來的狀態,特別是在單核處理對於多線程的時候。它通過讓多個線程交替執行,每個線程執行很短時間,從表面上看,這些線程同時執行,實現並發。
每個線程都想被執行,但是每次執行的線程數量是有限的,所以就要有一種方法來從眾多的線程中選出要執行的線程,現在討論下單核的情況,多核的類似。
在操作系統中有專門的線程調度算法來實現,下面列幾個簡單的“調度算法”
1、“先進先出”策略。所有的線程組成一個隊列,新生的線程加入到隊列末尾,每次取隊頭執行。有一個缺陷就是,如果新生成的線程是緊急操作,需要操作系統盡快相應,這種調度方法就不能滿足了。
2、按優先級調度。每個線程都有自己的優先級,並且是可以被操作系統修改的,調度時候每次選取優先級最高的執行。這種方法彌補了上一種方法的缺陷。對於需要及時相應的緊急事件,可以給他一個高優先級,這樣就能在下次被調度。然而這種方法也有一個問題,也就是所謂的飢餓。如果某線程“看似”無關緊要,被給予一個低得優先級,以后每次產生的線程優先級都比他高,那么這個線程會一直得不到執行,成為餓死。一個解決的辦法是隨之事件的推移而提升線程的優先級。這樣只要事件足夠長,低優先級的線程也會獲得高優先級而被執行。
從上面的分析來開,線程似乎有兩個狀態:執行和不執行(等待)。其實操作系統中的每個線程都對應三個狀態。
線程的狀態:
上面說了線程調度的問題,只有准備就緒的線程(這種線程稱為就緒的線程),才能被調度。調度以后,線程就在處理器中被執行,這時線程的狀態為運行時。如果該線程在等待某種事件的發生(如響應I/O),這種狀態成為等待。
總結下線程的三種狀態:
1)就緒:此時線程可以立刻運行i(如果該線程被調用的話)
2)運行:此時線程正在執行
3)等待:線程正在等待某件事的發生以便繼續執行
在windows中,線程的狀態有:
已初始化(Initialized):說明一個線程對象的內部狀態已經初始化,這是線程創建過程中的一個內部狀態,此時線程尚未加入到進程的線程鏈表中,也沒有啟動。
.
個線程來執行時,它只考慮處於就緒狀態的線程。此時,線程已被加入到某個處理器的就緒線程鏈表中。
運行(Running):線程正在運行。該線程一直占有處理器,直至分到的時限結束,或者被一個更高優先級的線程搶占,或者線程終止,或者主動放棄處理器執行權,或者進入等待狀態。
備用(Standby):處於備用狀態的線程已經被選中作為某個處理器上下一個要運行的線程。對於系統中的每個處理器,只能有一個線程可以處於備用狀態。然而,一個處於備用狀態的線程在真正被執行以前,有可能被更高優先級的線程搶占。
已終止(Terminated):表示線程已經完成任務,正在進行資源回收。KeTerminateThread函數用於設置此狀態。
等待(Waiting):表示一個線程正在等待某個條件,比如等待一個分發器對象變成有信號狀態,也可以等待多個對象。當等待的條件滿足時,線程或者立即開始運行,或者回到就緒狀態。
轉移(Transition):處於轉移狀態的線程已經准備好運行,但是它的內核棧不在內存中。一旦它的內核棧被換入內存,則該線程進入就緒狀態。
延遲的就緒(DeferredReady):處於延遲的就緒狀態的線程也已經准備好可以運行了,但是,與就緒狀態不同的是,它尚未確定在哪個處理器上運行。當有機會被調度時,或者直接轉入備用狀態,或者轉到就緒狀態。因此,此狀態是為了多處理器而引入的,對於單處理器系統沒有意義。
門等待(GateWait):線程正在等待一個門對象。此狀態與等待狀態類似,只不過它是專門針對門對象而設計。
他們之間的轉換圖如下:
更多關於調度的知識可參見《現代操作系統》《windows內核分析》
可搶占線程和不可搶占線程
可搶占線程就是說,在該線程用完自己的時間片以后,操作系統會強制把該線程切出,以便執行其他線程。
而不可搶占線程則是線程不能強制切出,除非他自己放棄cpu的使用權而終止線程,而不是靠時間片的用盡而強制切出。不可搶占線程的線程切換時間是確定的,當該線程自願切出時發生。
線程安全
多線程並發時,在訪問數據方面會出現一些問題。特別是當多個線程訪問同一個變量的時候。
下面將用一個例子來說明可能出現的問題:
線程A、線程B都對變量X進行操作,操作順序如下:
1)線程A對X賦值
2)線程A對X自加
3)線程B使用X的值(比如說把它賦值給另一個變量)
代碼經過編譯之后,在處理器中執行代碼時,通常一個很簡單的運算(如自家運算)都會被分為多個步驟執行(指令流水)。
比如當線程A對X自加運算的時候,編譯后的自加運算共分為三步,當沒有執行完這三步的時候,可能線程A就會被切出(比如說有需要即時響應的操作發生)。也就是說線程A在對數據還沒處理完全的時候被切出了,這樣當線程B執行的時候,使用的X值將不是我們期望的值,顯然,發生了錯誤。
解決策略:
上面問題的出現本質上是因為一個不應該被打斷的操作被強行中斷了,那么有一種解決的辦法就是設置一種規定一些操作,在執行的時候不能被中斷。這樣就避免了操作還沒完成就被換出的情況。把這些簡單的操作稱為原子的操作。windows中對於這種操作也有支持。
但是這種策略只適用於簡單的情況。對於復雜的情況,我們用一種稱為同步與鎖的機制來實現。
同步與鎖
簡單的說,就是在一個線程對數據訪問結束之前,其他線程不能對這個數據進行訪問。這樣的話,對數據的訪問就原子化了。
這種機制的實現也很簡單:每個線程對數據訪問的時候都會嘗試獲取鎖,當訪問結束后釋放。在獲取鎖的時候如果有線程在訪問數據,就會獲取失敗,這時候線程會等待, 直到訪問數據的那個線程釋放鎖。
二元信號量
是最簡單的一種鎖,它只有兩個狀態:占用與非占用。它用於只能被一個線程訪問的資源。只有資源狀態為非占用 的時候,才能被線程獲取,獲取之后修改資源狀態為占用,訪問結束后修改資源狀態為非占用。
信號量
稍微復雜一些,他適用於可以被多個線程同時訪問的資源。一個初始值為N=n的信號量能被n個線程同時訪問。當要訪問數據的時候,先查看N值,(N的值代表還有多少個線程能訪問資源)如果N值大於0,該線程能訪問資源,線程進入后把N值減一。當訪問結束后N值+1.
如果信號量的值小於0,則進入等待狀態。
互斥量
和二元信號相似,資源只能被一個線程訪問,但是同一個信號量只能被獲取該信號量的線程釋放,也就是說對於二元信號量,同一個互斥量可以被別的(任意)線程釋放。相對二元信號量來說更嚴格了。
臨界區
是比互斥量更嚴格的同步手段。臨界區和上面的區別在於,互斥量和信號量在任何進程都是可見的,也就是說,一個進程創建了互斥量和信號量,在其他進程都是可見的,而臨界區的作用范圍僅限於本進程。
讀寫鎖
讀寫鎖用於更加通用的場合。對於同一個數據,多個進程同時讀是沒問題的,但是如果有線程要對數據進行修改,就要使用同步手段來避免出錯。對於同一個讀寫鎖,有兩種獲取方式:
1、共享的,對數據只進行讀操作,可以多個線程同時進行
2、獨占的。會修改數據,在修改完成之前,不能有其他線程操作數據
當鎖處於自由狀態時,以任何一種方式獲取鎖都會成功,並將鎖至於相應的狀態。
如果鎖處於共享狀態,那么其他與以共享方式獲取鎖的線程都能成功,共同讀數據。
對於獨占式獲取的,則要等到以共享方式獲取的所有線程釋放后(鎖重新回到自由狀態)才能獲取。並且對於以獨占方式獲取的鎖,其他任何對鎖的請求都不會成功。
條件變量
條件變量類似於一個發令槍,可以有多個線程等待槍響,槍響的時候,這些等待槍響的線程會同時恢復執行。發令槍何時響也可由線程來決定。
也就是說,條件變量可以讓多個線程等待某件事的發生,當時間發生時(條件變量被喚醒),所有的線程可以一起恢復執行。
小結
這篇文章主要介紹了線程的概念,然后簡單的說了線程調度,最后敘述了線程的安全性問題,也是就是多線程共享資源時候的相關問題。而對於實際的例子這里沒有給出。是因為網上關於線程的實例已經有很多了。
寫這篇文章的目的是總結一下自己對線程的認識,從我的角度去理解線程,希望這篇文章能對各位園友有幫助。
參看資料:computer systems 《windows內核分析》《程序員的自我修養》
如有轉載請注明出處:http://www.cnblogs.com/yanlingyin/
一條魚~@ 博客園 2012-2-12
E-mail:yanlingyin@yeah.net