Go 语言并发编程(1)
1. 并发与并行
并发:同一时间段内执行多个任务(CPU 调度切换),多个任务之间有可能是串行的。
并行:同一时刻执行多个任务(多核 CPU),多个任务之间是同时进行的。
2. goroutine
Go 语言中的并发通过 goroutine 实现。goroutine 类似于线程,属于用户态的线程,我们可以根据需要创建成千上万个 goroutine 并发工作。
2.1 goroutine 的创建
func main() {
go func() {
fmt.Println("Hello World!")
}()
time.Sleep(time.Second)
}
2.2 goroutine 的本质
goroutine 的本质是一个函数,我们通过 go 关键字来启动一个函数,这个函数并不会阻塞当前的函数。
2.3 goroutine 的调度
Go 语言中的 goroutine 和线程是多对多的关系,即 m:n。一个 goroutine 底层对应一个线程,一个线程对应一个操作系统的内核线程。
Go 语言的调度器使用了 M:N 调度中的 工作窃取技术。M 个操作系统线程对应 N 个 goroutine,M 和 N 是变化的,M:N 调度会复用和重复利用少量的操作系统线程,来执行大量的 goroutine。
2.4 goroutine 的实现原理
Go 语言中的 goroutine 是由 Go 语言的运行时(runtime)调度完成,而线程是由操作系统调度完成。
runtime.GOMAXPROCS(n) 用来设置可以并行计算的 CPU 核数的最大值,并返回之前的值。如果 n < 1,不会改变当前设置。以后 Go 语言的新版本中,这个函数会被废弃。
func main() {
n := runtime.GOMAXPROCS(1)
fmt.Println(n)
}
2. channel
Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明 channel 的时候需要为其指定元素类型。
2.1 channel 的声明
var 变量 chan 元素类型
2.2 channel 的初始化
make(chan 元素类型, [缓冲大小])
2.3 channel 的操作
ch <- x // 把 x 发送到(写入)通道 ch
x = <-ch // 从通道 ch 接收(读取)数据,并把值赋给 x
<-ch // 从通道 ch 接收数据,忽略结果
2.4 channel 的关闭
close(ch)
2.5 channel 的注意事项
- 向已经关闭的通道发送数据会引发 panic。
- 重复关闭同一个通道会引发 panic。
- 向已经关闭的通道接收数据可以正常获取数据。
2.6 channel 的应用场景
- 用于多个 goroutine 之间进行通信。
- 用于 goroutine 和 main 函数之间进行通信。
3. select
select 语句用于在多个发送/接收信道操作中进行选择。select 语句会一直阻塞,直到发送/接收操作准备就绪。如果有多个信道操作准备完毕,select 会随机地选取其中之一执行。
3.1 select 的声明
select {
case <-ch1:
// 如果 ch1 通道成功读到数据,则进行该 case 处理语句
case data := <-ch2:
// 如果成功从 ch2 通道读取数据,则进行该 case 处理语句
case ch3 <- data:
// 如果成功向 ch3 通道写入数据,则进行该 case 处理语句
default:
// 如果上面都没有成功,则进入 default 处理流程
}
3.2 select 的注意事项
- 每个 case 都必须是一个通信操作,要么是发送要么是接收。
- 所有 channel 表达式都会被求值。
- 所有被发送的表达式都会被求值。