程序員的自我修養(yǎng):溫故而知新
1.1從Hello World說起
目的:從最基本的編譯,靜態(tài)鏈接到操作系統(tǒng)如何轉(zhuǎn)載程序,動態(tài)鏈接及運(yùn)行庫和標(biāo)準(zhǔn)庫的實(shí)現(xiàn),和一些操作系統(tǒng)的機(jī)制。了解計(jì)算機(jī)上程序運(yùn)行的一個基本脈絡(luò)。
1.2變不離其宗
計(jì)算機(jī)最關(guān)鍵的三個部分:CPU,內(nèi)存,I/O控制芯片。
- 早期的計(jì)算機(jī):沒有復(fù)雜的圖形功能,CPU和內(nèi)存頻率一樣,都連接在同一個總線上。
- CPU頻率提升:內(nèi)存跟不上CPU,產(chǎn)生了和內(nèi)存頻率一致的系統(tǒng)總線,CPU使用倍頻的方式和總線通信。
- 圖形界面的出現(xiàn):圖形芯片需要和內(nèi)存和CPU大量交換數(shù)據(jù),慢速的I/O總線無法滿足圖形設(shè)備的巨大需求。為了高效處理數(shù)據(jù),設(shè)計(jì)了一個高速的北橋芯片。后來有設(shè)計(jì)處理低速處理設(shè)備南橋芯片,磁盤,USB,鍵盤都是連接在南橋上。在由南橋?qū)⑺鼈儏R總到北橋上。
北橋
- 北橋左邊CPU和cache:CPU負(fù)責(zé)所有控制和運(yùn)算。
- 北橋下面PCI總線
- 北橋右邊memory
SMP和多核
現(xiàn)在CPU已經(jīng)達(dá)到物理極限,被4GHz所限制,于是,開始通過增加CPU數(shù)量來提高計(jì)算機(jī)速度。
對稱多處理器(SMP):最常見的一種形式。每個CPU在系統(tǒng)中所處的地位和所發(fā)揮的功能是一樣,是相互對稱的。但在處理程序時,我們并不能把他們分成若干個不相干的子問題,所以,使得多處理器速度實(shí)際提高得并沒有理論上那么高。當(dāng)對于相互獨(dú)立的問題,多處理器就能最大效能的發(fā)揮威力了(比如:大型數(shù)據(jù)庫,網(wǎng)絡(luò)服務(wù)等)。
對處理器由于造價比較高昂,主要用在商用電腦上,對于個人電腦,主要是多核處理器。
多核處理器:其實(shí)際上是(SMP)的簡化版,思想是將多個處理器合并在一起打包出售,它們之間共享比較昂貴的緩存部件,只保留了多個核心。在邏輯上看,它們和SMP完全相同。
1.3站得高,看得遠(yuǎn)
系統(tǒng)軟件:一般用于管理計(jì)算機(jī)本地的軟件。
主要分為兩塊:
- 平臺性的:操作系統(tǒng)內(nèi)核,驅(qū)動程序,運(yùn)行庫。
- 程序開發(fā):編譯器,匯編器,鏈接器。
計(jì)算機(jī)系統(tǒng)軟件體系結(jié)構(gòu)采用一種層的結(jié)構(gòu)。
每個層次之間都需要相互通信,那么它們之間就有通信協(xié)議,我們將它稱為接口,接口下層是提供者,定義接口。上層是使用者,使用接口實(shí)現(xiàn)所需功能。
除了硬件和應(yīng)用程序,其他的都是中間層,每個中間層都是對它下面的那層的包裝和擴(kuò)展。它們使得應(yīng)用程序和硬件之間保持相對獨(dú)立。
從整個層次結(jié)構(gòu)來看,開發(fā)工具與應(yīng)用程序?qū)儆谕粋€層次,因?yàn)樗鼈兌际褂猛粋€接口—操作系統(tǒng)應(yīng)用程序編程接口。應(yīng)用程序接口提供者是運(yùn)行庫,什么樣的運(yùn)行庫提供什么樣的接口。winsows的運(yùn)行庫提供Windows API,Linux下的Gliba庫提供POSIX的API。
運(yùn)行庫使用操作系統(tǒng)提供的系統(tǒng)調(diào)用接口。
系統(tǒng)調(diào)用接口在實(shí)現(xiàn)中往往以軟件中斷的方式提供。
操作系統(tǒng)內(nèi)核層對于硬件層來說是硬件接口的使用者,而硬件是接口的定義者。這種接口叫做硬件規(guī)格。
1.4操作系統(tǒng)做了什么
操作系統(tǒng)的一個功能是提供抽象的接口,另外一個主要功能是管理硬件資源。
一個計(jì)算機(jī)中的資源主要分CPU,存儲器(包括內(nèi)存和磁盤)和I/O設(shè)備。下面從這3個方面來看如何挖機(jī)它們。
1.4.1不要讓CPU打盹
多道程序:編譯一個監(jiān)控程序,當(dāng)程序不需要使用CPU時,將其他在等待CPU的程序啟動。但它的弊端是不分輕重緩急,有時候一個交互操作可能要等待數(shù)十分鐘。
改進(jìn)后
分時系統(tǒng):每個CPU運(yùn)行一段時間后,就主動讓出給其他CPU使用。完整的操作系統(tǒng)雛形在此時開始出現(xiàn)。但當(dāng)一個程序死機(jī)的時候,無法主動讓出CPU,那么,整個系統(tǒng)都無法響應(yīng)。
目前操作系統(tǒng)采用的方式
多任務(wù)系統(tǒng):操作系統(tǒng)接管了所有的硬件資源,并且本身運(yùn)行在一個受硬件保護(hù)的級別。所有的應(yīng)用都以進(jìn)程的方式運(yùn)行在比操作系統(tǒng)更低的級別,每個進(jìn)程都有自己獨(dú)立的地址空間,使得進(jìn)程之間的地址空間相互隔離。CPU由操作系統(tǒng)進(jìn)行同一分配,每個進(jìn)程根據(jù)進(jìn)程優(yōu)先級的高低都有機(jī)會獲得CPU,但如果運(yùn)行超過一定的時間,CPU會將資源分配給其他進(jìn)程,這種CPU分配方式是搶占式。如果操作系統(tǒng)分配每個進(jìn)程的時間很短,就會造成很多進(jìn)程都在同時運(yùn)行的假象,即所謂的宏觀并行,微觀串行。
設(shè)備驅(qū)動
操作系統(tǒng)作為硬件層的上層,它是對硬件的管理和抽象。
對于操作系統(tǒng)上面的運(yùn)行庫和應(yīng)用程序來說,它們只希望看到一個統(tǒng)一的硬件訪問模式。
當(dāng)成熟的操作系統(tǒng)出現(xiàn)后,硬件逐漸成了抽象的概念。在UNIX中,硬件設(shè)備的訪問形式和訪問普通的文件形式一樣。在Windows系統(tǒng)中,圖形硬件被抽象成GDI,聲音和多媒體設(shè)備被抽象成DirectX對象,磁盤被抽象成普通文件系統(tǒng)。
這些繁瑣的硬件細(xì)節(jié)全都交給了操作系統(tǒng)中的硬件驅(qū)動。
文件系統(tǒng)管理這磁盤的存儲方式。
磁盤的結(jié)構(gòu):一個硬盤往往有多個盤片,每個盤片分兩面,每面按照同心圓劃分為若干磁道,每個磁道劃分為若干扇區(qū),每個扇區(qū)一般512字節(jié)。
LBA:整個硬盤中所有扇區(qū)從0開始編號,一直到最后一個扇區(qū),這個扇區(qū)編號叫做邏輯扇區(qū)號。
文件系統(tǒng)保存了這些文件的存儲結(jié)構(gòu),負(fù)責(zé)維護(hù)這些數(shù)據(jù)結(jié)構(gòu)并且保證磁盤中的扇區(qū)能有效的組織和利用。
1.5內(nèi)存不夠怎么辦
在早期計(jì)算機(jī)中,程序是直接運(yùn)行在物理內(nèi)存上的,程序所訪問的都是物理地址。
那么如何將計(jì)算機(jī)有限的地址分配給多個程序使用。
直接按物理內(nèi)存分配將產(chǎn)生很多問題:
- 地址空間不隔離:所有的程序都直接訪問物理地址,導(dǎo)致程序使用的物理地址不是相互隔離的,惡意的程序很容易串改其他程序的內(nèi)存數(shù)據(jù)。
- 內(nèi)存使用效率低:由于沒有有效的內(nèi)存管理機(jī)制,通常一個程序執(zhí)行的時候,監(jiān)控程序要將整個程序讀入。內(nèi)存不夠的時候,需要先將內(nèi)存中的程序讀出,保存在硬盤上,才能將需要運(yùn)行的程序讀入。這樣會使得整個過程有大量數(shù)據(jù)換入換出。
- 程序運(yùn)行地址不確定:每次程序運(yùn)行都需要內(nèi)存分配一塊足夠大的內(nèi)存空間,使得這個地址是不確定的。但在程序編寫的時候,他訪問的數(shù)據(jù)和指令跳轉(zhuǎn)的目標(biāo)地址都是固定的,這就涉及到了程序的重定向問題。
一種解決辦法:
中間層:使用一種間接的地址訪問方法,我們把程序給出的地址看作一種虛擬地址。虛擬地址是物理地址的映射,只要處理好這個過程,就可以起到隔離的作用。
1.5.1關(guān)于隔離
普通的程序它只需要一個簡單的執(zhí)行環(huán)境,一個單一的地址空間,有自己的CPU。
地址空間比較抽象,如果把它想象成一個數(shù)組,每一個數(shù)組是一字節(jié),數(shù)組大小就是地址空間的長度,那么32位的地址空間大小就是2^32=4294967296字節(jié),即4G,地址空間有效位是0x00000000~0xFFFFFFFF。
地址空間分為兩種:
物理空間:就是物理內(nèi)存。32位的機(jī)器,地址線就有32條,物理空間4G,但如果值裝有512M的內(nèi)存,那么實(shí)際有效的空間地址就是0x00000000~0x1FFFFFFF,其他部分都是無效的。
虛擬空間:每個進(jìn)程都有自己獨(dú)立的虛擬空間,而且每個進(jìn)程只能訪問自己的空間地址,這樣就有效的做到了進(jìn)程隔離。
1.5.2分段
基本思路:把一段與程序所需要的內(nèi)存空間大小的虛擬空間映射到某個地址空間。虛擬空間的每個字節(jié)對應(yīng)物理空間的每個字節(jié)。這個映射過程由軟件來完成。
分段的方式可以解決之前的第一個(地址空間不隔離)和第三個問題(程序運(yùn)行地址不確定)
第二問題內(nèi)存使用效率問題依舊沒有解決。
1.5.3分頁
基本方法:把地址空間人為的分成固定大小的頁,每一頁大小有硬件決定或硬件支持多種大小的頁,由操作系統(tǒng)決定頁的大小。
目前幾乎所有的PC上的操作系統(tǒng)都是4KB大小的頁。
我們把進(jìn)程的虛擬地址空間按頁分割,把常用的數(shù)據(jù)和代碼頁轉(zhuǎn)載到內(nèi)存中,把不常用的代碼和數(shù)據(jù)保存到磁盤里,當(dāng)需要的時候從磁盤取出來。
虛擬空間的頁叫做虛擬頁(VP),物理內(nèi)存中頁叫做物理頁,把磁盤中的頁叫做磁盤頁。虛擬空間的有的頁被映射到同一個物理頁,這樣就可以實(shí)現(xiàn)內(nèi)存共享。
當(dāng)進(jìn)程需要一個頁時,這個頁是磁盤頁時,硬件會捕獲到這個消息,就是所謂的頁錯誤,然后操作系統(tǒng)接管進(jìn)程,負(fù)責(zé)從磁盤中讀取內(nèi)容裝入內(nèi)存中,然后再將內(nèi)存和這個頁建立映射關(guān)系。
保護(hù)也是頁映射的目的之一,每個頁都可以設(shè)置權(quán)限屬性,只有操作系統(tǒng)可以修改這些屬性,這樣操作系統(tǒng)就可以保護(hù)自己保護(hù)進(jìn)程。
虛擬存儲的實(shí)現(xiàn)需要依靠硬件支持,所有硬件都采用一個叫做MMU的部件來進(jìn)行頁映射。
CPU發(fā)出虛擬地址經(jīng)過MMU轉(zhuǎn)換成物理地址,MMU一般都集成在CPU內(nèi)部。
1.6眾人拾柴火焰高
1.6.1線程基礎(chǔ)
多線程現(xiàn)在作為實(shí)現(xiàn)軟件并發(fā)執(zhí)行的一個重要方法,具有越來越重的地位。
什么是線程
線程有時被稱為輕量級的進(jìn)程,是程序執(zhí)行流的最小單位。
構(gòu)成:
- 線程ID
- 當(dāng)前指令指針
- 寄存器集合
- 堆棧空間(代碼段,數(shù)據(jù)段,堆)
- 進(jìn)程級的資源(打開文件和信號)
線程與進(jìn)程的關(guān)系:
多線程可以互不干擾的并發(fā)執(zhí)行,并共享進(jìn)程的全局變量和堆的數(shù)據(jù)。
使用多線程的原因有如下幾點(diǎn):
- 某個操作可能會陷入長時間等待,等待的線程會進(jìn)入睡眠狀態(tài),無法繼續(xù)執(zhí)行。
- 某個操作會消耗大量的時間,如果只有一個線程,程序和用戶之間的交互會中斷。
- 程序邏輯本身就要求并發(fā)操作。
- 多CPU或多核計(jì)算機(jī),本身具備同時執(zhí)行多個線程的能力。
- 相對于多進(jìn)程應(yīng)用,多線程在數(shù)據(jù)共享方面效率要高很多。
線程的訪問權(quán)限
線程的訪問非常自由,它可以訪問進(jìn)程內(nèi)存里所有數(shù)據(jù),包括其他線程的堆棧(如果知道地址的話,情況很少見)。
線程自己的私用存儲空間:
- 棧(并發(fā)完全無法被其他線程訪問)
- 線程局部存儲。某些操作系統(tǒng)為線程提供私用空間,但容量有限。
- 寄存器。執(zhí)行流的基本數(shù)據(jù),為線程私用。
線程私用 | 線程間共享(進(jìn)程所有) |
---|---|
局部變量 | 全局變量 |
函數(shù)參數(shù) | 堆上數(shù)據(jù) |
TLS數(shù)據(jù) | 函數(shù)里的靜態(tài)變量 |
程序代碼 | |
打開的文件,A線程打開的文件可以由B線程讀取 |
線程調(diào)度與優(yōu)先級
不論在多處理器還是單處理器上,線程都是“并發(fā)”的。
線程數(shù)量小于處理器數(shù)量時,是真正并發(fā)的。
單處理器下,并發(fā)是模擬的,操作系統(tǒng)會讓這些多線程程序輪流執(zhí)行,每次都只執(zhí)行一小段時間,這就稱為線程調(diào)度。
線程調(diào)度中,線程擁有三種狀態(tài):
- 運(yùn)行:線程正在執(zhí)行
- 就緒:線程可以立刻運(yùn)行,但CPU被占用
- 等待:線程正在等待某一事件發(fā)生,無法立即執(zhí)行。
處于運(yùn)行中的線程擁有一段可以執(zhí)行的時間,這稱為時間片,當(dāng)時間片用盡的時候,進(jìn)程進(jìn)入就緒狀態(tài),如果在用盡之前開始等待某事件,那么它就進(jìn)入等待狀態(tài)。每當(dāng)一個線程離開運(yùn)行狀態(tài)的時候,調(diào)度系統(tǒng)就會選擇一個其他的就緒線程繼續(xù)執(zhí)行。
現(xiàn)在的主流調(diào)度方法盡管都不一樣,但基本都帶有優(yōu)先級調(diào)度和輪轉(zhuǎn)法。
輪轉(zhuǎn)法:各個線程輪流執(zhí)行一段時間。
優(yōu)先級調(diào)度:按線程的優(yōu)先級來輪流執(zhí)行,每個線程都擁有各自的線程優(yōu)先級。
在win和lin里面,線程優(yōu)先級不僅可以由用戶手動設(shè)置,系統(tǒng)還會根據(jù)不同線程表現(xiàn)自動調(diào)整優(yōu)先級。
一般頻繁等待的線程稱之為IO密集型線程,而把很少等待的線程稱為CPU密集型線程。
優(yōu)先級調(diào)度下,存在一種餓死現(xiàn)象。
餓死:線程優(yōu)先級較低,在它執(zhí)行之前,總是有較高級的線程要執(zhí)行,所以,低優(yōu)先級線程總是無法執(zhí)行的。
當(dāng)一個CPU密集型線程獲得較高優(yōu)先級時,許多低優(yōu)先級線程就可能被餓死。
為了避免餓死,操作系統(tǒng)常常會逐步提升那些等待時間過長的線程。
線程優(yōu)先級改變一般有三種方式:
- 用戶指定優(yōu)先級
- 根據(jù)進(jìn)入等待狀態(tài)的頻繁程度提升或降低優(yōu)先級
- 長時間得不到執(zhí)行而被提升優(yōu)先級
可搶占線程和不可搶占線程
搶占:在線程用盡時間片之后被強(qiáng)制剝奪繼續(xù)執(zhí)行的權(quán)利,而進(jìn)入就緒狀態(tài)。
在早期的系統(tǒng)中,線程是不可搶占的,線程必須主動進(jìn)入就緒狀態(tài)。
在不可搶占線程中,線程主動放棄主要是2種:
- 當(dāng)線程試圖等待某個事件(I/O)時
- 線程主動放棄時間片
不可搶占線程有一個好處,就是線程調(diào)度只會發(fā)生在線程主動放棄執(zhí)行或線程等待某個事件的時候,這樣就可以避免一些搶占式線程時間不確定而產(chǎn)生的問題。
Linux的多線程
Linux內(nèi)核中并不存在真正意義上的線程概念。Linux所有執(zhí)行實(shí)體(線程和進(jìn)程)都稱為任務(wù),每一個任務(wù)概念上都類似一個單線程的進(jìn)程,具有內(nèi)存空間,執(zhí)行實(shí)體,文件資源等。Linux不同任務(wù)之間可以選擇共享內(nèi)存空間,相當(dāng)于同一個內(nèi)存空間的多個任務(wù)構(gòu)成一個進(jìn)程,這些任務(wù)就是進(jìn)程中的線程。
系統(tǒng)調(diào)用 | 作用 |
---|---|
fork | 復(fù)制當(dāng)前線程 |
exec | 使用新的可執(zhí)行映像覆蓋當(dāng)前可執(zhí)行映像 |
clone | 創(chuàng)建子進(jìn)程并從指定位置開始執(zhí)行 |
fork產(chǎn)生新任務(wù)速度非常快,因?yàn)閒ork不復(fù)制原任務(wù)的內(nèi)存空間,而是和原任務(wù)一起共享一個寫時復(fù)制的內(nèi)存空間。
寫時復(fù)制:兩個任務(wù)可以同時自由讀取內(nèi)存,當(dāng)任意一個任務(wù)試圖對內(nèi)存進(jìn)行修改時,內(nèi)存就會復(fù)制一份單獨(dú)提供給修改方使用。
fork只能夠產(chǎn)生本任務(wù)的鏡像,因此需要和exec配合才能啟動別的新任務(wù)。
而如果要產(chǎn)生新線程,則使用clone。
clone可以產(chǎn)生一個新的任務(wù),從指定位置開始執(zhí)行,并且共享當(dāng)前進(jìn)程的內(nèi)存空間和文件等,實(shí)際效果就是產(chǎn)生一個線程。
1.6.2線程安全
多線程程序處于一個多變的環(huán)境中,可訪問的全局變量和堆數(shù)據(jù)隨時都可能被其他的線程改變。因此多線程程序在并發(fā)時數(shù)據(jù)的一致性變得非常重要。
競爭與原子操作
++i的實(shí)現(xiàn)方法:
- 讀取i到某個寄存器X
- X++
-
將X的內(nèi)容存儲回i
單條指令的操作稱為原子的,單挑指令的執(zhí)行不會被打斷。在windows里,有一套API專門進(jìn)行一些原子操作,這些API稱為Interlocked API。
同步與鎖
為了防止多個線程讀取同一個數(shù)據(jù)產(chǎn)生不可預(yù)料結(jié)果,我們將各個線程對一個數(shù)據(jù)的訪問同步。
同步:在一個線程對一個數(shù)據(jù)訪問結(jié)束的時候,其他線程不能對同一個數(shù)據(jù)進(jìn)行訪問。對數(shù)據(jù)的訪問被原子化。
鎖:鎖是一種非強(qiáng)制機(jī)制,每一個線程在訪問數(shù)據(jù)或者資源之前會先獲取鎖,在訪問結(jié)束后會釋放鎖。在鎖被占用時候試圖獲取鎖時,線程會等待,知道鎖可以重新使用。
二元信號量:最簡單的鎖,它適合只能被唯一一個線程獨(dú)占訪問的資源,它的兩種狀態(tài):
- 非占用狀態(tài):第一個獲取該二元信號量的線程會獲得該鎖,并將二元信號量置為占用狀態(tài),其他所有訪問該二元信號量線程將會等待。
- 占用狀態(tài)
信號量:允許多個線程并發(fā)訪問的資源。一個初始值為N的信號量允許N個線程并發(fā)訪問。
操作如下:
- 將信號量值鍵1
- 如果信號量值小于0,就進(jìn)入等待狀態(tài)。
訪問完資源后,線程釋放信號量:
- 將信號量加1
- 如果信號量的值小于1,喚醒一個等待中的線程。
互斥量:和二元信號量很類似,但和信號量不同的是:信號量在一個系統(tǒng)中,可以被任意線程獲取或釋放。互斥量要求那個線程獲取互斥量,那么哪個線程就釋放互斥量,其他線程釋放無效。
臨界區(qū):比互斥量更加嚴(yán)格的手段。把臨界區(qū)的鎖獲取稱為進(jìn)入臨界區(qū),而把鎖的釋放稱為離開臨界區(qū)。臨界區(qū)和互斥量,信號量區(qū)別在與互斥量,信號量在系統(tǒng)中任意進(jìn)程都是可見的。臨界區(qū)的作用范圍僅限于本線程,其他線程無法獲取。其他性質(zhì)與互斥量相同。
讀寫鎖:致力于一種更加特定的場合的同步。如果使用之前使用的信號量、互斥量或臨界區(qū)中的任何一種進(jìn)行同步,對于讀取頻繁,而僅僅是偶爾寫入的情況會顯得非常低效。讀寫鎖可以避免這個問題。對于同一個鎖,讀寫鎖有兩種獲取方式:
- 共享的
- 獨(dú)占的
讀寫鎖的總結(jié)
讀寫鎖狀態(tài) | 以共享方式獲取 | 以獨(dú)占方式獲取 |
---|---|---|
自由 | 成功 | 成功 |
共享 | 成功 | 等待 |
獨(dú)占 | 等待 | 等待 |
條件變量:作為同步的手段,作用類似于一個柵欄。對于條件變量,線程有兩個操作:
- 線程可以等待條件變量,一個條件變量可以被多個線程等待
- 線程可以喚醒條件變量,此時某個或所有等待此條件變量的線程都會被喚醒并繼續(xù)支持
使用條件變量可以讓許多線程一起等待某個事件的發(fā)生,當(dāng)事件發(fā)生時,所有線程可以一起恢復(fù)執(zhí)行。
可重入與線程安全
一個函數(shù)被重入,表示這個函數(shù)沒有執(zhí)行完成,由于外部因素或內(nèi)部調(diào)用,又一次進(jìn)入該函數(shù)執(zhí)行。
一個函數(shù)要被重入,只有兩種情況:
- 多個線程同時執(zhí)行這個函數(shù)
- 函數(shù)自身(可能經(jīng)過多層調(diào)用之后)調(diào)用自身
一個函數(shù)被稱為可重入,表示重入之后不會產(chǎn)生任何不良影響
可重入函數(shù):
1
2
3
4
|
int sqr(int x)
{
return x * x;
}
|
一個函數(shù)要成為可重入,必須具有如下特點(diǎn):
- 不使用任何(局部)靜態(tài)或全局的非const變量
- 不返回任何(局部)靜態(tài)或全部的非const變量的指針
- 僅依賴調(diào)用方提供的參數(shù)
- 不依賴任何單個資源的鎖
- 不調(diào)用任何不可重入的函數(shù)
可重入是并發(fā)安全的強(qiáng)力保障,一個可重入的函數(shù)可以在多程序環(huán)境下方向使用
過度優(yōu)化
有時候合理的合理的使用了鎖也不一定能保證線程的安全。
1
2
3
4
5
6
7
8
9
10
|
//Thread1
x=0;
lock();
x++;
unlock();
//Thread2
x=0;
lock();
x++;
unlock();
|
上面X的值應(yīng)該為2,但如果編譯器為了提高X的訪問速度,把X放到了某個寄存器里面,不同線程的寄存器是各自獨(dú)立的,因此,如果Thread1先獲得鎖,則程序的執(zhí)行可能會呈現(xiàn)如下:
[Thread1]讀取x的值到某個寄存器R [1] (R[1]=0);
[Thread1]R[1]++(由于之后可能要訪問到x,所以Thread1暫時不將R[1]寫回x);
[Thread2]讀取x的值到某個寄存器R[2] (R[2]=0);
[Thread2]R[2]++(R[2]=1);
[Thread2]將R[2]寫回至x(x=1);
[Thread1] (很久以后)將R[1]寫回至x(x=1);
如果這樣,即使加鎖也不能保證線程安全
1
2
3
4
5
6
7
|
x=y=0;
//Thread1
x=1;
r1=y;
//Thread2
y=1;
r2=x;
|
上面代碼有可能發(fā)生r1=r2=0的情況。
CPU動態(tài)調(diào)度:在執(zhí)行程序的時候,為了提高效率有可能交換指令的順序。
編譯器在進(jìn)行優(yōu)化的時候,也可能為了效率交換兩個毫不相干的相鄰指令的執(zhí)行順序。
上面代碼執(zhí)行順序可能是這樣:
1
2
3
4
5
6
7
|
x=y=0;
[Thread1]
r1=y;
x=1;
[Thread2]
y=1;
r2=x;
|
使用volatile關(guān)鍵字可以阻止過度優(yōu)化,colatile可以做兩件事情:
- 阻止編譯器為了提高速度將一個變量緩存到寄存器內(nèi)而不寫回
- 阻止編譯器調(diào)整操作volatile變量的指令順序
但volatile無法阻止CPU動態(tài)調(diào)度換序
C++中,單例模式。
1
2
3
4
5
6
7
8
9
10
11
12
|
volatile T * pInst = 0;
T * GetInstance()
{
if (pInst == NULL)
{
LOCK();
if (pInst == NULL)
pInst = new T;
unlock();
}
return pInst;
}
|
CPU的亂序執(zhí)行可能會對上面代碼照成影響
C++里的new包含兩個步驟:
- 分配內(nèi)存
- 調(diào)用構(gòu)造函數(shù)
所以pInst=new T包含三個步驟:
- 分配內(nèi)存
- 在內(nèi)存的位置上調(diào)用構(gòu)造函數(shù)
- 將內(nèi)存的地址賦值給pInst
這三步中2和3的步驟可以顛倒,可能出現(xiàn)這種情況:pInst中的值不是NULL,但對象還是沒有構(gòu)造完成。
要阻止CPU換序,可以調(diào)用一條指令,這條指令常常被稱為barrier:它會阻止CPU將該指令之前的指令交換到barrier之后。
許多體系的CPU都提供了barrier指令,不過,它們的名稱各不相同。例如POWERPC提供的指令就叫做lwsync。所以我們可以這樣保證線程安全:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
#define barrier() __asm__ volatile("lwsync")
volatile T * pInst = 0;
T * GetInstance()
{
if (pInst == NULL)
{
LOCK();
if (pInst == NULL)
{
T * temp = new T;
barrier();
pInst = temp;
}
unlock();
}
return pInst;
}
|
1.6.3多線程的內(nèi)部情況
線程的并發(fā)執(zhí)行是由多處理器或操作系統(tǒng)調(diào)度來實(shí)現(xiàn)的。windows和linux都在內(nèi)核中提供線程支持,有多處理器或調(diào)度來實(shí)現(xiàn)并發(fā)。用戶實(shí)際使用線程并不是內(nèi)核線程,而是存在于用戶態(tài)的用戶線程。用戶線程并不一定在操作系統(tǒng)內(nèi)核里對應(yīng)同等數(shù)量的內(nèi)核線程。對用戶來說,如果有三個線程同時執(zhí)行,可能在內(nèi)核中只有一個線程。
一對一模型
對于直接支持線程的系統(tǒng),一對一模型始終是最為簡單的模型。一個用戶使用的線程就唯一對應(yīng)一個內(nèi)核使用的線程,但返回來,一個內(nèi)核里面的線程在用戶態(tài)不一定有對應(yīng)的線程存在。
對于一對一模型,線程之間的并發(fā)是真正的并發(fā),一個線程因?yàn)槟硞€原因阻塞,并不會影響到其他線程。一對一模型也可以讓多線程程序在多處理器的系統(tǒng)上有更好的表現(xiàn)。
一般直接使用API或者系統(tǒng)調(diào)用創(chuàng)建的線程均為一對一線程。
一對一線程的兩個缺點(diǎn):
- 由于許多操作系統(tǒng)限制了內(nèi)核線程數(shù)量,因此一對一線程會讓用戶的線程數(shù)量受到限制。
- 許多操作系統(tǒng)內(nèi)核線程調(diào)度是,上下文切換的開銷較大,導(dǎo)致用戶線程的執(zhí)行效率下降。
多對一模型
多對一模型將多個用戶線程映射到一個內(nèi)核線程上,線程之間的切換由用戶態(tài)的代碼來進(jìn)行,相對于一對一模型,多對一模型的線程切換要快速許多。
多對一模型的問題就是如果一個用戶線程阻塞了,那么所有的線程都將無法執(zhí)行。在多處理系統(tǒng)上,處理器的增多對多對一模型的線程性能不會有明顯幫助。多對一模型得到的好處是高效的上下文切換和幾乎無限制的線程數(shù)量。
多對多模型
多對多模型結(jié)合了多對一和一對一的特點(diǎn),將多個用戶線程映射到少數(shù)但不止一個內(nèi)核線程上。
一個用戶線程阻塞并不會使得所有的用戶線程阻塞。并且對用戶線程數(shù)量也沒有什么限制,在多處理器系統(tǒng)上,多對多模型的線程也能得到一定的性能提升,不過提升的幅度沒有一對一模型高。
- 贊