并发编程 1 (Goroutine)
1. 并发基础概念
串行/并行/并发
串行:我们都是先读小学,小学毕业后再读初中,读完初中再读高中。
并发:同一时间段内执行多个任务(你在用微信和两个朋友聊天)。
并行:同一时刻执行多个任务(你和你朋友都在用微信和指定朋友聊天)。
进程、线程和协程
进程(process):程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
线程(thread):操作系统基于进程开启的轻量级进程,是操作系统调度执行的最小单位。
协程(coroutine):非操作系统提供而是由用户自行创建和控制的用户态‘线程’,比线程更轻量级。
并发机制简述
Go 语言中的并发程序主要是通过基于 CSP(communicating sequential processes)的 goroutine 和 channel 来实现,当然也支持使用传统的多线程共享内存的并发方式。
Goroutine 是 Go 语言支持并发的核心,在一个 Go 程序中同时创建成百上千个 goroutine 是非常普遍的,一个 goroutine 会以一个很小的栈开始其生命周期,一般只需要 2KB。
区别于操作系统线程由系统内核进行调度, goroutine 是由 Go 运行时(runtime)负责调度。
例如 Go 运行时会智能地将 m 个 goroutine 合理地分配给 n 个操作系统线程,实现类似 m:n 的调度机制,不再需要 Go 开发者自行在代码层面维护一个线程池。
2. 线程启动
关键
Go 语言中使用 goroutine 非常简单,只需要在函数或方法调用前加上 go 关键字就可以创建一个 goroutine ,从而让该函数或方法在新创建的 goroutine 中执行。
go f() // 创建一个新的 goroutine 运行函数f
go func(){
// ...
}()//或者这样,注意避免形成闭包函数,出现value不合理的问题
2.1 启动单一线程
package main
import (
"fmt"
"sync"
)
// 声明全局等待组变量
var wg sync.WaitGroup
func hello() {
fmt.Println("hello")
wg.Done() // 告知当前goroutine完成,计数拍-1
}
func main() {
wg.Add(1) // 登记1个goroutine
go hello()
fmt.Println("你好")
wg.Wait() // 阻塞等待登记的goroutine完成(等到计数排为0是才会释放阻塞)
}
2.2 启动多个线程
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func hello(i int) {
defer wg.Done() // goroutine结束就登记-1
fmt.Println("hello", i)
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个goroutine就登记+1
go hello(i)
}
wg.Wait() // 等待所有登记的goroutine都结束
}
3. 线程的相关理论
线程动态栈空间
- 操作系统的线程一般都有固定的栈内存(通常为 2MB),而 Go 语言中的 goroutine 非常轻量级,一个 goroutine 的初始栈空间很小(一般为 2KB),
- 所以在 Go 语言中一次创建数万个 goroutine 也是可能的。
- 并且 goroutine 的栈不是固定的,可以根据需要动态地增大或缩小, Go 的 runtime 会自动为 goroutine 分配合适的栈空间。
底层实现
- Go 运行时的调度器使用
GOMAXPROCS参数来确定需要使用多少个 OS 线程来同时执行 Go 代码。默认值是机器上的 CPU 核心数。例如在一个 8 核心的机器上,GOMAXPROCS 默认为 8. - Go 语言中可以通过
runtime.GOMAXPROCS函数设置当前程序并发时占用的 CPU 逻辑核心数。(Go1.5 版本之前,默认使用的是单核心执行。Go1.5 版本之后,默认使用全部的 CPU 逻辑核心数。) :::