go channel简介
go语言有一句很经典的话,不要通过共享内存来通信,而应该通过通信来共享内存;
传统多线程编程的局限
在Java等多线程编程中,线程间通信的主流方式是通过共享内存来通信。具体来说,多个线程之间通过共享一个全局变量来访问同一块内存区域,写线程将数据放入变量所指定的内存,读线程从该变量读取数据。然而,这种方式很容易导致数据访问冲突的问题,即多个线程同时访问同一个变量时,可能会出现数据不一致的情况。为了解决这一问题,需要引入锁机制来保证同一时间点该数据只能被一个线程访问,但这又带来了新的问题,如死锁、线程阻塞以及上下文切换带来的额外开销。
Go语言的并发编程模型
Go语言从进程、线程基础上发展出了更轻量级的协程(goroutine),并为其设计了一套独特的并发编程模型。Go语言的作者深受通信顺序进程(Communicating Sequential Process,CSP)思想的启发,并在Go语言中实践了这套思想。CSP认为,程序就是一组无共享状态进程的并行组合,进程间的通信和同步采用信道(在Go语言中即Channel)完成。
通过通信来共享内存的优势
在Go语言中,通过通信来共享内存的方式主要体现在使用Channel这一数据结构上。Channel类似于一个管道,有数据的输入端和接收端,不同协程可以根据实际需求选择发送数据到Channel中或是从Channel中接收数据。这种方式的优势在于:
- 简化并发编程:Go语言通过Channel为开发者提供了一种优雅简单的并发编程工具,使得开发者在编写并发程序时不必再陷入到各种锁的操作中。
- 避免数据访问冲突:由于同一时间只有一个协程能够访问Channel里面的数据,因此可以避免多线程编程中的数据访问冲突问题。
- 提高程序的可读性和可维护性:使用Channel进行通信可以使程序的结构更加清晰,逻辑更加简单,从而提高程序的可读性和可维护性。
channel基本使用
无缓冲Channel的使用
无缓冲channel是指没有缓冲区的channel,发送操作会阻塞发送方,直到另一方准备好接收数据。同样,接收操作也会阻塞接收方,直到有数据可以接收。
package main
import (
"fmt"
"time"
)
func main() {
// 创建一个无缓冲的整型channel
c := make(chan int)
// 启动一个新的goroutine,用于从channel中接收数据
go func() {
num := <-c // 接收数据,阻塞直到有数据可以接收
fmt.Println(num)
}()
// 向channel中发送数据
c <- 1 // 发送数据,阻塞直到另一方准备好接收
// 为了保证goroutine有足够的时间去接收channel中的值,这里等待3秒钟
time.Sleep(time.Second * 3)
fmt.Println("main function return")
}
//运行结果
1
main function return
Process finished with the exit code 0
有缓冲Channel的使用
有缓冲channel是指带有缓冲区的channel,发送操作会在缓冲区未满的情况下立即返回,接收操作也会在缓冲区不为空的情况下立即返回。
package main
import (
"fmt"
)
func main() {
// 创建一个带有缓冲区大小为2的整型channel
c := make(chan int, 2)
// 向channel中发送数据
c <- 1
c <- 2
// 启动一个新的goroutine,用于从channel中接收数据
go func() {
for num := range c { // 使用range循环接收数据,直到channel被关闭
fmt.Println(num)
}
}()
// 继续向channel中发送数据,此时会阻塞,直到有接收方接收数据或缓冲区已满
c <- 3
// 为了保证goroutine有足够的时间去接收channel中的值,这里等待一段时间(实际应用中可能需要更复杂的同步机制)
time.Sleep(time.Second)
// 关闭channel(注意:向一个已关闭的channel发送数据会导致panic)
close(c)
}
//运行结果
1
2
3
Process finished with the exit code 0
单向channel
单向channel是一种限制channel使用方向的机制。通过单向channel,我们可以确保一个函数只能发送数据到channel中,或者只能从channel中接收数据,但不能同时进行这两种操作。
只发送的单向Channel
var sendChan chan<- int //这表示sendChan是一个只能发送整数的单向channel。
只接收的单向Channel
var recvChan <-chan int //这表示recvChan是一个只能接收整数的单向channel。
package main
import "fmt"
// 定义一个只发送整数的函数
func sendData(out chan<- int) {
out <- 42 // 向channel发送数据
}
// 定义一个只接收整数的函数
func receiveData(in <-chan int) {
data := <-in // 从channel接收数据
fmt.Println(data)
}
func main() {
// 创建一个双向的整型channel
ch := make(chan int)
// 启动一个新的goroutine来发送数据
go sendData(ch)
// 在主goroutine中接收数据
receiveData(ch)
}
channel数据结构
hchan:channel 数据结构
type hchan struct {
qcount uint
dataqsiz uint
buf unsafe.Pointer
elemsize uint16
closed uint32
elemtype *_type
sendx uint
recvx uint
recvq waitq
sendq waitq
lock mutex
}
- qcount:当前 channel 中存在多少个元素;
- dataqsize: 当前 channel 能存放的元素容量;
- buf:channel 中用于存放元素的环形缓冲区;
- elemsize:channel 元素类型的大小;
- closed:标识 channel 是否关闭;
- elemtype:channel 元素类型;
- sendx:发送元素进入环形缓冲区的 index;
- recvx:接收元素所处的环形缓冲区的 index;
- recvq:因接收而陷入阻塞的协程队列;
- sendq:因发送而陷入阻塞的协程队列;
waitq
waitq:阻塞的协程队列
type waitq struct {
first *sudog
last *sudog
}
- first:队列头部
- last:队列尾部
sudog
sudog:用于包装协程的节点
type sudog struct {
g *g
next *sudog
prev *sudog
elem unsafe.Pointer // data element (may point to stack)
isSelect bool
c *hchan
}
- g:goroutine,协程;
- next:队列中的下一个节点;
- prev:队列中的前一个节点;
- elem: 读取/写入 channel 的数据的容器;
- isSelect:标识当前协程是否处在 select 多路复用的流程中;
- c:标识与当前 sudog 交互的 chan.
往channel中写数据
- 对于未初始化的 chan,写入操作会引发死锁
- 对于已关闭的 chan,写入操作会引发 panic
func chansend1(c *hchan, elem unsafe.Pointer) {
chansend(c, elem, true, getcallerpc())
}
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
if c == nil {
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
lock(&c.lock)
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
从channel中读取数据
对于未初始化的 chan,读取操作会引发死锁
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
if c == nil {
gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
throw("unreachable")
}
}
关闭channel
关于channel的几点总结
- 关闭一个末初始化的 channel 会产生 panic。
- channel只能被关闭一次,对同一个channel重复关闭会产生 panic。
- 向一个已关闭的 channel 发送消息会产生 panic。
- 从一个已关闭的channel读取消息不会发生panic,会一直读取所有数据,直到零值。
- channel可以读端和写端都可有多个goroutine操作,在一端关闭channel的时候,该channel读端的所有goroutine 都会收到channel已关闭的消息。
- channel是并发安全的,多个goroutine同时读取channel中的数据,不会产生并发安全问题