并发是go为什么被重视的重要原因,相比于其他语言来说,go语言实现并发是非常容易的一件事
启动多个goroutine
当我们启动了多个goroutine时,可能有些goroutine还没有执行完main函数就已经退出了,main函数一结束其余的goroutine就直接退出了,为了解决这一问题我们可以使用sync.WaitGroup来解决这一问题,例如:
var wg sync.WaitGroup
func count(i int) {
defer wg.Done() // goroutine结束就-1
fmt.Println("Goroutine:", i)
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个goroutine就+1
go hello(i)
}
wg.Wait() // 等待所有登记的goroutine都结束
}
上面启动了10个goroutine并发执行,打印的i顺序是不定的,但是可以确保所有都执行完再结束main函数
GMP模型
Go语言运行时有自己的一套调用系统,与OS线程调度是有区别的 G(goroutine):,里面有自己的信息,还有与P的一些关系的信息,它非常地轻量,一个goroutine只占几KB P(Processor):管理一组goroutine队列,如果M要调用G时,就必须在这里获取 M(thread):每个M都代表了1个内核线程,OS调度器负责把内核线程分配到CPU上运行
我们看一下每个节点的作用:
CPU:线程M在CPU上运行,一个CPU一次只能处理一个线程
M:通过P的本地队列中获取G,把G放在M上执行,执行完再获取
OS调度器:管理M和CPU之间的调度
P:goroutine 执行所需的资源,协调M和G,所有P都在程序启动时创建,最多有GOMAXPROCS个。P与M一一对应,当队列消费完了就会去全局队列里取,如果全局队列也没了就会去其他P里抢任务
goroutine调度器:管理P和G之间的调度
P的本地队列:存放等待运行的G, 新建G时会优先加入到P的本地队列, 如果队列满了, 则会把本地队列中一半的G移动到全局队列
全局队列:存放等待运行的G
如果需要更详细的解析,以上的GMP模型图文参考:www.jianshu.com/p/90d20e3da…
GOMZXPROCS
GOMZXPROCS调度器的默认值决定于这台机子上有几个核心,他就有几个,我们可以通过runtime.GOMAXPROCS()来设置并发时占用几个CPU
Go语言中的操作系统线程和goroutine的关系:
- 一个线程对应着多个goroutine。
- go程序可以同时使用多个操作系统线程。
- goroutine和OS线程是多对多的关系。
CSP(Communicating Sequential Processes)并发模型
使用信道通信共享内存,go语言中通过channel连接起goroutine,goroutine之间通过channel通信,channel是引用类型,像一个队列,数据实行先进先出原则
锁确保并发安全
互斥锁
它可以确保同一时间只有一个goroutine访问共享的资源下面来解释一些为什么需要互斥锁,先看下面代码:
var x int64
var wg sync.WaitGroup
var lock sync.Mutex // 互斥锁
func add() {
for i := 0; i < 5000; i++ {
lock.Lock() // 加锁
x = x + 1
lock.Unlock() // 解锁
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}
x=x+1是分为三部执行的,第一步从一个地方取出x,第二步对x加1,第三步放回去,再这个过程中如果其他的goroutine来拿x则拿到的x可能是还没加完的x,这样就会导致数据混乱;
这个时候为了使这三步一气呵成,就可以在其执行前加互斥锁,使其他goroutine不能访问,执行完了再解锁;当互斥锁释放后,等待的goroutine才可以获取锁进入临界区,多个goroutine同时等待一个锁时,唤醒的策略是随机的。
读写互斥锁
读写锁分为两种:读锁和写锁。
加上读锁后:说明我要开始读了,其他人可以来读,但是不能写
加上写锁后:我要写了,其他人什么都不能干
读写锁使用的是sync包中的RWMutex类型,可以这样var rwlock sync.RWMutex定义读写锁,rwlock.Lock()加写锁,对应rwlock.Unlock解锁;rwlock.RLock()加写锁,对应rwlock.RUnlock解锁;