理解并發(fā)和并行
并發(fā):同時(shí)管理多件事情。
并行:同時(shí)做多件事情楷兽。表示同時(shí)發(fā)生了多件事情,通過時(shí)間片切換叉讥,哪怕只有單一的核心窘行,也可以實(shí)現(xiàn)“同時(shí)做多件事情”這個(gè)效果。
預(yù)熱——用戶級(jí)線程和內(nèi)核級(jí)線程
線程被分為兩類:用戶級(jí)線程(User Level Thread)和內(nèi)核級(jí)線程(Kernal Level Thread)
用戶級(jí)線程
· 用戶級(jí)線程只存在于用戶空間图仓,有關(guān)它的創(chuàng)建罐盔、調(diào)度和管理工作都由用戶級(jí)線程庫來支持。用戶級(jí)線程庫是用于用戶級(jí)線程管理的例程包救崔,支持線程的創(chuàng)建惶看、終止,以及調(diào)度線程的執(zhí)行并保存和恢復(fù)線程的上下文六孵,這些操作都在用戶空間運(yùn)行纬黎,無需內(nèi)核的支持。
· 由于內(nèi)核無法感知用戶級(jí)線程的存在劫窒,因此內(nèi)核是以進(jìn)程為單位進(jìn)行調(diào)度的本今。當(dāng)內(nèi)核調(diào)度一個(gè)進(jìn)程運(yùn)行時(shí),用戶級(jí)線程庫調(diào)度該進(jìn)程的一個(gè)線程運(yùn)行主巍,如果時(shí)間片允許冠息,該進(jìn)程的其他線程也可能被運(yùn)行。即該進(jìn)程的多個(gè)線程共享該進(jìn)程的運(yùn)行時(shí)間片孕索。
· 若該進(jìn)程的一個(gè)線程進(jìn)行IO操作逛艰,則該線程調(diào)用系統(tǒng)調(diào)用進(jìn)入內(nèi)核,啟動(dòng)IO設(shè)備后搞旭,內(nèi)核會(huì)把該進(jìn)程阻塞散怖,并把CPU交給其他進(jìn)程。即使被阻塞的進(jìn)程的其他線程可以執(zhí)行肄渗,內(nèi)核也不會(huì)發(fā)現(xiàn)這一情況杭抠。在該進(jìn)程的狀態(tài)變?yōu)榫途w前,內(nèi)核不會(huì)調(diào)度它運(yùn)行恳啥。屬于該進(jìn)程的線程都不可能運(yùn)行,因而用戶級(jí)線程的并行性會(huì)受到一定的限制丹诀。
內(nèi)核級(jí)線程
· 內(nèi)核級(jí)線程的所有創(chuàng)建钝的、調(diào)度及管理操作都由操作系統(tǒng)內(nèi)核完成,內(nèi)核保存線程的狀態(tài)及上下文信息铆遭。
· 當(dāng)一個(gè)線程引起阻塞的系統(tǒng)調(diào)用時(shí)硝桩,內(nèi)核可以調(diào)度進(jìn)程的其他線程執(zhí)行,多處理器系統(tǒng)上枚荣,內(nèi)核分派屬于同一進(jìn)程的多個(gè)線程在多個(gè)處理器上執(zhí)行碗脊,提升進(jìn)程的并行度。
· 內(nèi)核管理線程效率比用戶態(tài)管理線程慢的多橄妆。
操作系統(tǒng)的三種線程模型
1.多對(duì)一模型
允許將多個(gè)用戶級(jí)線程映射到一個(gè)內(nèi)核線程衙伶。線程管理是在用戶空間進(jìn)行的祈坠,效率比較高。如果有一個(gè)線程執(zhí)行了阻塞系統(tǒng)調(diào)用矢劲,那么整個(gè)進(jìn)程就會(huì)阻塞赦拘。所以任意時(shí)刻只允許一個(gè)線程訪問內(nèi)核,這樣多個(gè)線程不能并行運(yùn)行在多處理器上芬沉。雖然多對(duì)一模型對(duì)創(chuàng)建用戶級(jí)線程的數(shù)目并沒有限制躺同,但這些線程在同一時(shí)刻只能有一個(gè)被執(zhí)行。
2.一對(duì)一模型
每個(gè)用戶線程映射到一個(gè)內(nèi)核線程丸逸。當(dāng)一個(gè)線程執(zhí)行阻塞系統(tǒng)調(diào)用蹋艺,該模型允許另一個(gè)線程繼續(xù)執(zhí)行。這樣提供了更好的并發(fā)功能黄刚。該模型也允許多個(gè)線程運(yùn)行在多核處理器上捎谨。一對(duì)一模型可以獲得高并發(fā)性,但因耗費(fèi)資源而使線程數(shù)會(huì)受到限制隘击。
3.多對(duì)多模型
在多對(duì)一模型和一對(duì)一模型中取了個(gè)折中侍芝,克服了多對(duì)一模型的并發(fā)度不高的缺點(diǎn),又克服了一對(duì)一模型的一個(gè)用戶進(jìn)程占用太多內(nèi)核級(jí)線程埋同,開銷太大的缺點(diǎn)州叠。又擁有多對(duì)一模型和一對(duì)一模型各自的優(yōu)點(diǎn),可謂集兩者之所長凶赁。
GO并發(fā)調(diào)度模型——G-P-M模型
GO可以使用如下方式創(chuàng)建一個(gè)"線程"(GO語言中所謂的goroutine)咧栗。
go func(paramName paramType, ...){
//函數(shù)體
}(param, ...)
等價(jià)于Java代碼
new java.lang.Thread(() -> {
// do something in one new thread
}).start();
G-P-M模型圖解
其圖中的G, P和M都是Go語言運(yùn)行時(shí)系統(tǒng)(其中包括內(nèi)存分配器,并發(fā)調(diào)度器虱肄,垃圾收集器等組件致板,可以想象為Java中的JVM)抽象出來概念和數(shù)據(jù)結(jié)構(gòu)對(duì)象。
G:G就是goroutine咏窿,通過go關(guān)鍵字創(chuàng)建斟或,封裝了所要執(zhí)行的代碼邏輯,可以稱為是用戶線程集嵌。屬于用戶級(jí)資源萝挤,對(duì)OS透明,具備輕量級(jí)根欧,可以大量創(chuàng)建怜珍,上下文切換成本低等特點(diǎn)。
P:Processor即邏輯處理器凤粗,默認(rèn)GO運(yùn)行時(shí)的Processor數(shù)量等于CPU數(shù)量酥泛,也可以通過GOMAXPROCS函數(shù)指定P的數(shù)量。P的主要作用是管理G運(yùn)行,每個(gè)P擁有一個(gè)本地隊(duì)列柔袁,并為G在M上的運(yùn)行提供本地化資源呆躲。
M:是操作系統(tǒng)創(chuàng)建的系統(tǒng)線程,作用就是執(zhí)行G中包裝的并發(fā)任務(wù)瘦馍,被稱為物理處理器歼秽。其屬于OS資源,可創(chuàng)建的數(shù)量上也受限了OS情组,通常情況下G的數(shù)量都多于活躍的M的燥筷。Go運(yùn)行時(shí)調(diào)度器將G公平合理的安排到多個(gè)M上去執(zhí)行。
·G和M的關(guān)系:G是要執(zhí)行的邏輯院崇,M具體執(zhí)行G的邏輯肆氓。Java中Thread實(shí)際上就是對(duì)M的封裝,通過指定run()函數(shù)指定要執(zhí)行的邏輯底瓣。GO語言中講二者分開谢揪,通過P建立G和M的聯(lián)系從而執(zhí)行。
·G和P的關(guān)系:P是G的管理者捐凭,P將G交由M執(zhí)行拨扶,并管理一定系統(tǒng)資源供G使用。一個(gè)P管理存儲(chǔ)在其本地隊(duì)列的所有G茁肠。P和G是1:n的關(guān)系患民。
·P和M的關(guān)系:P和M是1:1的關(guān)系。P將管理的G交由M具體執(zhí)行垦梆,當(dāng)遇到阻塞時(shí)匹颤,P可以與M解綁,并找到空閑的M進(jìn)行綁定繼續(xù)執(zhí)行隊(duì)列中其他可執(zhí)行的G托猩。
問題:為什么要有P印蓖?
G是對(duì)需要執(zhí)行的代碼邏輯的封裝,M具體執(zhí)行G京腥,P存在的意義是什么赦肃?
Go語言運(yùn)行時(shí)系統(tǒng)早期(Go1.0)的實(shí)現(xiàn)中并沒有P的概念,Go中的調(diào)度器直接將G分配到合適的M上運(yùn)行公浪。但這樣帶來了很多問題他宛,例如,不同的G在不同的M上并發(fā)運(yùn)行時(shí)可能都需向系統(tǒng)申請(qǐng)資源(如堆內(nèi)存)因悲,由于資源是全局的,將會(huì)由于資源競爭造成很多系統(tǒng)性能損耗勺爱。
Go 1.1起運(yùn)行時(shí)系統(tǒng)加入了P晃琳,讓P去管理G對(duì)象,M要想運(yùn)行G必須先與一個(gè)P綁定,然后才能運(yùn)行該P(yáng)管理的G卫旱。P對(duì)象中預(yù)先申請(qǐng)一些系統(tǒng)資源作為本地資源人灼,G需要的時(shí)候先向自己的P申請(qǐng)(無需鎖保護(hù)),如果不夠用或沒有再向全局申請(qǐng)顾翼,而且從全局拿的時(shí)候會(huì)多拿一部分投放,以供后面高效的使用。
P的存在解耦了G和M适贸,當(dāng)M執(zhí)行的G被阻塞時(shí)灸芳,P可以綁定到其他M上繼續(xù)執(zhí)行其管理的G,提升并發(fā)性能拜姿。
GO調(diào)度過程
①創(chuàng)建一個(gè)goroutine烙样,調(diào)度器會(huì)將其放入全局隊(duì)列。
②調(diào)度器為每個(gè)goroutine分配一個(gè)邏輯處理器蕊肥。并放到邏輯處理器的本地隊(duì)列中谒获。
③本地隊(duì)列中的goroutine會(huì)一直等待直到被邏輯處理器運(yùn)行。
func task1() {
go task2()
go task3()
}
假設(shè)現(xiàn)在task1在稱為G1的goroutine中運(yùn)行壁却,并在運(yùn)行過程中創(chuàng)建兩個(gè)新的goroutine批狱,新創(chuàng)建的兩個(gè)goroutine將會(huì)被放到全局隊(duì)列中,調(diào)度器會(huì)再將他們分配給合適的P展东。
問題:如果遇到阻塞的情況怎么處理赔硫?
假設(shè)正在運(yùn)行的goroutine要執(zhí)行一個(gè)阻塞的系統(tǒng)調(diào)用,如打開一個(gè)文件琅锻,在這種情況下卦停,這個(gè)M將會(huì)被內(nèi)核調(diào)度器調(diào)度出CPU并處于阻塞狀態(tài)。相應(yīng)的與M相關(guān)聯(lián)的P的本地隊(duì)列中的其他G將無法被運(yùn)行恼蓬,但Go運(yùn)行時(shí)系統(tǒng)的一個(gè)監(jiān)控線程(sysmon線程)能探測(cè)到這樣的M惊完,將M與P解綁,M將繼續(xù)被阻塞直到系統(tǒng)調(diào)用返回处硬。P則會(huì)尋找新的M(沒有則創(chuàng)建一個(gè))與之綁定并執(zhí)行剩余的G小槐。
當(dāng)之前被阻塞的M得到返回后,相應(yīng)的G將會(huì)被放回本地隊(duì)列荷辕,M則會(huì)保存好凿跳,等待再次使用。
問題:GO有時(shí)間片概念嗎疮方?
和操作系統(tǒng)按時(shí)間片調(diào)度線程不同控嗜,Go并沒有時(shí)間片的概念。如果一個(gè)G沒有發(fā)生阻塞的情況(如系統(tǒng)調(diào)用或阻塞在channel上)骡显,M是如何讓G停下來并調(diào)度下一個(gè)G的呢疆栏?
G是被搶占調(diào)度的曾掂。GO語言運(yùn)行時(shí),會(huì)啟動(dòng)一個(gè)名為sysmon的M壁顶,該M無需綁定P珠洗。sysmon每20us~10ms啟動(dòng)一次,按照《Go語言學(xué)習(xí)筆記》中的總結(jié)若专,sysmon主要完成如下工作:
1.回收閑置超過5分鐘的span物理內(nèi)存
2.如果超過2分鐘沒有垃圾回收许蓖,強(qiáng)制執(zhí)行
3.向長時(shí)間運(yùn)行的G任務(wù)發(fā)出搶占調(diào)度
4.收回因syscall長時(shí)間阻塞的P
5.將長時(shí)間未處理的netpoll結(jié)果添加到任務(wù)隊(duì)列
注:go的內(nèi)存分配也是基于兩種粒度的內(nèi)存單位:span和object。span是連續(xù)的page调衰,按page的數(shù)量進(jìn)行歸類膊爪,比如分為2個(gè)page的span,4個(gè)page的span等窖式。object是span中按預(yù)設(shè)大小劃分的塊蚁飒,也是按大小分類。同一個(gè)span中萝喘,只有一種類型大小的object淮逻。
sysmom的大致工作思路:
//$GOROOT/src/runtime/proc.go
// main方法
func main() {
... ...
systemstack(func() {
newm(sysmon, nil)
})
.... ...
}
func sysmon() {
// 回收閑置超過5分鐘的span物理內(nèi)存
scavengelimit := int64(5 * 60 * 1e9)
... ...
if .... {
... ...
// 如果P因syscall阻塞
//通過retake方法搶占G
if retake(now) != 0 {
idle = 0
} else {
idle++
}
... ...
}
}
搶占方法retake
// forcePreemptNS是指定的時(shí)間片,超過這一時(shí)間會(huì)嘗試搶占
const forcePreemptNS = 10 * 1000 * 1000 // 10ms
func retake(now int64) uint32 {
... ...
// 實(shí)施搶占
t := int64(_p_.schedtick)
if int64(pd.schedtick) != t {
pd.schedtick = uint32(t)
pd.schedwhen = now
continue
}
if pd.schedwhen+forcePreemptNS > now {
continue
}
preemptone(_p_)
... ...
}
可以看出阁簸,如果一個(gè)G任務(wù)運(yùn)行10ms爬早,sysmon就會(huì)認(rèn)為其運(yùn)行時(shí)間太久而發(fā)出搶占式調(diào)度的請(qǐng)求。
goroutine
1.gouroutine切換
package main
import (
"runtime"
"sync"
"fmt"
)
func main() {
//指定調(diào)度器所能調(diào)度的邏輯處理器數(shù)量
runtime.GOMAXPROCS(1)
//使用wg等待程序完成
var wg sync.WaitGroup
//計(jì)數(shù)器+2启妹,等待兩個(gè)goroutine
wg.Add(2)
fmt.Println("Start GoRoutine")
//聲明一個(gè)匿名函數(shù)筛严,使用go關(guān)鍵字創(chuàng)建goroutine
go func() {
//函數(shù)退出時(shí)通過Done通知main函數(shù)工作已經(jīng)完成
defer wg.Done()
for i := 1; i <= 3; i++ {
for char := 'a'; char < 'a' + 26; char++ {
fmt.Printf("%c ", char)
}
}
}()
go func() {
defer wg.Done()
for i := 1; i <= 3; i++ {
for char := 'A'; char < 'A' + 26; char++ {
fmt.Printf("%c ", char)
}
}
}()
//等待goroutine結(jié)束
fmt.Println("Waiting for finish")
wg.Wait()
fmt.Println("Program end")
}
輸出:
Start GoRoutine
Waiting for finish
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z a b c d e f g h i j k l m n o p q r s t u v w x y z a b c d e f g h i j k l m n o p q r s t u v w x y z
Program end
這個(gè)程序給人的感覺是串行的,原因是當(dāng)調(diào)度器還沒有準(zhǔn)備切換打印小寫字母的goroutine時(shí)饶米,打印大寫字母的goroutine就執(zhí)行完了桨啃。
修改下,打印6000以內(nèi)的素?cái)?shù)檬输。
package main
import (
"sync"
"runtime"
"fmt"
)
var wg sync.WaitGroup
func main(){
runtime.GOMAXPROCS(1)
wg.Add(2)
go printPrime("A")
go printPrime("B")
fmt.Println("Wait for finish")
wg.Wait()
fmt.Println("Program End")
}
func printPrime(prefix string){
defer wg.Done()
nextNum:
for i := 2; i < 6000; i++ {
for j := 2; j < i; j++ {
if i % j == 0 {
continue nextNum
}
}
fmt.Printf("%s:%d\n", prefix, i)
}
fmt.Printf("complete %s\n", prefix)
}
輸出結(jié)果:
Wait for finish
B:2
B:3
B:5
B:7
B:11
...
B:457
B:461
B:463
B:467
A:2
A:3
A:5
A:7
...
A:5981
A:5987
complete A
B:5939
B:5953
B:5981
B:5987
complete B
Program End
在打印素?cái)?shù)的過程中照瘾,goroutine的輸出是混在一起的,由于runtime .GOMAXPROCS(1)丧慈,可以看出兩個(gè)G是在一個(gè)P上并發(fā)執(zhí)行的析命。
package main
import (
"runtime"
"sync"
"fmt"
)
func main() {
runtime.GOMAXPROCS(2)
var wgp sync.WaitGroup
wgp.Add(2)
fmt.Println("Start Goroutines")
//第一個(gè)goroutine打印3遍小寫字母
go func() {
defer wgp.Done()
for i := 0; i < 3; i++ {
for char := 'a'; char < 'a'+26; char++ {
fmt.Printf("%c ", char)
}
}
}()
//第二個(gè)goroutine打印三遍大寫字母
go func() {
defer wgp.Done()
for i := 0; i < 3; i++ {
for char := 'A'; char < 'A'+26; char++ {
fmt.Printf("%c ", char)
}
}
}()
fmt.Println("Waiting for finish")
wgp.Wait()
fmt.Println("\nAll finish")
}
輸出:
Start Goroutines
Waiting for finish
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z A B C D E F G H I J K L a b c d e f g h i j k l m n o p q r s t u v w x y z a b c d M N O P Q R S T U V e f g h i j k l m n o p q r s t u v w x y z a b c d e f g h i j k l m W X Y Z n o p q r s t u v w x y z
All finish
設(shè)置兩個(gè)P,短時(shí)間內(nèi)會(huì)有類似上面打印0-6000所有素?cái)?shù)的效果逃默,證明兩個(gè)goroutine是并行執(zhí)行的鹃愤,但只有在多個(gè)P且每個(gè)可以同時(shí)讓每個(gè)G運(yùn)行再一個(gè)可用的M上時(shí),G才會(huì)達(dá)到并行的效果完域。
競爭狀態(tài)
多個(gè)goroutine再?zèng)]有同步的情況下软吐,同時(shí)讀寫某個(gè)共享資源就會(huì)產(chǎn)生競爭狀態(tài)。對(duì)一個(gè)資源的讀寫必須是原子化的吟税,即同一時(shí)刻只能有一個(gè)goroutine進(jìn)行讀寫操作凹耙。
包含競爭狀態(tài)的實(shí)例:
package main
import (
"sync"
"runtime"
"fmt"
)
var (
counter int
wgs sync.WaitGroup
)
func main(){
wgs.Add(2)
go incrCounter()
go incrCounter()
fmt.Println("adding")
wgs.Wait()
fmt.Printf("now counter is %d\n", counter)
}
//非原子操作加法
func incrCounter() {
defer wgs.Done()
for i := 1; i <= 2000; i++ {
val := counter
//讓出處理器
runtime.Gosched()
val++
counter = val
}
}
輸出:
adding
now counter is 2000
每次讀取完count后存入副本鸟蟹,手動(dòng)讓出M,再次輪到G執(zhí)行時(shí)會(huì)將之前的副本的值進(jìn)行增1的操作使兔,覆蓋了另一個(gè)goroutine的操作。
goroutine同步的幾種方式
①原子函數(shù)
原子函數(shù)以操作系統(tǒng)底層的枷鎖機(jī)制同步訪問變量藤韵,使用原子鎖方式修改之前的代碼:
package main
import (
"sync"
"fmt"
"sync/atomic"
"runtime"
)
var (
counter2 int64
wg2 sync.WaitGroup
)
func main() {
wg2.Add(2)
go incrCounterAtomic()
go incrCounterAtomic()
fmt.Println("adding...")
wg2.Wait()
fmt.Printf("now counter is : %d\n", counter2)
fmt.Println("Program end")
}
func incrCounterAtomic() {
defer wg2.Done()
for i := 1; i <= 2000; i++ {
atomic.AddInt64(&counter2, 1)
runtime.Gosched()
}
}
輸出:
adding...
now counter is : 4000
Program end
互斥鎖
互斥鎖用于在代碼當(dāng)中建立一個(gè)臨界區(qū)虐沥,保證同一時(shí)間只有一個(gè)G執(zhí)行臨界區(qū)的代碼。
package main
import (
"sync"
"fmt"
"runtime"
)
var (
counter3 int
wg3 sync.WaitGroup
mutex sync.Mutex
)
func main() {
wg3.Add(2)
go incrCounterMutex()
go incrCounterMutex()
fmt.Println("Adding...")
wg3.Wait()
fmt.Printf("now counter is : %d\n", counter3)
}
func incrCounterMutex() {
defer wg3.Done()
for i := 1; i <= 2000; i++ {
//建立臨界區(qū)
mutex.Lock()
{
val := counter3
runtime.Gosched()
val++
counter3 = val
}
mutex.Unlock()
}
}
通道
資源再goroutine之間共享時(shí)泽艘,可以使用通道實(shí)現(xiàn)同步欲险。聲明通道時(shí),需要指定將要被共享的數(shù)據(jù)類型匹涮,可以通過通道共享內(nèi)置類型天试、命名類型、結(jié)構(gòu)類型然低、引用類型的值或指針喜每。GO語言使用make函數(shù)創(chuàng)建通道。
①無緩沖通道
無緩沖通道使用make(chan type)聲明雳攘,通道中存取消息都是阻塞的带兜。
樣例:
func main() {
var messages chan string = make(chan string)
go func(message string) {
messages <- message // 存消息
}("hello!")
fmt.Println(<-messages) // 取消息
}
阻塞即無緩沖的通道道在取消息和存消息的時(shí)候都會(huì)掛起當(dāng)前的goroutine,除非另一端已經(jīng)準(zhǔn)備好吨灭。例:
package main
import (
"fmt"
"strconv"
)
var ch chan int = make(chan int)
func main() {
go input(0)
<- ch
fmt.Println("main finish")
}
func input(i int) {
for i := 0; i < 10; i++ {
fmt.Print(strconv.Itoa(i) + " ")
}
ch <- i
}
嘗試注釋掉ch的輸入和輸出刚照,對(duì)比運(yùn)行結(jié)果
不注釋:
0 1 2 3 4 5 6 7 8 9 main finish
注釋:
main finish
如果不用通道來阻塞主線的話,主線就會(huì)過早跑完喧兄,loop線將沒有機(jī)會(huì)執(zhí)行无畔。
無緩沖的通道永遠(yuǎn)不會(huì)存儲(chǔ)數(shù)據(jù),只負(fù)責(zé)數(shù)據(jù)的流通吠冤。
如果從無緩沖通道讀數(shù)據(jù)浑彰,必須要有數(shù)據(jù)寫入通道,否則讀取操作的goroutine將一直阻塞咨演。
如果向無緩沖通道寫數(shù)據(jù)闸昨,必須要有其他goroutine讀取,否則該goroutine將一直被阻塞薄风。
如果無緩沖通道中有數(shù)據(jù)饵较,再向其寫入,或者從無流入的通道數(shù)據(jù)中讀取遭赂,則會(huì)形成死鎖循诉。
死鎖
為什么會(huì)死鎖?非緩沖信道上如果發(fā)生了流入無流出撇他,或者流出無流入茄猫,也就導(dǎo)致了死鎖狈蚤。或者這樣理解 Go啟動(dòng)的所有g(shù)oroutine里的非緩沖信道一定要一個(gè)線里存數(shù)據(jù)划纽,一個(gè)線里取數(shù)據(jù)脆侮,要成對(duì)才行 。
c, quit := make(chan int), make(chan int)
go func() {
c <- 1 // c通道的數(shù)據(jù)沒有被其他goroutine讀取走勇劣,堵塞當(dāng)前goroutine
quit <- 0 // quit始終沒有辦法寫入數(shù)據(jù)
}()
<- quit // quit 等待數(shù)據(jù)的寫
是否所有不成對(duì)向信道存取數(shù)據(jù)的情況都是死鎖靖避?反例:
package main
var ch chan int = make(chan int)
func main() {
go input(0)
}
func input(i int) {
ch <- i
}
通道ch中只有流入沒有流出,但運(yùn)行不會(huì)報(bào)錯(cuò)比默。原因是main沒等待其它goroutine幻捏,自己先跑完了, 所以沒有數(shù)據(jù)流入c信道命咐,一共執(zhí)行了一個(gè)goroutine, 并且沒有發(fā)生阻塞篡九,所以沒有死鎖錯(cuò)誤。
②緩沖通道
緩沖信道不僅可以流通數(shù)據(jù)醋奠,還可以緩存數(shù)據(jù)榛臼。它是有容量的,存入一個(gè)數(shù)據(jù)的話 , 可以先放在信道里窜司,不必阻塞當(dāng)前線而等待該數(shù)據(jù)取走讽坏。但是當(dāng)緩沖信道達(dá)到滿的狀態(tài)的時(shí)候,就會(huì)表現(xiàn)出阻塞了例证。
package main
var ch chan int = make(chan int, 3)
func main() {
ch <- 1
ch <- 1
ch <- 1
//ch <- 1
}
如果是非緩沖通道路呜,這段代碼會(huì)報(bào)死鎖。但是當(dāng)有緩沖的通道時(shí)织咧,代碼正常執(zhí)行胀葱。如果再加入一行數(shù)據(jù)流入,才會(huì)報(bào)死鎖笙蒙。
通道可以看做是一個(gè)先進(jìn)先出的隊(duì)列抵屿。
package main
import "fmt"
var ch chan int = make(chan int, 3)
func main() {
ch <- 1
ch <- 2
ch <- 3
fmt.Println(<- ch)
fmt.Println(<- ch)
fmt.Println(<- ch)
}
會(huì)按照數(shù)據(jù)寫入的順序依次讀取
1
2
3
通道的其他基本操作
除了<-外,range也可以進(jìn)行讀取捅位。
package main
import (
"fmt"
)
var ch chan int = make(chan int, 3)
func main() {
ch <- 1
ch <- 2
ch <- 3
for v := range ch {
fmt.Println(v)
}
}
輸出
1
2
3
fatal error: all goroutines are asleep - deadlock!
可以讀取但產(chǎn)生死鎖轧葛,原因是range不等到通道關(guān)閉不結(jié)束讀取,在沒有數(shù)據(jù)流入的情況下艇搀,產(chǎn)生死鎖尿扯。有兩種方式可以避免這一情況。
1.通道長度為0即結(jié)束
package main
import (
"fmt"
)
var ch chan int = make(chan int, 3)
func main() {
ch <- 1
ch <- 2
ch <- 3
for v := range ch {
fmt.Println(v)
if len(ch) == 0 {
break
}
}
}
2.手動(dòng)關(guān)閉通道
關(guān)閉通道只是關(guān)閉了向通道寫入數(shù)據(jù)焰雕,但可以從通道讀取衷笋。
package main
import (
"fmt"
)
var ch chan int = make(chan int, 3)
func main() {
ch <- 1
ch <- 2
ch <- 3
close(ch)
for v := range ch {
fmt.Println(v)
}
}
有緩沖通道圖解: