開篇算法
一、背景編程
以前的很長一段時間裏,隨着加工工藝的發展,cpu的處理速度一直在提高,基本上每18個月就會翻倍。直到04年cpu主頻達到了4.0GH以來,這種規律彷佛已經失效,緣由是人們在製造cpu的工藝方面已經達到了物理極限。除非技術有本質突破,才能進一步提升cpu的處理速度。然而須要處理的數據量並無所以而中止增加,其中的一個方法就是採用多核、並行處理技術。這會成爲而且正在成爲將來發展的趨勢。要理解並行技術,對線程有必定的瞭解是很必要的。這篇博客主要說一下本身對線程的見解,這只是從簡單的角度來看問題,入門級文章,筆者認知有限,有不足之處還望不吝指正。windows
二、個人想法安全
關於併發編程,我以爲若是能有一種專門爲並行而設計的語言,將是最好的解決方案。由於現有的語言大多的針對單核處理器的,如今的併發多數是由操做系統完成,用現行語言來編寫併發程序的技術還顯得不是很成熟。若是能有一種專門爲併發而設計的語言,將會大幅度的提升程序的運行效率。固然這只是個人小想法而已。多線程
三、內容一覽:併發
1)程序和進程的概念區別函數
2)線程的概念post
3)多線程的調度問題spa
4)線程安全問題(當多個線程同時訪問一個變量時)操作系統
程序和進程
通常意義上的程序能夠認爲是在特定操做系統上的可執行文件。也就是源碼通過編譯、連接而造成的可執行文件,它依賴於執行的操做系統,由於正是操做系統提供了該程序運行的環境(運行庫等)。它是一個靜態的概念。
而進程,是一個動態的概念,它有本身的地址空間,能執行一些操做。程序的執行都會伴隨着進程的生成,一個程序的執行會產生一個或多個進程。
因此可認爲進程是程序的動態概念。
總結下程序和進程的區別
1)進程是動態的,而程序是靜態的
2)進程有一個生命週期 ,而程序是指令集合,自己無「運動」含義。沒有創建進程的程序不能做爲一個單獨的單位獲得操做系承認
3)1個程序可產生多個進程,一個進程只對應一個程序
什麼是線程
線程有時候又被稱爲輕量級進程,是程序執行的最小單元。和上文中同樣的,一個進程可對應多個線程,而一個線程只屬於一個進程。
進程的執行是以線程爲單位進行得,好比說一個簡單「hello world」程序只有一個線程,就是main()函數對應的線程。
線程的構成:
1)線程ID。用於標實線程
2)當前指令指針PC。標明下一指令執行點
3)寄存器集合和堆棧。該線程的可用空間
多線程
大多數軟件應用中,線程的數量都不止一個。多線程可用併發的執行,並共享進程的全局便來那個和堆的數據。
多線程優點:
1)某個操做可能會陷入長時間的等待。採用多線程,當一個線程等待的時候,可執行其餘線程,充分利用cpu。
2)某操做(如計算)可能會消耗大量時間,而致使和用戶之間的交互中斷。多線程可以讓一個線程負責計算,另外一線程負責交互。
3)軟件自己就要求併發操做,如多端下載軟件
4)多核cpu,自己就具有同時執行多個線程的能力
線程訪問權限
通常來講線程能訪問進程內存中的全部數據,但實際應用中線程也有本身的空間
1)棧(可能被其餘進程訪問,但仍可認爲是私有數據)
2)線程局部存儲,通常只有很小容量
3)寄存器(包括PC寄存器)
線程調度
最好的狀況是:當處理器數量大於要處理的線程數目的時候,全部線程均可以同時執行。實現真正意義上的併發。
而這種狀況在現實中基本不可能。現實中的併發只是一種模擬出來的狀態,特別是在單核處理對於多線程的時候。它經過讓多個線程交替執行,每一個線程執行很短期,從表面上看,這些線程同時執行,實現併發。
每一個線程都想被執行,可是每次執行的線程數量是有限的,因此就要有一種方法來從衆多的線程中選出要執行的線程,如今討論下單核的狀況,多核的相似。
在操做系統中有專門的線程調度算法來實現,下面列幾個簡單的「調度算法」
一、「先進先出」策略。全部的線程組成一個隊列,新生的線程加入到隊列末尾,每次取隊頭執行。有一個缺陷就是,若是新生成的線程是緊急操做,須要操做系統儘快相應,這種調度方法就不能知足了。
二、按優先級調度。每一個線程都有本身的優先級,而且是能夠被操做系統修改的,調度時候每次選取優先級最高的執行。這種方法彌補了上一種方法的缺陷。對於須要及時相應的緊急事件,能夠給他一個高優先級,這樣就能在下次被調度。然而這種方法也有一個問題,也就是所謂的飢餓。若是某線程「看似」可有可無,被給予一個低得優先級,之後每次產生的線程優先級都比他高,那麼這個線程會一直得不到執行,成爲餓死。一個解決的辦法是隨之事件的推移而提高線程的優先級。這樣只要事件足夠長,低優先級的線程也會得到高優先級而被執行。
從上面的分析來開,線程彷佛有兩個狀態:執行和不執行(等待)。其實操做系統中的每一個線程都對應三個狀態。
線程的狀態:
上面說了線程調度的問題,只有準備就緒的線程(這種線程稱爲就緒的線程),才能被調度。調度之後,線程就在處理器中被執行,這時線程的狀態爲運行時。若是該線程在等待某種事件的發生(如響應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,則進入等待狀態。
互斥量
和二元信號類似,資源只能被一個線程訪問,可是同一個信號量只能被獲取該信號量的線程釋放,也就是說對於二元信號量,同一個互斥量能夠被別的(任意)線程釋放。相對二元信號量來講更嚴格了。
臨界區
是比互斥量更嚴格的同步手段。臨界區和上面的區別在於,互斥量和信號量在任何進程都是可見的,也就是說,一個進程建立了互斥量和信號量,在其餘進程都是可見的,而臨界區的做用範圍僅限於本進程。
讀寫鎖
讀寫鎖用於更加通用的場合。對於同一個數據,多個進程同時讀是沒問題的,可是若是有線程要對數據進行修改,就要使用同步手段來避免出錯。對於同一個讀寫鎖,有兩種獲取方式:
一、共享的,對數據只進行讀操做,能夠多個線程同時進行
二、獨佔的。會修改數據,在修改完成以前,不能有其餘線程操做數據
當鎖處於自由狀態時,以任何一種方式獲取鎖都會成功,並將鎖至於相應的狀態。
若是鎖處於共享狀態,那麼其餘與以共享方式獲取鎖的線程都能成功,共同讀數據。
對於獨佔式獲取的,則要等到以共享方式獲取的全部線程釋放後(鎖從新回到自由狀態)才能獲取。而且對於以獨佔方式獲取的鎖,其餘任何對鎖的請求都不會成功。
條件變量
條件變量相似於一個發令槍,能夠有多個線程等待槍響,槍響的時候,這些等待槍響的線程會同時恢復執行。發令槍什麼時候響也可由線程來決定。
也就是說,條件變量可讓多個線程等待某件事的發生,當時間發生時(條件變量被喚醒),全部的線程能夠一塊兒恢復執行。