概述
簡而言之,所謂并發(fā)編程是指在一臺處理器上“同時”處理多個任務(wù)怯晕。
隨著硬件的發(fā)展缸棵,并發(fā)程序變得越來越重要。Web服務(wù)器會一次處理成千上萬的請求吧凉。平板電腦和手機app在渲染用戶畫面同時還會后臺執(zhí)行各種計算任務(wù)和網(wǎng)絡(luò)請求阀捅。即使是傳統(tǒng)的批處理問題--讀取數(shù)據(jù),計算凄诞,寫輸出--現(xiàn)在也會用并發(fā)來隱藏掉I/O的操作延遲以充分利用現(xiàn)代計算機設(shè)備的多個核心帆谍。計算機的性能每年都在以非線性的速度增長轴咱。
宏觀的并發(fā)是指在一段時間內(nèi),有多個程序在同時運行窖剑。
并發(fā)在微觀上,是指在同一時刻只能有一條指令執(zhí)行酪术,但多個程序指令被快速的輪換執(zhí)行绘雁,使得在宏觀上具有多個進程同時執(zhí)行的效果,但在微觀上并不是同時執(zhí)行的欣除,只是把時間分成若干段挪略,使多個程序快速交替的執(zhí)行杠娱。
并行和并發(fā)
并行(parallel): 指在同一時刻,有多條指令在多個處理器上同時執(zhí)行禽拔。
并發(fā)(concurrency): 指在同一時刻只能有一條指令執(zhí)行睹栖,但多個進程指令被快速的輪換執(zhí)行,使得在宏觀上具有多個進程同時執(zhí)行的效果恼除,但在微觀上并不是同時執(zhí)行的梁只,只是把時間分成若干段搪锣,通過cpu時間片輪轉(zhuǎn)使多個進程快速交替的執(zhí)行。
常見并發(fā)編程技術(shù)
1. 進程并發(fā)
-
程序和進程
- 程序灰追,是指編譯好的二進制文件弹澎,在磁盤上努咐,不占用系統(tǒng)資源(cpu、內(nèi)存佩迟、打開的文件报强、設(shè)備拱燃、鎖....)
- 進程碗誉,是一個抽象的概念,與操作系統(tǒng)原理聯(lián)系緊密苍蔬。進程是活躍的程序蝴蜓,占用系統(tǒng)資源。在內(nèi)存中執(zhí)行格仲。(程序運行起來凯肋,產(chǎn)生一個進程)
-
進程狀態(tài)
進程基本的狀態(tài)有5種侮东。分別為初始態(tài),就緒態(tài)驱敲,運行態(tài)宽闲,掛起態(tài)與終止態(tài)容诬。其中初始態(tài)為進程準(zhǔn)備階段,常與就緒態(tài)結(jié)合來看狈定。
-
進程并發(fā)
在使用進程 實現(xiàn)并發(fā)時會出現(xiàn)什么問題呢纽什?- 系統(tǒng)開銷比較大稿湿,占用資源比較多押赊,開啟進程數(shù)量比較少流礁。
- 在unix/linux系統(tǒng)下罗丰,還會產(chǎn)生“孤兒進程”和“僵尸進程”萌抵。
通過前面查看操作系統(tǒng)的進程信息元镀,我們知道在操作系統(tǒng)中栖疑,可以產(chǎn)生很多的進程滔驶。在unix/linux系統(tǒng)中揭糕,正常情況下,子進程是通過父進程fork創(chuàng)建的揪漩,子進程再創(chuàng)建新的進程氢拥。
并且父進程永遠無法預(yù)測子進程 到底什么時候結(jié)束锨侯。 當(dāng)一個 進程完成它的工作終止之后囚痴,它的父進程需要調(diào)用系統(tǒng)調(diào)用取得子進程的終止?fàn)顟B(tài)。
孤兒進程:父進程先于子進程結(jié)束奕谭,則子進程成為孤兒進程痴荐,子進程的父進程成為init進程生兆,稱為init進程領(lǐng)養(yǎng)孤兒進程鸦难。
僵尸進程: 進程終止,父進程尚未回收击敌,子進程殘留資源(PCB)存放于內(nèi)核中沃斤,變成僵尸(Zombie)進程。
Windows下的進程和Linux下的進程是不一樣的捅彻,它比較懶惰鞍陨,從來不執(zhí)行任何東西诚撵,只是為線程提供執(zhí)行環(huán)境寿烟。然后由線程負責(zé)執(zhí)行包含在進程的地址空間中的代碼。當(dāng)創(chuàng)建一個進程的時候缝其,操作系統(tǒng)會自動創(chuàng)建這個進程的第一個線程内边,成為主線程待锈。
2. 線程并發(fā)
-
什么是線程?
LWP:light weight process 輕量級的進程竿音,本質(zhì)仍是進程 (Linux下)
進程:獨立地址空間春瞬,擁有PCB,最小分配資源單位随常,可看成是只有一個線程的進程抹竹。
線程: 最小的執(zhí)行單位窃判,有獨立的PCB袄琳,但沒有獨立的地址空間(共享)
區(qū)別:在于是否共享地址空間。獨居(進程)宛琅;合租(線程)嘿辟。
Windows系統(tǒng)下红伦,可以直接忽略進程的概念昙读,只談線程膨桥。因為線程是最小的執(zhí)行單位只嚣,是被系統(tǒng)獨立調(diào)度和分派的基本單位。而進程只是給線程提供執(zhí)行環(huán)境壮虫。 -
線程同步
同步即協(xié)同步調(diào)囚似,按預(yù)定的先后次序運行饶唤。線程同步贯钩,指一個線程發(fā)出某一功能調(diào)用時角雷,在沒有得到結(jié)果之前,該調(diào)用不返回雷滚。同時其它線程為保證數(shù)據(jù)一致性祈远,不能調(diào)用該功能车份。
線程不同步產(chǎn)生的現(xiàn)象叫做“與時間有關(guān)的錯誤”(time related)。為了避免這種數(shù)據(jù)混亂出爹,線程需要同步以政。
“同步”的目的,是為了避免數(shù)據(jù)混亂伴找,解決與時間有關(guān)的錯誤盈蛮。實際上,不僅線程間需要同步技矮,進程間抖誉、信號間等等都需要同步機制。
因此衰倦,所有“多個控制流袒炉,共同操作一個共享資源”的情況,都需要同步樊零。
-
互斥量mutex
Linux中提供一把互斥鎖mutex(也稱之為互斥量)。
每個線程在對資源操作前都嘗試先加鎖驻襟,成功加鎖才能操作夺艰,操作結(jié)束解鎖。
資源還是共享的沉衣,線程間也還是競爭的郁副,但通過“鎖”就將資源的訪問變成互斥操作,而后與時間有關(guān)的錯誤也不會再產(chǎn)生了豌习。
但存谎,應(yīng)注意:同一時刻,只能有一個線程持有該鎖肥隆。
當(dāng)A線程對某個全局變量加鎖訪問既荚,B在訪問前嘗試加鎖,拿不到鎖栋艳,B阻塞恰聘。C線程不去加鎖,而直接訪問該全局變量,依然能夠訪問憨琳,但會出現(xiàn)數(shù)據(jù)混亂。
所以旬昭,互斥鎖實質(zhì)上是操作系統(tǒng)提供的一把“建議鎖”(又稱“協(xié)同鎖”)篙螟,建議程序中有多線程訪問共享資源的時候使用該機制。但问拘,并沒有強制限定遍略。
因此,即使有了mutex骤坐,如果有線程不按規(guī)則來訪問數(shù)據(jù)绪杏,依然會造成數(shù)據(jù)混亂。
-
讀寫鎖
與互斥量類似纽绍,但讀寫鎖允許更高的并行性蕾久。其特性為:寫?yīng)氄迹x共享拌夏。讀寫鎖狀態(tài):
特別強調(diào):讀寫鎖只有一把僧著,但其具備兩種狀態(tài):- 讀模式下加鎖狀態(tài) (讀鎖)
- 寫模式下加鎖狀態(tài) (寫鎖)
讀寫鎖特性:
- 讀寫鎖是“寫模式加鎖”時, 解鎖前障簿,所有對該鎖加鎖的線程都會被阻塞盹愚。
- 讀寫鎖是“讀模式加鎖”時, 如果線程以讀模式對其加鎖會成功站故;如果線程以寫模式加鎖會阻塞皆怕。
- 讀寫鎖是“讀模式加鎖”時, 既有試圖以寫模式加鎖的線程西篓,也有試圖以讀模式加鎖的線程愈腾。那么讀寫鎖會阻塞隨后的讀模式鎖請求。優(yōu)先滿足寫模式鎖岂津。讀鎖顶滩、寫鎖并行阻塞,寫鎖優(yōu)先級高寸爆。
讀寫鎖也叫共享-獨占鎖礁鲁。當(dāng)讀寫鎖以讀模式鎖住時,它是以共享模式鎖住的赁豆;當(dāng)它以寫模式鎖住時仅醇,它是以獨占模式鎖住的。寫?yīng)氄寄е帧⒆x共享析二。
讀寫鎖非常適合于對數(shù)據(jù)結(jié)構(gòu)讀的次數(shù)遠大于寫的情況。
3. 協(xié)程并發(fā)
協(xié)程:coroutine。也叫輕量級線程叶摄。
與傳統(tǒng)的系統(tǒng)級線程和進程相比属韧,協(xié)程最大的優(yōu)勢在于“輕量級”「蛳牛可以輕松創(chuàng)建上萬個而不會導(dǎo)致系統(tǒng)資源衰竭宵喂。而線程和進程通常很難超過1萬個。這也是協(xié)程別稱“輕量級線程”的原因会傲。
一個線程中可以有任意多個協(xié)程锅棕,但某一時刻只能有一個協(xié)程在運行,多個協(xié)程分享該線程分配到的計算機資源淌山。
多數(shù)語言在語法層面并不直接支持協(xié)程裸燎,而是通過庫的方式支持,但用庫的方式支持的功能也并不完整泼疑,比如僅僅提供協(xié)程的創(chuàng)建德绿、銷毀與切換等能力。如果在這樣的輕量級線程中調(diào)用一個同步 IO 操作退渗,比如網(wǎng)絡(luò)通信脆炎、本地文件讀寫,都會阻塞其他的并發(fā)執(zhí)行輕量級線程氓辣,從而無法真正達到輕量級線程本身期望達到的目標(biāo)秒裕。
在協(xié)程中,調(diào)用一個任務(wù)就像調(diào)用一個函數(shù)一樣钞啸,消耗的系統(tǒng)資源最少几蜻!但能達到進程、線程并發(fā)相同的效果体斩。
在一次并發(fā)任務(wù)中梭稚,進程、線程絮吵、協(xié)程均可以實現(xiàn)弧烤。從系統(tǒng)資源消耗的角度出發(fā)來看,進程相當(dāng)多蹬敲,線程次之暇昂,協(xié)程最少。
Go并發(fā)
1. 什么是Goroutine
goroutine是Go并行設(shè)計的核心伴嗡。goroutine說到底其實就是協(xié)程急波,它比線程更小,十幾個goroutine可能體現(xiàn)在底層就是五六個線程瘪校,Go語言內(nèi)部幫你實現(xiàn)了這些goroutine之間的內(nèi)存共享澄暮。執(zhí)行g(shù)oroutine只需極少的棧內(nèi)存(大概是4~5KB)名段,當(dāng)然會根據(jù)相應(yīng)的數(shù)據(jù)伸縮。也正因為如此泣懊,可同時運行成千上萬個并發(fā)任務(wù)伸辟。goroutine比thread更易用、更高效馍刮、更輕便信夫。
一般情況下,一個普通計算機跑幾十個線程就有點負載過大了渠退,但是同樣的機器卻可以輕松地讓成百上千個goroutine進行資源競爭忙迁。
2. Goroutine的創(chuàng)建
只需在函數(shù)調(diào)?語句前添加 go 關(guān)鍵字脐彩,就可創(chuàng)建并發(fā)執(zhí)?單元碎乃。開發(fā)?員無需了解任何執(zhí)?細節(jié),調(diào)度器會自動將其安排到合適的系統(tǒng)線程上執(zhí)行惠奸。
在并發(fā)編程中梅誓,我們通常想將一個過程切分成幾塊,然后讓每個goroutine各自負責(zé)一塊工作佛南,當(dāng)一個程序啟動時梗掰,主函數(shù)在一個單獨的goroutine中運行,我們叫它main goroutine嗅回。新的goroutine會用go語句來創(chuàng)建及穗。而go語言的并發(fā)設(shè)計,讓我們很輕松就可以達成這一目的绵载。
示例代碼:
package main
import (
"fmt"
"time"
)
func newTask() {
i := 0
for {
i++
fmt.Printf("new goroutine: i = %d\n", i)
time.Sleep(1 * time.Second) //延時1s
}
}
func main() {
//創(chuàng)建一個 goroutine埂陆,啟動另外一個任務(wù)
go newTask()
i := 0
//main goroutine 循環(huán)打印
for {
i++
fmt.Printf("main goroutine: i = %d\n", i)
time.Sleep(1 * time.Second) //延時1s
}
}
3. Goroutine特性
主goroutine退出后,其它的工作goroutine也會自動退出:
package main
import (
"fmt"
"time"
)
func newTask() {
i := 0
for {
i++
fmt.Printf("new goroutine: i = %d\n", i)
time.Sleep(1 * time.Second) //延時1s
}
}
func main() {
//創(chuàng)建一個 goroutine娃豹,啟動另外一個任務(wù)
go newTask()
fmt.Println("main goroutine exit")
}
4. runtime包
-
Gosched
runtime.Gosched() 用于讓出CPU時間片焚虱,讓出當(dāng)前goroutine的執(zhí)行權(quán)限,調(diào)度器安排其他等待的任務(wù)運行懂版,并在下次再獲得cpu時間輪片的時候鹃栽,從該出讓cpu的位置恢復(fù)執(zhí)行。有點像跑接力賽躯畴,A跑了一會碰到代碼runtime.Gosched() 就把接力棒交給B了民鼓,A歇著了,B繼續(xù)跑蓬抄。
示例代碼:
package main
import (
"fmt"
"runtime"
)
func main() {
//創(chuàng)建一個goroutine
go func(s string) {
for i := 0; i < 2; i++ {
fmt.Println(s)
}
}("world")
for i := 0; i < 2; i++ {
runtime.Gosched() //import "runtime" 包
/*
屏蔽runtime.Gosched()運行結(jié)果如下:
hello
hello
沒有runtime.Gosched()運行結(jié)果如下:
world
world
hello
hello
*/
fmt.Println("hello")
}
}
以上程序的執(zhí)行過程如下:
主協(xié)程進入main()函數(shù)摹察,進行代碼的執(zhí)行。當(dāng)執(zhí)行到go func()匿名函數(shù)時倡鲸,創(chuàng)建一個新的協(xié)程供嚎,開始執(zhí)行匿名函數(shù)中的代碼,主協(xié)程繼續(xù)向下執(zhí)行,執(zhí)行到runtime.Gosched( )時會暫停向下執(zhí)行克滴,直到其它協(xié)程執(zhí)行完后逼争,再回到該位置,主協(xié)程繼續(xù)向下執(zhí)行劝赔。
-
Goexit
調(diào)用 runtime.Goexit() 將立即終止當(dāng)前 goroutine 執(zhí)?誓焦,調(diào)度器確保所有已注冊 defer延遲調(diào)用被執(zhí)行。示例代碼:
package main
import (
"fmt"
"runtime"
)
func main() {
go func() {
defer fmt.Println("A.defer")
func() {
defer fmt.Println("B.defer")
runtime.Goexit() // 終止當(dāng)前 goroutine, import "runtime"
fmt.Println("B") // 不會執(zhí)行
}()
fmt.Println("A") // 不會執(zhí)行
}() //不要忘記()
//死循環(huán)着帽,目的不讓主goroutine結(jié)束
for {
}
}
-
GOMAXPROCS
調(diào)用 runtime.GOMAXPROCS() 用來設(shè)置可以并行計算的CPU核數(shù)的最大值杂伟,并返回之前的值。示例代碼:
package main
import (
"fmt"
)
func main() {
//n := runtime.GOMAXPROCS(1) // 第一次 測試
//打印結(jié)果:111111111111111111110000000000000000000011111...
n := runtime.GOMAXPROCS(2) // 第二次 測試
//打印結(jié)果:010101010101010101011001100101011010010100110...
fmt.Printf("n = %d\n", n)
for {
go fmt.Print(0)
fmt.Print(1)
}
}
在第一次執(zhí)行runtime.GOMAXPROCS(1) 時仍翰,最多同時只能有一個goroutine被執(zhí)行赫粥。所以會打印很多1。過了一段時間后予借,GO調(diào)度器會將其置為休眠越平,并喚醒另一個goroutine,這時候就開始打印很多0了灵迫,在打印的時候秦叛,goroutine是被調(diào)度到操作系統(tǒng)線程上的。
在第二次執(zhí)行runtime.GOMAXPROCS(2) 時瀑粥, 我們使用了兩個CPU挣跋,所以兩個goroutine可以一起被執(zhí)行,以同樣的頻率交替打印0和1狞换。