GO語言并發(fā)

理解并發(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ì)一模型
多對(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ì)一模型
一對(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ì)一模型和一對(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模型

其圖中的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ì)保存好凿跳,等待再次使用。


goroutine阻塞處理
問題: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)
    }
}

有緩沖通道圖解:


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市矩屁,隨后出現(xiàn)的幾起案子辟宗,更是在濱河造成了極大的恐慌爵赵,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,490評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件泊脐,死亡現(xiàn)場(chǎng)離奇詭異空幻,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)容客,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門氛悬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人耘柱,你說我怎么就攤上這事」飨郑” “怎么了调煎?”我有些...
    開封第一講書人閱讀 165,830評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長己肮。 經(jīng)常有香客問我士袄,道長,這世上最難降的妖魔是什么谎僻? 我笑而不...
    開封第一講書人閱讀 58,957評(píng)論 1 295
  • 正文 為了忘掉前任娄柳,我火速辦了婚禮,結(jié)果婚禮上艘绍,老公的妹妹穿的比我還像新娘赤拒。我一直安慰自己,他們只是感情好诱鞠,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評(píng)論 6 393
  • 文/花漫 我一把揭開白布挎挖。 她就那樣靜靜地躺著,像睡著了一般航夺。 火紅的嫁衣襯著肌膚如雪蕉朵。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,754評(píng)論 1 307
  • 那天阳掐,我揣著相機(jī)與錄音始衅,去河邊找鬼。 笑死缭保,一個(gè)胖子當(dāng)著我的面吹牛汛闸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播艺骂,決...
    沈念sama閱讀 40,464評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼蛉拙,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了彻亲?” 一聲冷哼從身側(cè)響起孕锄,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤吮廉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后畸肆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體宦芦,經(jīng)...
    沈念sama閱讀 45,847評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評(píng)論 3 338
  • 正文 我和宋清朗相戀三年轴脐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了调卑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,137評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡大咱,死狀恐怖恬涧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情碴巾,我是刑警寧澤溯捆,帶...
    沈念sama閱讀 35,819評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站厦瓢,受9級(jí)特大地震影響提揍,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜煮仇,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評(píng)論 3 331
  • 文/蒙蒙 一劳跃、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧浙垫,春花似錦刨仑、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至佃声,卻和暖如春艺智,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背圾亏。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評(píng)論 1 272
  • 我被黑心中介騙來泰國打工十拣, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人志鹃。 一個(gè)月前我還...
    沈念sama閱讀 48,409評(píng)論 3 373
  • 正文 我出身青樓夭问,卻偏偏與公主長得像,于是被迫代替她去往敵國和親曹铃。 傳聞我的和親對(duì)象是個(gè)殘疾皇子缰趋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容