目录

1.什么是 channel,介绍管道

2.channel 的基本使用

3.channel 的使用场景

4.使用 channel的注意事项及死锁分析

什么是 channel 管道

它是一个数据管道,可以往里面写数据,从里面读数据。

channel 是 goroutine 之间数据通信桥梁,而且是线程安全的。

channel 遵循先进先出原则。

写入,读出数据都会加锁。

channel 可以分为 3 种类型:

  • 只读 channel,单向 channel

  • 只写 channel,单向 channel

  • 可读可写 channel

channel 还可按是否带有缓冲区分为:

  • 带缓冲区的 channel,定义了缓冲区大小,可以存储多个数据

  • 不带缓冲区的 channel,只能存一个数据,并且只有当该数据被取出才能存下一个数据

channel 的基本使用

定义和声明

// 只读 channel
var readOnlyChan <-chan int // channel 的类型为 int // 只写 channel
var writeOnlyChan chan<- int // 可读可写
var ch chan int // 或者使用 make 直接初始化
readOnlyChan1 := make(<-chan int, 2) // 只读且带缓存区的 channel
readOnlyChan2 := make(<-chan int) // 只读且不带缓存区 channel readOnlyChan3 := make(chan<- int, 4) // 只写且带缓存区 channel
readOnlyChan4 := make(chan<- int) // 只写且不带缓存区 channel ch := make(chan int, 10) // 可读可写且带缓存区 ch <- 20 // 写数据
i := <-ch // 读数据
i, ok := <-ch // 还可以判断读取的数据

chan_var.go


package main import (
"fmt"
) func main() {
// var 声明一个 channel,它的零值是nil
var ch chan int
fmt.Printf("var: the type of ch is %T \n", ch)
fmt.Printf("var: the val of ch is %v \n", ch) if ch == nil {
// 也可以用make声明一个channel,它返回的值是一个内存地址
ch = make(chan int)
fmt.Printf("make: the type of ch is %T \n", ch)
fmt.Printf("make: the val of ch is %v \n", ch)
} ch2 := make(chan string, 10)
fmt.Printf("make: the type of ch2 is %T \n", ch2)
fmt.Printf("make: the val of ch2 is %v \n", ch2)
} // 输出:
// var: the type of ch is chan int
// var: the val of ch is <nil>
// make: the type of ch is chan int
// make: the val of ch is 0xc000048060
// make: the type of ch2 is chan string
// make: the val of ch2 is 0xc000044060

操作channel的3种方式

操作 channel 一般有如下三种方式:

  1. 读 <-ch

  2. 写 ch<-

  3. 关闭 close(ch)

操作 nil的channel 正常channel 已关闭的channel
读 <-ch 阻塞 成功或阻塞 读到零值
写 ch<- 阻塞 成功或阻塞 panic
关闭 close(ch) panic 成功 panic

注意 对于 nil channel 的情况,有1个特殊场景:

当 nil channel 在 select 的某个 case 中时,这个 case 会阻塞,但不会造成死锁。

单向 channel

单向 channel:只读和只写的 channel

chan_uni.go

package main

import "fmt"

func main() {
// 单向 channel,只写channel
ch := make(chan<- int)
go testData(ch)
fmt.Println(<-ch)
} func testData(ch chan<- int) {
ch <- 10
} // 运行输出
// ./chan_uni.go:9:14: invalid operation: <-ch (receive from send-only type chan<- int)
// 报错,它是一个只写 send-only channel

把上面代码main()函数里初始化的单向channel,修改为可读可写channel,再运行

chan_uni2.go

package main

import "fmt"

func main() {
// 把上面代码main()函数初始化的单向 channel 修改为可读可写的 channel
ch := make(chan int)
go testData(ch)
fmt.Println(<-ch)
} func testData(ch chan<- int) {
ch <- 10
} // 运行输出:
// 10 // 没有报错,可以正常输出结果

带缓冲和不带缓冲的 channel

不带缓冲区 channel

chan_unbuffer.go

package main

import "fmt"

func main() {
ch := make(chan int) // 无缓冲的channel
go unbufferChan(ch) for i := 0; i < 10; i++ {
fmt.Println("receive ", <-ch) // 读出值
}
} func unbufferChan(ch chan int) {
for i := 0; i < 10; i++ {
fmt.Println("send ", i)
ch <- i // 写入值
}
} // 输出
send 0
send 1
receive 0
receive 1
send 2
send 3
receive 2
receive 3
send 4
send 5
receive 4
receive 5
send 6
send 7
receive 6
receive 7
send 8
send 9
receive 8
receive 9

带缓冲区 channel

chan_buffer.go

package main

import (
"fmt"
) func main() {
ch := make(chan string, 3)
ch <- "tom"
ch <- "jimmy"
ch <- "cate" fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
} // 运行输出:
// tom
// jimmy
// cate

再看一个例子:chan_buffer2.go

package main

import (
"fmt"
"time"
) var c = make(chan int, 5) func main() {
go worker(1)
for i := 1; i < 10; i++ {
c <- i
fmt.Println(i)
}
} func worker(id int) {
for {
_ = <-c
}
} // 运行输出:
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9

判断 channel 是否关闭

if v, ok := <-ch; ok {
fmt.Println(ch)
}

说明:

  • ok 为 true,读到数据,且管道没有关闭
  • ok 为 false,管道已关闭,没有数据可读

读已经关闭的 channel 会读到零值,如果不确定 channel 是否关闭,可以用这种方法来检测。

range and close

range 可以遍历数组,map,字符串,channel等。

一个发送者可以关闭 channel,表明没有任何数据发送给这个 channel 了。接收者也可以测试channel是否关闭,通过 v, ok := <-ch 表达式中的 ok 值来判断 channel 是否关闭。上一节已经说明 ok 为 false 时,表示 channel 没有接收任何数据,它已经关闭了。

注意:仅仅只能是发送者关闭一个 channel,而不能是接收者。给已经关闭的 channel 发送数据会导致 panic。

Note: channels 不是文件,你通常不需要关闭他们。那什么时候需要关闭?当要告诉接收者没有值发送给 channel 了,这时就需要了。

比如终止 range 循环。

当 for range 遍历 channel 时,如果发送者没有关闭 channel 或在 range 之后关闭,都会导致 deadlock(死锁)。

下面是一个会产生死锁的例子:

package main

import "fmt"

func main() {
ch := make(chan int) go func() {
for i := 0; i < 10; i++ {
ch <- i
}
}() for val := range ch {
fmt.Println(val)
}
close(ch) // 这里关闭channel已经”通知“不到range了,会触发死锁。
// 不管这里是否关闭channel,都会报死锁,close(ch)的位置就不对。
// 且关闭channel的操作者也错了,只能是发送者关闭channel
}
// 运行程序输出
// 0
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9
// fatal error: all goroutines are asleep - deadlock!

改正也很简单,把 close(ch) 移到 go func(){}() 里,代码如下

go func() {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}()

这样程序就可以正常运行,不会报 deadlock 的错误了。

把上面程序换一种方式来写,chan_range.go

package main

import (
"fmt"
) func main() {
ch := make(chan int)
go test(ch)
for val := range ch { //
fmt.Println("get val: ", val)
}
} func test(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
} // 运行输出:
// get val: 0
// get val: 1
// get val: 2
// get val: 3
// get val: 4

发送者关闭 channel 时,for range 循环自动退出。

for 读取channel

用 for 来不停循环读取 channel 里的数据。

把上面的 range 程序修改下,chan_for.go

package main

import (
"fmt"
) func main() {
ch := make(chan int)
go test(ch) for {
val, ok := <-ch
if ok == false {// ok 为 false,没有数据可读
break // 跳出循环
}
fmt.Println("get val: ", val)
}
} func test(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
} // 运行输出:
// get val: 0
// get val: 1
// get val: 2
// get val: 3
// get val: 4

select 使用

例子 chan_select.go

package main

import "fmt"

// https://go.dev/tour/concurrency/5
func fibonacci(ch, quit chan int) {
x, y := 0, 1
for {
select {
case ch <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
} func main() {
ch := make(chan int)
quit := make(chan int) go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-ch)
}
quit <- 0
}() fibonacci(ch, quit)
} // 运行输出:
// 0
// 1
// 1
// 2
// 3
// 5
// 8
// 13
// 21
// 34
// quit

channel 的一些使用场景

1. 作为goroutine的数据传输管道

package main

import "fmt"

// https://go.dev/tour/concurrency/2
func sums(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum
} func main() {
s := []int{7, 2, 8, -9, 4, 0} c := make(chan int)
go sums(s[:len(s)/2], c)
go sums(s[len(s)/2:], c) x, y := <-c, <-c // receive from c fmt.Println(x, y, x+y)
}

用 goroutine 和 channel 分批求和

2. 同步的channel

没有缓冲区的 channel 可以作为同步数据的管道,起到同步数据的作用。

对没有缓冲区的 channel 操作时,发送的 goroutine 和接收的 goroutine 需要同时准备好,也就是发送和接收需要一一配对,才能完成发送和接收的操作。

如果两方的 goroutine 没有同时准备好,channel 会导致先执行发送或接收的 goroutine 阻塞等待。这就是没有缓冲区的 channel 作为数据同步的作用。

gobyexample 中的一个例子:

package main

import (
"fmt"
"time"
) //https://gobyexample.com/channel-synchronization
func worker(done chan bool) {
fmt.Println("working...")
time.Sleep(time.Second)
fmt.Println("done") done <- true
} func main() {
done := make(chan bool, 1)
go worker(done) <-done
}

注意:同步的 channel 千万不要在同一个 goroutine 协程里发送和接收数据。可能导致deadlock死锁。

3. 异步的channel

有缓冲区的 channel 可以作为异步的 channel 使用。

有缓冲区的 channel 也有操作注意事项:

  1. 如果 channel 中没有值了,channel 为空了,那么接收者会被阻塞。

  2. 如果 channel 中的缓冲区满了,那么发送者会被阻塞。

    注意:有缓冲区的 channel,用完了要 close,不然处理这个channel 的 goroutine 会被阻塞,形成死锁。

package main

import (
"fmt"
) func main() {
ch := make(chan int, 4)
quitChan := make(chan bool) go func() {
for v := range ch {
fmt.Println(v)
}
quitChan <- true // 通知用的channel,表示这里的程序已经执行完了
}() ch <- 1
ch <- 2
ch <- 3
ch <- 4
ch <- 5 close(ch) // 用完关闭channel
<-quitChan // 接到channel通知后解除阻塞,这也是channel的一种用法
}

4.channel 超时处理

channel 结合 time 实现超时处理。

当一个 channel 读取数据超过一定时间还没有数据到来时,可以得到超时通知,防止一直阻塞当前 goroutine。

chan_timeout.go

package main

import (
"fmt"
"time"
) func main() {
ch := make(chan int)
quitChan := make(chan bool) go func() {
for {
select {
case v := <-ch:
fmt.Println(v)
case <-time.After(time.Second * time.Duration(3)):
quitChan <- true
fmt.Println("timeout, send notice")
return
}
}
}() for i := 0; i < 4; i++ {
ch <- i
} <-quitChan // 输出值,相当于收到通知,解除主程阻塞
fmt.Println("main quit out")
}

使用 channel 的注意事项及死锁分析

未初始化的 channel 读写关闭操作

  1. 读:未初始化的channel,读取里面的数据时,会造成死锁deadlock
var ch chan int
<-ch // 未初始化channel读数据会死锁
  1. 写:未初始化的channel,往里面写数据时,会造成死锁deadlock
var ch chan int
ch<- // 未初始化channel写数据会死锁
  1. 关闭:未初始化的channel,关闭该channel时,会panic
var ch chan int
close(ch) // 关闭未初始化channel,触发panic

已初始化的 channel 读写关闭操作

  1. 已初始化,没有缓冲区的channel

    // 代码片段1
    func main() {
    ch := make(chan int)
    ch <- 4
    }

    代码片段1:没有缓冲channel,且只有写入没有读取,会产生死锁

    // 代码片段2
    func main() {
    ch := make(chan int)
    val, ok := <-ch
    }

    代码片段2:没有缓冲channel,且只有读取没有写入,会产生死锁

    // 代码片段3
    func main() {
    ch := make(chan int)
    val, ok := <-ch
    if ok {
    fmt.Println(val)
    }
    ch <- 10 // 这里进行写入。但是前面已经产生死锁了
    }

    代码片段3:没有缓冲channel,既有写入也有读出,但是在代码 val, ok := <-c 处已经产生死锁了。下面代码执行不到。

    // 代码片段4
    func main() {
    ch := make(chan int)
    ch <- 10
    go readChan(ch) time.Sleep(time.Second * 2)
    } func readChan(ch chan int) {
    for {
    val, ok := <-ch
    fmt.Println("read ch: ", val)
    if !ok {
    break
    }
    }
    }

    代码片段4:没有缓冲channel,既有写入也有读出,但是运行程序后,报错 fatal error: all goroutines are asleep - deadlock!

    这是因为往 channle 里写入数据的代码 ch <- 10,这里写入数据时就已经产生死锁了。把 ch<-10go readChan(ch) 调换位置,程序就能正常运行,不会产生死锁。

    // 代码片段5
    func main() {
    ch := make(chan int) go writeChan(ch) for {
    val, ok := <-ch
    fmt.Println("read ch: ", val)
    if !ok {
    break
    }
    } time.Sleep(time.Second)
    fmt.Println("end")
    } func writeChan(ch chan int) {
    for i := 0; i < 4; i++ {
    ch <- i
    }
    }

    代码片段5:没有缓冲的channel,既有写入,也有读出,与上面几个代码片段不同的是,写入channel的数据不是一个。

    思考一下,这个程序会产生死锁吗?10 秒时间思考下,先不要看下面。

    也会产生死锁,它会输出完数据后,报错 fatal error: all goroutines are asleep - deadlock!

    为什么呢?这个程序片段,既有读也有写而且先开一个goroutine写数据,为什么会死锁?

    原因在于 main() 里的 for 循环。可能你会问,不是有 break 跳出 for 循环吗?代码是写了,但是程序并没有执行到这里。

    因为 for 会不停的循环,而 val, ok := <-ch, 这里 ok 值一直是 true,因为程序里并没有哪里关闭 channel 啊。你们可以打印这个 ok 值看一看是不是一直是 true。当 for 循环把 channel 里的值读取完了后,程序再次运行到 val, ok := <-ch 时,产生死锁,因为 channel 里没有数据了。

    找到原因了,那解决办法也很简单,在 writeChan 函数里关闭 channel,加上代码 close(ch)。告诉 for 我写完了,关闭 channel 了。

    加上关闭 channel 代码后运行程序:

    read ch:  0 , ok:  true
    read ch: 1 , ok: true
    read ch: 2 , ok: true
    read ch: 3 , ok: true
    read ch: 0 , ok: false
    end

    程序正常输出结果。

对于没有缓冲区的 channel (unbuffered channel) 容易产生死锁的几个代码片段分析,总结下:

  1. channel 要用 make 进行初始化操作
  2. 读取和写入要配对出现,并且不能在同一个 goroutine 里
  3. 一定先用 go 起一个协程执行读取或写入操作
  4. 多次写入数据,for 读取数据时,写入者注意关闭 channel(代码片段5)
  1. 已初始化,有缓冲区的 channel

    // 代码片段1
    func main() {
    ch := make(chan int, 1)
    val, ok := <-ch
    }

    代码片段1:有缓冲channel,先读数据,这里会一直阻塞,产生死锁。

    // 代码片段2
    func main() {
    ch := make(chan int, 1)
    ch <- 10
    }

    代码片段2:同代码片段1,有缓冲channel,只有写没有读,也会阻塞,产生死锁。

    // 代码片段3
    func main() {
    ch := make(chan int, 1)
    ch <- 10 val, ok := <-ch
    if ok {
    fmt.Println(val, ok)
    }
    }

    代码片段3:有缓冲的channel,有读有写,正常的输出结果。

有缓冲区的channel总结:

  1. 如果 channel 满了,发送者会阻塞

  2. 如果 channle 空了,接收者会阻塞

  3. 如果在同一个 goroutine 里,写数据操作一定在读数据操作前

参考

golang 中 channel 的详细使用、使用注意事项及死锁分析的更多相关文章

  1. golang 中 channel 的非阻塞访问方法

    在golang中,基本的channel读写操作都是阻塞的,如果你想要非阻塞的,可以使用如下示例: 即只要在select中加入default,阻塞立即变成非阻塞: package main import ...

  2. golang中channel的超时处理

    并发中超时处理是必不可少的,golang没有提供直接的超时处理机制,但可以利用select机制来解决超时问题. func timeoutFunc() { //首先,实现并执行一个匿名的超时等待函数 t ...

  3. 如何在golang中打印grpc详细日志

    最近捣鼓fabric,在一个tls证书问题上纠结挺久,连接orderer服务时候,grpc日志总是冷冰冰的显示这个信息 Orderer Client Status Code: (2) CONNECTI ...

  4. golang中channel

    1. Channel是Go中的一个核心类型,你可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯(communication). 2. select package main im ...

  5. golang中channel讲解

    1. 无缓冲通道 2. 有缓冲通道 有缓冲通道特点:当channel已经满,在向里面写数据就会阻塞,当channel已经为空,在从里面读数据就会阻塞. 3. 关闭channel package mai ...

  6. 【GoLang】golang中 channel 实现同步 与mutex/atomic 实现同步的讨论

    参考资料: https://groups.google.com/forum/#!topic/golang-china/q4pFH-AGnfs

  7. golang中并发sync和channel

    golang中实现并发非常简单,只需在需要并发的函数前面添加关键字"go",但是如何处理go并发机制中不同goroutine之间的同步与通信,golang 中提供了sync包和channel ...

  8. 说说不知道的Golang中参数传递

    本文由云+社区发表 导言 几乎每一个C++开发人员,都被面试过有关于函数参数是值传递还是引用传递的问题,其实不止于C++,任何一个语言中,我们都需要关心函数在参数传递时的行为.在golang中存在着m ...

  9. Golang 入门 : channel(通道)

    笔者在<Golang 入门 : 竞争条件>一文中介绍了 Golang 并发编程中需要面对的竞争条件.本文我们就介绍如何使用 Golang 提供的 channel(通道) 消除竞争条件. C ...

随机推荐

  1. webpack4 mini-css-extract-plugin

    在使用webpack的extract-text-webpack-plugin插件提取单独打包css文件时,报错,说是这个插件要依赖webpack3的版本. webpack4得使用mini-css-ex ...

  2. Mac搭建Git服务器—开启SSH

    SSH开启 在osx中开启ssh访问非常简单,只需要打开"系统偏好设置"并且点击"共享"图标即可. 选中下图中的check box即允许远程登陆.server处 ...

  3. 虫师Selenium2+Python_00学习大纲

        1.自动化测试基础 5.自动化测试模型 9.Selenium Grid2 13.GitHub托管项目 2.测试环境搭建 6.SeleniumIDE 10.Python多线程 14.持续集成Je ...

  4. Spring IOC-基于注解配置的容器

    Spring中提供了基于注解来配置bean的容器,即AnnotationConfigApplicationContext 1. 开始 先看看在Spring家族中,AnnotationConfigApp ...

  5. ConcurrentHashMap怎么保证安全的

    HashMap是一个线程不安全的容器,当容量大于总量*负载因子发生扩容时可能会出现环形链表从而导致死循环 扩容就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下, ...

  6. 5、Linux基础--etc(文件系统)、启动模式、单用户模式修改密码、安装目录、日志目录、状态目录

    笔记 1.晨考 1.存放系统配置文件的目录 /etc 2.存储系统实时运行状态的目录 /proc 3.存储系统硬件接口的目录 /dev 4.查看系统挂载情况的命令 df -h 5.系统网卡文件路径 / ...

  7. C++ 反汇编:关于函数调用约定

    函数是任何一门高级语言中必须要存在的,使用函数式编程可以让程序可读性更高,充分发挥了模块化设计思想的精髓,今天我将带大家一起来探索函数的实现机理,探索编译器到底是如何对函数这个关键字进行实现的,并使用 ...

  8. 「BUAA OO Pre」Git生成多个ssh key并连接GitLab仓库

    「BUAA OO Pre」Git生成多个ssh key并连接GitLab仓库 Part 0 前言 写作背景 笔者在配置学校GitLab的ssh key时遇到一些问题,原因应为曾经配置过GitHub的s ...

  9. Java并发基础之Compare And Swap/Set(CAS)

    什么是 CAS?CAS(Compare And Swap/Set)比较并交换, CAS 算法的过程是这样:它包含 3 个参数CAS(V,E,N). V 表示待更新的变量(内存值), E 表示预期值(旧 ...

  10. 从Spring容器的角度理解Dubbo扩展点的加载时机

    对于Dubbo提供的扩展点,主程序执行的过程中并没有显示调用加载的过程,无论是自激活的Filter还是自适应的ThreadPool.那么这样的扩展点在程序运行的哪个节点调用的呢?跟踪之前性能监控扩展点 ...