goroutine
进程和线程
- 进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位
- 线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位
- 一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行
- 一个程序至少有一个进程,一个进程至少有一个线程
程序,进程和线程的关系
并发和并行
- 多线程程序在单核上运行,就是并发
- 多线程程序在多核上运行,就是并行
并发:
因为是在一个cpu上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发
并行:
因为是在多个cpu上(比如有10个cpu),比如有10个线程,每个线程执行10毫秒(各自在不同cpu上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行
go协程和go主线程
go主线程(可以称为线程/也可以理解成进程):一个go线程上,可以起多个协程,协程是轻量级的线程【编译器做优化】
go协程的特点
- 有独立的栈空间
- 共享程序堆空间
- 调度由用户控制
- 协程是轻量级的线程
goroutine案例
- 在主线程(可以理解成进程)中,开启一个goroutine, 该协程每隔1秒输出 “hello, world”
- 在主线程中也每隔1秒输出"hello, golang",输出10次后退出程序
- 要求主线程和goroutine同时执行
package main
import (
"fmt"
"strconv"
"time"
)
func test() {
for i := 1; i <= 10; i++ {
fmt.Println("test() hello, world " + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
func main() {
go test() //开启了一个协程
for i := 1; i <= 10; i++ {
fmt.Println("main() hello, golang " + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
说明:
- 主线程是一个物理线程,直接作用在cpu上的。是重量级的,非常耗费cpu资源
- 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小
- golang的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显golang在并发上的优势
goroutine的调度模型
MPG模式
- M: 操作系统的主线程(物理线程)
- P: 协程执行需要的上下文
- G: 协程
MPG模式运行的状态1
- 当前程序有三个M,如果三个M都在一个cpu运行,就是并发,如果在不同的cpu运行就是并行
- M1,M2,M3正在执行一个G,M1的协程队列有三个,M2的协程队列有三个,M3协程队列有2个
- 可以看到: go的协程是轻量级的线程,是逻辑态的。go可以容易的起上万个协程
- 其它程序c/java的多线程,往往是内核态的,比较重量级,几千个线程可能耗光cpu
MPG模式运行的状态2
- 分成两个部分来看
- 原来的情况是M0主线程正在执行G0协程,另外有三个协程在队列等待
- 如果G0协程阻塞,比如读取文件或者数据库等
- 这时就会创建M1主线程(也可能是从已有的线程池中取出M1),并且将等待的3个协程挂到M1下开始执行,M0的主线程下的G0仍然执行文件io的读写
- 这样的MPG调度模式,可以既让G0执行,同时也不会让队列的其它协程一直阻塞,仍然可以并发/并行执行
- 等到G0不阻塞了,M0会被放到空闲的主线程继续执行(从已有的线程池中取),同时G0又会被唤醒
设置golang运行的cpu数
为了充分利用多cpu的优势,在golang程序中,设置运行的cpu数目
package main
import (
"fmt"
"runtime"
)
func main() {
//获取当前系统cpu的数量
num := runtime.NumCPU()
//设置num-1的cpu运行go程序
runtime.GOMAXPROCS(num)
fmt.Println("num=", num)
}
- go1.8后,默认让程序运行在多个核上,可以不用设置了
- go1.8前,还要设置下,可以更高效的利用cpu
channel(管道)
计算1-200的各个数的阶乘,并且把各个数的阶乘放入到map中。最后显示出来。使用goroutine完成
package main
import (
"fmt"
"time"
)
//1.编写一个函数,计算各个数的阶乘,并放入到map中
//2. 启动的多个协程,统计的将结果放入到map中
//3. map应该做出一个全局的
var (
myMap = make(map[int]int, 10)
)
//test函数就是计算n!,将这个结果放入到myMap
func test(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
myMap[n] = res
}
func main() {
//开启多个协程完成这个任务[200个]
for i := 1; i <= 200; i++ {
go test(i)
}
//休眠10秒
time.Sleep(time.Second * 10)
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
}
不同goroutine之间如何通讯
- 全局变量的互斥锁
- 使用管道channel
使用全局变量加锁同步改进程序
- 没有对全局变量加锁,会出现资源争夺问题,代码会出现错误,提示 concurrent map writes
- 解决:加入互斥锁
- 数的阶乘很大,结果会越界,可以将求阶乘改成 sum += i
package main
import (
"fmt"
"sync"
"time"
)
//1.编写一个函数,计算各个数的阶乘,并放入到map中
//2. 启动的多个协程,统计的将结果放入到map中
//3. map应该做出一个全局的
var (
myMap = make(map[int]int, 10)
//声明一个全局的互斥锁
//lock 是一个全局的互斥锁
//sync 是包: synchornized 同步
//Mutex : 是互斥
lock sync.Mutex
)
//test函数就是计算n!,将这个结果放入到myMap
func test(n int) {
res := 1
for i := 1; i <= n; i++ {
res += i
}
//res放入到myMap
//加锁
lock.Lock()
myMap[n] = res
//解锁
lock.Unlock()
}
func main() {
//开启多个协程完成这个任务[200个]
for i := 1; i <= 200; i++ {
go test(i)
}
//休眠10秒
time.Sleep(time.Second * 10)
lock.Lock()
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
lock.Unlock()
}