Go 语言并发编程(1) | 青训营笔记

80 阅读3分钟

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 表达式都会被求值。
  • 所有被发送的表达式都会被求值。