使用
- goroutine无法设置优先级,无法获取ID,没有局部储存,返回值被忽略
- 可以通过channel传递返回值
- 通过设置
runtime.GOMAXPROCS(n)
可以控制真正的并行度- 默认值为CPU核数:
runtime.GOMAXPROCS(runtime.NumCPU())
- 在某些IO密集型的应用里,这个值可能并不意味着性能最好
- 理论上当某个Goroutine进入系统调用时,会有一个新的M被启用或创建,继续占满CPU。但由于Go调度器检测到M被阻塞是有一定延迟的,也即旧的M被阻塞和新的M得到运行之间是有一定间隔的
- 所以在IO密集型应用中不妨把GOMAXPROCS设置的大一些
- 在某些IO密集型的应用里,这个值可能并不意味着性能最好
- 并不一定是物理线程数
- 如果物理线程阻塞在系统调用,会开启更多的物理线程(M)
- 陷入系统调用被阻塞时M将释放P给某个空闲的M,继续执行P队列中剩下的G
- 只有 n 个活跃的线程执行 user-level 代码
- 如果物理线程阻塞在系统调用,会开启更多的物理线程(M)
- 默认值为CPU核数:
-
可以利用
runtime.Gosched()
让协程轮流执行runtime.Gosched()
表示让CPU把时间片让给别人,下次某个时候继续恢复执行该goroutine- 该函数很少被使用,因为无法保证调度总是成功
-
和
defer
一样, goroutine也会对传入的参数立刻进行计算
var c int
func counter() {
c++
return c
}
go func(x){
counter()
fmt.Print(x) // 打印出0,而不是1
}(counter())
- 并行for循环
- 注意
i, xi
需要通过参数传入而不能当成闭包的环境,否则会出错- 也可以在for循环中赋值给新建的局部变量解决
- 原因在于
i, xi
在循环的过程中地址不变
- 注意
for i, xi := range data {
go func (i int, xi float64) {
res[i] = doSomething(i, xi)
sem <- empty
} (i, xi)
}
// wait for goroutines to finish
for i := 0; i < N; i++ { <-sem }
- 多个goroutine协调工作涉及 通信,同步,通知,退出 四个方面
- 通信:chan通道是各goroutine之间通信的基础。注意这里的通信主要指程序的数据通道。
- 同步:可以使用不带缓冲的chan;sync.WaitGroup为多个gorouting提供同步等待机制;mutex锁与读写锁机制。
- 通知:通知与上文通信的区别是,通知的作用为管理,控制流数据。一般的解决方法是在输入端绑定两个chan,通过select收敛处理。这个方案可以解决简单的问题,但不是一个通用的解决方案。
- 退出:简单的解决方案与通知类似,即增加一个单独的通道,借助chan和select的广播机制(close chan to broadcast)实现退出
原理
-
goroutine采用M:N模型,在所有线程上进行多路复用
- 当一个goroutine被阻塞时,所在的线程也被阻塞,而go的runtime会把该线程上的其他协程移动到未阻塞的线程
- 已存在的 goroutine 总是会被优先复用
- 每个goroutine拥有自己的寄存器上下文和栈
- 优点
- 控制了系统线程的数量
- 保证每个线程一定的运行时间
- 避免线程过多而导致调度开销过大
- 程序可以在用户态自行调度
- 避免了单个协程阻塞了整个线程的情况、
- 避免了大量内核态和用户态切换
- 更小的栈空间占用
- 允许用户创建成千上万的实例
- 控制了系统线程的数量
- 当一个goroutine被阻塞时,所在的线程也被阻塞,而go的runtime会把该线程上的其他协程移动到未阻塞的线程
-
调度方式
- 在runtime中实现了一个调度器, 一旦进行了Golang库函数调用, runtime就有机会进行调度
- 如果写了死循环就没法触发goroutine的调度
- 操作系统会在物理处理器上调度线程来运行,而Go 语言的运行时会在逻辑处理器上调度
goroutine来运行 - 调度器把goroutine调度到的CPU不一定是程序进程所在的CPU
- 在runtime中实现了一个调度器, 一旦进行了Golang库函数调用, runtime就有机会进行调度
- 只要P不空闲,就可以保证充分利用CPU
- 一般情况下M的个数会略大于P的个数
- 这多出来的M将会在G产生系统调用时发挥作用
- 这多出来的M将会在G产生系统调用时发挥作用