15 Go并发编程(二):通道 —— Go并发的通信同步

Go 通道

1.什么是通道?

我们知道多个并发单元在对同一资源进行访问时会涉及资源的占用问题,在其他语言的方案中,都是通过共享内存的方式去访问资源,即互斥锁。当一个并发单元对一个资源进行访问时,如该资源有可能也被其他并发单元访问,我们会在对该资源操作时加互斥锁,以保证在同一时间单位里该资源是确定的。这种方式解决了资源访问的安全问题,但也同时带来并发效率问题,如锁定资源的范围和时间过大,则其他相关并发单元会处于等待状态,带来效率的损失。

为此Go在并发单元对资源的访问里设计另一种可能性,即channel,中文可叫通道或管道,它提倡要通过通信共享内存,而不要通过共享内存进行通信

通道最早由CSP模型提出,以点对点通道代替内存共享实现并发单元间的数据交互,相比内存共享数据交互,它在程序并发原语上更加直观,也可能在某种程度上会有相较的效率提升。关于CSP模型在《Go并发编程初探》篇章已提到,这里不再赘述。

在Go中,通道是一种特殊的程序原语,它为CSP并发模型而设计,用chan关键字表示。它的主要作用是在协程之间实现通信,可以说是go协程间的高速公路。

通道声明:

var {变量名} chan {数据类型}

//声明一个整型通道chA
var chA chan int

通道创建:

{接收变量名} := make(chan {数据类型},{通道容量})

//使用make函数创建一个容量为5的整型通道
chB := make(chan int,5)

通道有几个特点:

1.通道是有类型的,基于go的基本数据类型
2.通道是有方向的,可从通道读出数据,可向通道写入数据。当通道被传递时可设置其读写方向
3.通道是有缓存的,通过缓存容量可控制协程间的阻塞
4.通道是可关闭的,且只能被关闭一次,通道也是资源,如果不使用应关闭

通道使用常识:

1.通道数据读写,通道写入数据后必须关闭;
2.从一个已经关闭的通道中读取数据,读完了之后,继续读会读到其通道类型的零值;
3.没有初始化的通道被关闭会报panic;
4.读取通道数据时应校验其有效性;
5.关闭通道时会产出一个广播,所有从通道读取数据的协程都会收到消息;
6.被遍历的通道如果没有收到通道被关闭的广播,遍历会一直被阻塞;
7.通道一般在写入协程处调用关闭,写只有一个写入协程的情况,通道被关闭后不能写入数据,但其他协程可以读出,注意重复关闭的情况;

以下演示通道在协程间的通信作用:

func BaseChannel01() {
    //声明一个通道:声明后没有初始化的通道是空指针nil,证明其为引用类型
    //PS:go的引用类型有五种:slice、map、chan、指针、接口,前三种都是直接可以用make()函数创建
    //var chA chan int
    //fmt.Println(chA)

    //初始化一个通道
    chB0 := make(chan int, 0)    //无缓存能力的整型通道
    chB1 := make(chan string, 5) //缓存能力为5的字符串通道
    chB2 := make(chan<- int, 3)  //缓存能力为3的整型只写通道
    chB3 := make(<-chan int, 3)  //缓存能力为3的整型只读通道
    fmt.Printf("chB0:类型%T,值%v\n", chB0, chB0)
    fmt.Printf("chB1:类型%T,值%v\n", chB1, chB1)
    fmt.Printf("chB2:类型%T,值%v\n", chB2, chB2)
    fmt.Printf("chB3:类型%T,值%v\n", chB3, chB3)

    //协程从通道不断的写入读出数据
    //开辟一个协程往通道里写数据
    go func(ch chan int) {
        //循环不断往通道里写入当前秒数
        for {
            now := time.Now()
            second := now.Second()
            ch <- second
        }

    }(chB0)

    //另一个协程不断从通道读出数据
    go func(ch chan int) {
        for {
            nowSecond := <-ch
            fmt.Println("子协程读出数据:", nowSecond)
            time.Sleep(time.Second)
        }
    }(chB0)

    //父协程不断从通道读出数据
    for {
        nowSecond := <-chB0
        fmt.Println("父协程读出数据:", nowSecond)
        time.Sleep(time.Second)
    }

    //读取通道数据时应校验其有效性
    fmt.Println("读取通道数据时校验其有效性:")
    chCheck := make(chan int, 3)

    chCheck <- 123
    chCheck <- 456
    chCheck <- 789
    close(chCheck)

    go func(ch chan int) {
        chc1 := <-chCheck
        chc2 := <-chCheck
        chc3 := <-chCheck
        chc4 := <-chCheck
        chc5 := <-chCheck

        fmt.Println(chc1, "-", chc2, "-", chc3, "-", chc4, "-", chc5)

        chc6, ok := <-chCheck
        fmt.Printf("chc5:值%v,有效性:%v\n", chc6, ok)

    }(chCheck)

    time.Sleep(time.Second * 3)

    //遍历通道,通道关闭以后遍历读取会自动被通知退出
    for v := range chCheck{
        time.Sleep(time.Second)
        fmt.Println(v)
    }

}
2.通道的读写和异常

关闭通道的几个注意事项:

  • 不能关闭一个没有初始化的通道
  • 通道不能重复关闭
  • 不能往已经关闭的通道中写入数据,但是可以从中读数据
func BaseChanner02() {
    var ch1 chan int
    close(ch1) // panic: close of nil channel

    ints1 := make(chan int, 1)
    ints1 <- 111
    close(ints1)
    close(ints1) //panic: close of closed channel]

    ints := make(chan int, 1)
    ints <- 111
    close(ints)
    ints <- 111 //panic: send on closed channel
}
3.无缓存的通道

使用一个无缓存的通道时应该注意,它是阻塞的:

  • 没人读就永远写不进(阻塞)
  • 没人写就永远读不出(阻塞)
func BaseChanner03() {
    chInt := make(chan int)

    go func(ch chan int) {
        fmt.Println("启动协程1")
        ch <- 111
        close(ch)
        fmt.Println("结束协程1")
    }(chInt)

    go func(ch chan int) {
        fmt.Println("启动协程2")
        fmt.Println("通道数据:", <-ch)
        fmt.Println("结束协程2")
    }(chInt)

    for {
        time.Sleep(time.Second)
    }

}
4.有缓存能力的通道

可利用通道缓存能力进行协程调度,通道的元素个数或称缓存能力,决定协程是否产生阻塞,若通道数据已满则阻塞,写入阻塞读出也阻塞,这是相互的。

func BaseChanner04() {
    //创建一个缓存能力为3的整型通道
    chInt := make(chan int, 3)

    go func(ch chan int) {
        fmt.Println("启动协程1")
        for i := 1; i <= 5; i++ {
            time.Sleep(time.Second * 2)
            fmt.Println("协程1写入数据:", i)
            ch <- i
        }
        fmt.Println("结束协程1")
    }(chInt)

    go func(ch chan int) {
        fmt.Println("启动协程2")
        for i := 1; i <= 5; i++ {
            num := <-ch
            fmt.Println("协程2读出数据:", num)

        }
        fmt.Println("结束协程2")
    }(chInt)

    for {
        time.Sleep(time.Second * 3)
    }

}
5.select选择通道,协程多路复用

在讲到Go外壳:分支专题是提到select,select关键字是go特有的,其主要用于配合通道实现多路复用。

func BaseChanner05() {
    //创建三个通道
    ch1 := make(chan int, 3)
    ch2 := make(chan int, 4)
    ch3 := make(chan int, 5)

    //创建3条协程
    go func(c chan int) {
        ticker := time.NewTicker(time.Second * 1)
        for {
            <-ticker.C
            c <- 1
        }
    }(ch1)
    go func(c chan int) {
        ticker := time.NewTicker(time.Second * 2)
        for {
            <-ticker.C
            c <- 2
        }
    }(ch2)
    go func(c chan int) {
        ticker := time.NewTicker(time.Second * 3)
        for {
            <-ticker.C
            c <- 3
        }
    }(ch3)

    //time.Sleep(time.Second)
    //主协程select多路复用,for不断获取不同通道的数据,随先来则优先处理谁。
    for {
        select {
        case chV1, ok := <-ch1:
            fmt.Printf("通道ch1输出:%v,有效性%v\n", chV1, ok)
        case chV2, ok := <-ch2:
            fmt.Printf("通道ch2输出:%v,有效性%v\n", chV2, ok)
        case chV3, ok := <-ch3:
            fmt.Printf("通道ch3输出:%v,有效性%v\n", chV3, ok)
    }
}
6.通过容量控制并发数

利用有容量通道的阻塞能力——地铁闸机模型

func BaseChanner06() {
    //创建一个容量为5的通道,无论协程开多少,控制每次5条并发
    semaphore := make(chan int, 5)

    for i := 1; i <= 100; i++ {
        go func(c chan int, n int) {
            for {
                c <- i //抢通道写入,抢不到则阻塞
                fmt.Println("协程", n, "抢到通道")
                time.Sleep(time.Second)
                <-c //做完操作后自己读出,空出容量
            }
        }(semaphore, i)
    }

    for {
        time.Sleep(time.Second)
    }

}
7.定时器

固定时间和周期时长定时器,其与time.sleep()区别是可以终止和重置定时器。下面演示一下timer和ticker,以及在子协程中终止ticker

func BaseChannel07() {
    //使用固定时间定时
    timer := time.NewTimer(time.Second * 3)
    <-timer.C
    fmt.Println("父协程定时3秒输出!!!")

    //简单的使用变量标识ticker状态
    var tickerStopped = false
    //使用周期定时器
    ticker := time.NewTicker(time.Second * 1)
    go func(t *time.Ticker) {
        //5秒后终止周期器
        //使用固定时间定时
        timer := time.NewTimer(time.Second * 5)
        <-timer.C
        fmt.Println("子协程定时5秒关闭周期器!!!")
        t.Stop()
        tickerStopped = true
        runtime.Goexit()
    }(ticker)

    for {
        if !tickerStopped {
            s := <-ticker.C
            fmt.Println("父协程每隔1秒输出!!!", s)
        } else {
            fmt.Println("子协程已关闭周期定时器!!!")
            os.Exit(0)
        }

    }

    //不可撤销的time.Sleep()

}
8.如何优雅的关闭通道

比较优雅的方式一般建议在发送方关闭。

协程对通道的操作分几种情况:

  • 一个发送者,一个接收者;

在确认只有一个发送者的情况下,通道关闭时比较简单的,写入完成后记得立即或延迟关闭通道即可,接收者在读完所有数据后会校验读取数据的有效性。

  • 一个发送者,多个接收者;

这种情况与上一种类似,如果其他多个协程的接受者在遍历读取,它们都会收到通道被关闭的广播,并退出退出遍历阻塞状态。

  • 多个发送者,一个接收者;

这种情况比较复杂,各个发送者都不能粗暴的关闭通道,虽然可以通过sync.Once控制只关闭一次,但其他发送者仍有可能向一个已被关闭的通道发送数据。这里可能需要其他状态量标志各个发送者的完成状态,接收者监控该状态量确认各发送者写入完成后,由接收者关闭通道。可以使用等待组、状态变量、或其他通道等等。

  • 多个发送者,多个接收者;

一般这种状况较少,多个协程一起对同一个通道进行读写,一般都需要一个管理者监控该通道的读写完成情况,如单独一个协程检测各个操作该通道的协程的完成状态,当全部读写完成后再进行通道关闭。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容

  • 参考《快学 Go 语言》第 11 课 —— 千军万马跑协程《快学 Go 语言》第 12 课 —— 通道let's ...
    合肥黑阅读 2,868评论 2 7
  • 并发编程 1、并行和并发 并行(parallel): 指同一时刻,有多条指令在多个处理器上执行 并发(concur...
    Pauley阅读 6,042评论 0 12
  • Go 并发编程 选择 Go 编程的原因可能是看中它简单且强大,那么你其实可以选择C语言;除此之外,我看中 Go 的...
    PRE_ZHY阅读 846评论 1 6
  • 必备的理论基础 1.操作系统作用: 隐藏丑陋复杂的硬件接口,提供良好的抽象接口。 管理调度进程,并将多个进程对硬件...
    drfung阅读 3,451评论 0 5
  • 第四章 拜师 文鹰跟张道陵还有几个小伙伴一路有说有笑的踏上了拜师之路。 “道凌,你知道轩辕住哪里吗?” “不知道!...
    鹰羽魔阅读 246评论 0 0