Go語言goroutine、channel【重點】
Go語言goroutine、channel
進(jìn)程、線程:
①進(jìn)程就是程序程序在操作系統(tǒng)中的一次執(zhí)行過程,是系統(tǒng)進(jìn)行資源分配和調(diào)度的基本單位。
②線程是進(jìn)程的一個執(zhí)行實例,是程序執(zhí)行的最小單元,它是比進(jìn)程更小的能獨立運行的基本單位。
③一個進(jìn)程可以創(chuàng)建和銷毀多個線程,同一個進(jìn)程中的多個線程可以并發(fā)執(zhí)行。
④一個程序至少有一個進(jìn)程,一個進(jìn)程至少有一個線程。
?
并發(fā):多線程程序在單核上運行
因為是在一個cpu上,比如有10個線程,每個線程執(zhí)行10毫秒(進(jìn)行輪詢操作),從人的角度看,好像這10個線程都在運行,但是從微觀上看,在某一個時間點看,其實只有一個線程在執(zhí)行。
?
并行:多線程程序在多核上運行
因為是在多個cpu上(比如有10個cpu),比如有10個線程,每個線程執(zhí)行10毫秒(各自在不同cpu上執(zhí)行),從人的角度看,這10個線程都在運行,但是從微觀上看,在某一個時間點看,也同時有10個線程在執(zhí)行。

Go協(xié)程goroutine、Go主線程:
Go主線程(有程序員直接稱為線程?/?也可以理解成進(jìn)程):一個Go線程上,可以起多個協(xié)程goroutine,可以這樣理解,協(xié)程是輕量級的線程【編譯器做優(yōu)化】。
Go協(xié)程goroutine的特點:
①有獨立的??臻g
②共享程序堆空間
③調(diào)度由用戶控制
④協(xié)程是輕量級的線程
Go協(xié)程goroutine的案例:
1.在主線程(可以理解成進(jìn)程)中,開啟一個goroutine,該協(xié)程每隔1秒輸出"hello,world"。
2.在主線程中也每隔一秒輸出"hello.golang",輸出10次后,退出程序。
3.要求主線程和goroutine同時執(zhí)行。


①如果主線程退出了,協(xié)程沒執(zhí)行完畢也會退出。
②主線程是一個物理線程,直接作用在cpu上的。是重量級的,非常耗費cpu資源。
③協(xié)程從主線程開啟的,是輕量級的線程,是邏輯態(tài)。對資源消耗相對小。
④Golang的協(xié)程機制是重要的特點,可以輕松的開啟上萬個協(xié)程。其它編程語言的并發(fā)機制是一般基于線程的,開啟過多的線程,資源耗費大,這里就突顯Golang 在并發(fā)上的優(yōu)勢了。
?
Go協(xié)程goroutine的調(diào)度模型:
MPG:
M:操作系統(tǒng)的主線程(是物理線程)
P:協(xié)程執(zhí)行需要的上下文
G:協(xié)程
MPG模式運行的狀態(tài)1:

當(dāng)前程序有三個M,如果都在一個cpu運行,就是并發(fā),如果在不同的cpu運行,就是并行。
?
MPG模式運行的狀態(tài)2:

分成兩個部分來看;
M0主線程正在執(zhí)行G0協(xié)程,另外有三個協(xié)程在隊列等待。
如果G0協(xié)程阻塞,這時就會創(chuàng)建M1主線程(也可能是從已有的線程池中取出M1),并且將等待的3個協(xié)程掛到M1下開始執(zhí)行,M0的主線程下的G0仍然執(zhí)行。
這樣的MPG調(diào)度模式,可以既讓G0執(zhí)行,同時也不會讓隊列的其它協(xié)程一直阻塞,仍然可以并發(fā)/并行執(zhí)行。
等到G0不阻塞了,M0會被放到空閑的主線程繼續(xù)執(zhí)行(從已有的線程池中?。瑫rG0又會被喚醒。
?
設(shè)置Golang運行的cpu數(shù):
為了充分利用多cpu的優(yōu)勢,在Golang程序中,設(shè)置運行的cpu數(shù)目。


go1.8后,默認(rèn)讓程序運行在多個核上,可以不用設(shè)置了。
go1.8前,還是要設(shè)置一下,可以更高效的利益cpu。
?
channel(管道)-看個需求
需求:
現(xiàn)在要計算 1-200 的各個數(shù)的階乘,并且把各個數(shù)的階乘放入到map中。
最后顯示出來。要求使用goroutine完成
思路:
1. 編寫一個函數(shù),來計算各個數(shù)的階乘,并放入到 map中。
2. 我們啟動的協(xié)程多個,統(tǒng)計的將結(jié)果放入到 map中。
3. map 應(yīng)該做出一個全局的。
4.因為沒有對全局變量m加鎖,因此會出現(xiàn)資源爭奪問題,代碼會出現(xiàn)錯誤,提示 concurrent map?writes。解決方案:加入互斥鎖。
?
不同goroutine之間如何通訊
①全局變量的互斥鎖
②使用管道channel來解決
?



為什么需要channel
①前面使用全局變量加鎖同步來解決goroutine的通訊,但不完美
②主線程在等待所有 goroutine全部完成的時間很難確定,這里設(shè)置10秒,僅僅是估算。
③如果主線程休眠時間長了,會加長等待時間,如果等待時間短了,可能還有 goroutine 處于工作狀態(tài),這時也會隨主線程的退出而銷毀
④通過全局變量加鎖同步來實現(xiàn)通訊,也并不利于多個協(xié)程對全局變量的讀寫操作。
⑤上面種種分析都在呼喚一個新的通訊機制-channe1
?
channel的基本介紹
Go語言中的通道(channel)是一種特殊的類型。
在任何時候,同時只能有一個goroutine訪問通道進(jìn)行發(fā)送和獲取數(shù)據(jù)。
①channel本身是一個隊列,先進(jìn)先出
②線程安全,多goroutine訪問時,不需要加鎖
③本身是有類型的,string,int等,如果要存多種類型,則定義成interface類型
④channel是引用類型,必須make后才能使用,一旦make,容量就確定了,不會增加!!
?

特點:
①一旦初始化容量,就不會改變了。
②當(dāng)寫滿時,不可以寫,取空時,不可以取,否則報dead lock。
③發(fā)送將持續(xù)阻塞直到數(shù)據(jù)被接收。
Go程序運行時能智能地發(fā)現(xiàn)一些永遠(yuǎn)無法發(fā)送成功的語句并做出提示。
④接收將持續(xù)阻塞直到發(fā)送方發(fā)送數(shù)據(jù)。
如果接收方接收時,通道中沒有發(fā)送方發(fā)送數(shù)據(jù),接收方也會發(fā)生阻塞,直到發(fā)送方發(fā)送數(shù)據(jù)為止。
⑤通道一次只能接收一個數(shù)據(jù)元素。
?
定義/聲明channel:var變量名chan數(shù)據(jù)類型
舉例:
Var intChan chan int (intChan用于存放int數(shù)據(jù))
Var mapChan chan map[int]string (mapChan用于存放map[int]string類型)
Var perChan chan Person
Var perChan2 chan *Person
說明:
channel是引用類型;
channel必須初始化才能寫入數(shù)據(jù),即make后才能使用管道是有類型的。
?
管道的初始化,寫入數(shù)據(jù)到管道,從管道讀取數(shù)據(jù)

fmt.Printf("intChan的值=%v ?intChan本身的地址=%p\n", ?intChan, ?&intChan):
channel和指針一樣,存放在一個內(nèi)存單元中,有它的地址,它的值是一個int類型的地址。
?
讀寫channel案例演示:
①創(chuàng)建一個intChan,最多可以存放3個int,存數(shù)據(jù)到intChan,然后再取出這三個int。

②創(chuàng)建一個mapChan,最多可以存放10個map[string]string的key-val,演示寫入和讀取。

③創(chuàng)建一個catChan,最多可以存放10個cat結(jié)構(gòu)體變量,演示寫入和讀取。

?④創(chuàng)建一個catChan2,最多可以存放10個*Cat變量,演示寫入和讀取

?⑤創(chuàng)建一個allChan,最多可以存放10個任意數(shù)據(jù)類型變量,演示寫入和讀取

?⑥注意空接口類型的 channel:

定義interface類型的空接口,可以接收任意類型的數(shù)據(jù),但是在取出來的時候,必須斷言!a := newCat.(Cat)
?
channel的關(guān)閉:close( )
關(guān)閉之后,不能再寫入,只能讀。只能由發(fā)送者執(zhí)行這句代碼。
?
channel 的遍歷:for--range遍歷,不用for循環(huán)
①在遍歷時,如果channel沒有關(guān)閉,則會出現(xiàn)deadlock的錯誤
②在遍歷時,如果channel已經(jīng)關(guān)閉,則會正常遍歷數(shù)據(jù),遍歷完后,就會退出遍歷。
應(yīng)用
實例1:請完成goroutine和channel協(xié)同工作的案例,具體要求:
①開啟一個writeData協(xié)程,向管道intChan中寫入50個整數(shù);
②開啟一個readData協(xié)程,從管道intChan中讀取writeData寫入的數(shù)據(jù);
③注意: writeData和readDate操作的是同一個管道;
④主線程需要等待writeData和readDate協(xié)程都完成工作才能退出管道。
方法:
①開兩個管道;
②當(dāng)writeData協(xié)程完成后,close數(shù)據(jù)管道,readData協(xié)程對數(shù)據(jù)管道intChan的數(shù)據(jù)讀完之后,就向退出管道exitChan寫入一個 true,close掉;
③主線程循環(huán)檢測退出管道里是否有數(shù)據(jù),如果有,說明readData協(xié)程完成,主程序就可以退出了。

實例2:要求統(tǒng)計1-200000的數(shù)字中,哪些是素數(shù)?
分析思路:使用并發(fā)/并行的方式,將統(tǒng)計素數(shù)的任務(wù)分配給多個(4個)goroutine去完成。
定義三個管道:
intChan :放80000個數(shù)
primeChan:放素數(shù)
exitChan :4個協(xié)程運行完畢的標(biāo)志

?

?

?channel使用細(xì)節(jié)和注意事項
①channel可以聲明為只讀?/ 只寫性質(zhì)

②使用 select可以解決從管道取數(shù)據(jù)的阻塞問題(不知道何時關(guān)閉管道時)

③goroutine中使用recover,解決“協(xié)程中出現(xiàn)panic,導(dǎo)致程序崩潰”的問題。

?

?通道的數(shù)據(jù)接收,?4種寫法。
①阻塞接收數(shù)據(jù)
將接收變量作為<-操作符的左值,格式:data := <-ch
執(zhí)行該語句時將會阻塞,直到接收到數(shù)據(jù)并賦值給data變量。
?
②非阻塞接收數(shù)據(jù)
語句不會發(fā)生阻塞,格式:data, ok := <-ch
data:表示接收到的數(shù)據(jù)。未接收到數(shù)據(jù)時,data為通道類型的零值。
ok:表示是否接收到數(shù)據(jù)。
非阻塞的通道接收方法可能造成高的CPU占用,因此使用非常少。如果需要實現(xiàn)接收超時檢測,可以配合 select 和計時器 channel進(jìn)行。
?
③接收任意數(shù)據(jù),忽略接收的數(shù)據(jù)
阻塞接收數(shù)據(jù)后,忽略從通道返回的數(shù)據(jù),格式:<-ch
執(zhí)行該語句時將會發(fā)生阻塞,直到接收到數(shù)據(jù),但接收到的數(shù)據(jù)會被忽略。
這個方式實際上只是通過通道在 goroutine 間阻塞收發(fā)實現(xiàn)并發(fā)同步。
?
使用通道做并發(fā)同步的寫法,可以參考下面的例子:

④循環(huán)接收
通道的數(shù)據(jù)接收可以借用for range語句進(jìn)行多個元素的接收操作,格式:
for data := range ch {
}
通道ch是可以進(jìn)行遍歷的,遍歷的結(jié)果就是接收到的數(shù)據(jù)。數(shù)據(jù)類型就是通道的數(shù)據(jù)類型。通過for遍歷獲得的變量只有一個,即上面例子中的data。

?
?