golang笔记10--go语言并发编程模块 goroutine
1 介绍
本文继上文 golang笔记09–go语言测试与性能调优, 进一步了解 go语言的并发编程模块 --goroutine,以及相应注意事项。
具体包括 : goroutine 和 go语言的调度器 等内容。
2 Goroutine
2.1 goroutine
主流的并发模型包括:多进程、多线程、基于回调的非阻塞|异步IO、协程 等方式,其优缺点如下:
并发模型 | 基本原理 | 优点 | 缺点 |
---|---|---|---|
多进程 | 操作系统层面进行并发的基本模式 | 简单、进程见互不影响 | 系统开销大 |
多线程 | 也属于操作系统层面的并发模型,但其比进程效率高 | 开销低,可提高 cpu 内存利用率 和 执行效率 | 线程会占用一定资源,使用过多会降低程序性能;程序设计比多进程复杂 |
非阻塞|异步IO | 通过事件驱动的方式进行IO,CPU执行指令后不需要立即返回,过段时间执行完成后通知cpu继续处理相关任务 | 能够降低线程使用两,适用于IO密集操作 | 编程相对复杂 |
协程 | 本质是用户态的线程(轻量级线程),不需要操作系统进行抢占式调度,属于编译器|解释器|虚拟机层面的多任务,多个协程可以在一个或多个线程上运行 | 开销极小能提高并发性,编程简单结构清晰 | 需要特定语言的支持,若不支持则小自行实现调度器 |
go 语言原生支持轻量级线程,即 goroutine,它由go运行时管理。 Go语言从初始化main package 并执行main() 函数开始,当mai()函数返回时程序退出,且程序不等待其它 goroutine(非主goroutine) 结束。
以下案例中通过1000 个 goroutine 输出当前时间 + hello 信息,为了避免主 goroutine 退出,当前使用较粗暴的方式直接 sleep 一分钟。
vim 10.1.go
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
/*
for i := 0; i < 1000; i++ {
go func(i int) {
for {
fmt.Printf("%s hello,i'm from goroutine %d\n", time.Now().String(), i)
}
}(i)
}
time.Sleep(time.Minute)
*/
var a [10]int
for i := 0; i < 10; i++ {
go func(i int) {
for {
a[i]++
runtime.Gosched()
}
}(i)
}
time.Sleep(time.Second)
fmt.Println("goroutine a[i]++")
fmt.Println(a)
}
输出(1):
2021-02-17 11:41:06.938943939 +0800 CST m=+3.550203203 hello,i'm from goroutine 469
2021-02-17 11:41:06.663614907 +0800 CST m=+3.274874166 hello,i'm from goroutine 5
2021-02-17 11:41:07.164039483 +0800 CST m=+3.775298734 hello,i'm from goroutine 469
2021-02-17 11:41:07.16404103 +0800 CST m=+3.775300290 hello,i'm from goroutine 5
2021-02-17 11:41:07.069884497 +0800 CST m=+3.681143750 hello,i'm from goroutine 785
......
输出(2):
goroutine a[i]++
[575155 549057 578855 550274 575947 557208 537390 548969 563552 539226]
注意:
此处使用函数式编程,必须将 i 作为参数传入;若不传入i,则函数闭包,会使用外部的i,最终导致使用到 a[10] 报错;
程序运行结果不达预期,可能原因为存在数据冲突,此时可以通过 go run -race 10.1.go 的形式来检测。
2.2 go语言的调度器
从程序角度来看,子程序是协程的一个特例,子程序代表的普通函数和协程的对比如下,从图中可见协程包含了普通函数:
从调度角度来看,一个或者多个协程都有可能被调度到某个线程中,其由调度器决定,一般情况下不需要用户关心。
goroutine 的定义:
- 任何函数只需要加上go就能送给调度器运行;
- 不需要在定义时区分是否是异步函数;
- 调度器在合适的点进行切换;
- 使用 -race 来检查数据访问冲突;
goroutine 可能的切换点:
- I/O, select
- channel
- 等待锁
- 函数调用(有可能)
- runtime.Gosched()
3 注意事项
- Go语言从初始化main package 并执行main() 函数开始,当mai()函数返回时程序退出,且程序不等待其它 goroutine(非主goroutine) 结束;因此可以使用sleep 等简单粗暴的方式避免程序退出。
4 说明
- 软件环境
go版本:go1.15.8
操作系统:Ubuntu 20.04 Desktop
Idea:2020.01.04 - 参考文档
由浅入深掌握Go语言 --慕课网
go 语言编程 --许式伟