goroutine间的通信机制-channel入门与实战
背景
线程间的通讯
协程没有出现之前,线程间的通信主要是以下几种方式:
- 共享内存加锁,缺点开销比较大并且容易产生死锁。解决方法就是避免加锁、顺序加锁、锁超时等。
共享内存
死锁
- 消息传递,线程间通过发送和接受消息来进行的通信。
从线程间的通信可以使用消息传递的方式相对于共享内存更加简单,不会出现复杂死锁这种复杂问题但是会出现队列阻塞这种情况下的死锁。go语言协程间的通讯也是使用的消息传递的方式,程通信队列为channel。
channel介绍
协程中channel是协程之间的通信通道,每一个channel只能缓存一种类型的数据,存储结构类似于队列的形式,可以被用于goroutine(可以是不同线程的groutine)间的通信与实现异步执行,从队尾添加数据,从队头删除删除数据,两个操作都是协程安全,因为channel内置了一些加锁操作。
基本使用
// 声明channel
var ch chan int
//初始化channel
go func() {
ch <- 1 // 向channel发送数据,完成初始化
}()
// 从通道中取出数据,赋值给i
i := <-ch
//同时声明和初始化
ch := make(chan int,8) //指定容量为8,默认为0
ch <- 1 // 直接发送数据到channel
阻塞特性下的同步机制
如果通道里面没有缓存区域并且没有goroutine接收数据,此时协程就会阻塞直到有缓存区或有goroutine接收通道数据,接收数据的时候如果通道内没有数据,接收的goroutine就会发生阻塞。通过缓存容量限制可以控制流量。根据这种特性channel可以实现协程间的同步机制,例如,我们可以在一个goroutine执行完成一段逻辑后,通过channel通知另一个goroutine执行其他操作。
实现消费者生产者
// 生产者
func producer(ch chan int) {
for i := 0; i < 10; i++ {
ch <- i // 发送数据到channel
}
close(ch) // 关闭channel
}
// 消费者
func consumer(ch chan int) {
for num := range ch { // 从channel接收数据
fmt.Println(num)
}
}
func main() {
ch := make(chan int)
go producer(ch)
consumer(ch) // 消费channel数据
}
实现信号量(限制goroutine的并发数)
sema := make(chan struct{}, 3) // 定义一个容量为3的信号量
// 对sema发送信号,模拟请求获取信号量
sema <- struct{}{}
// 对sema接收信号,模拟释放信号量
<-sema
select机制goroutine监听多个channel
go语言中通过select可以监听多个channel(select没有default)
select {
case <- channelA: //to do logic
case B <- channelB: //to do logic
case C <- channelC: // to do logic
}
实现超时机制
select {
case <- ch: // 从channel接收数据
// ...
case <- time.After(timeout): // 超时
// 处理超时逻辑
}
广播机制
一个channel可以被多个goroutine读取,利用这一特性可以实现发布订阅的消息广播
// 创建channel
messages := make(chan string)
// 接收器goroutine
go func() {
for msg := range messages {
fmt.Println("Received message:", msg)
}
}()
// 接收器goroutine
go func() {
for msg := range messages {
fmt.Println("Received message:", msg)
}
}()
// 发送广播消息
messages <- "Message 1"
messages <- "Message 2"
关闭机制
当channel关闭的时候,此时只能读取channel的信息不能发送给channel信息。goroutine结束后最好关闭channel避免内存泄漏。为了避免多个goroutine同时读取channel可以加上读取和写入锁,有时候要避免过度使用channel。
close(ch)