一、goroutine基础介绍
goroutine是Golang中的协程,它是一种微线程,比起线程它耗费更少的资源。线程的作用就是可以进行并发或者并行,完全利用电脑多核的资源。
- 并发 多个任务跑在一个cpu上,在某一时刻只处理一个任务,任务之间来回切换的时间极短
- 并行 多个任务跑在多个cpu上,在某一时刻多个cpu处理多个任务,达到并行的效果
那么在Golang中是如何来使用goroutine的呢?Golang的主线程中可以启动多个协程。比如现在有一个案例:
- 主线程启动一个协程来每隔1秒输出"Hello goroutine",输出10次
- 主线程每隔1秒输出“Hello main”,输出10次
- 主线程与协程同时工作
package main import ( "fmt" "time" ) func printGoroutine() { // 协程执行函数 for i := 0; i < 5; i++ { time.Sleep(time.Second) fmt.Printf("Hello goroutine %v \n", i) } } func main() { // 主线程 // 开启一个协程 go printGoroutine() // 继续执行主协程代码 for i := 0; i < 5; i++ { time.Sleep(time.Second) fmt.Printf("Hello main %v \n", i) } } /* 输出: Hello main 0 Hello goroutine 0 Hello goroutine 1 Hello main 1 Hello main 2 Hello goroutine 2 Hello goroutine 3 Hello main 3 Hello main 4 */
可以看到主线程和协程同时工作,流程图如下:
- 一旦通过go命令相当于开启一个协程,和主线程是同步进行的,相当于主线程中开启另一个分支
- 如果主线程结束,其它协程即使还没有执行完毕,也会结束
- 协程也可以在主线程结束之前先完成自己的任务
- 线程的开启是直接作用与物理级别的cpu,非常耗费资源,而协程可轻松驾驭百万级别
二、同步
(一)WaitGroup
上述的实例中出现一个明显的问题,就是printGoroutine应该打印5次,但是显然少了1次,这时为什么呢?其实在图中很明显的看出主协程中运行的main结束,那么其余的协程都会终止掉,所以如何在所有的协程结束后再终止主协程就是很重要的事情。
sync.WaitGroup可以解决这个问题,声明:
var wg sync.WaitGroup
之后就可以正常使用wg变量了,该类型有3个指针方法,Add、Done、Wait
sync.WaitGroup是一个结构体类型,内部有一个计数字段,当sync.WaitGroup类型的变量被声明后,该字段的值时0,可以通过Add方法增大或者减少其中的计数值。而该值就是启动的协程数量。
Wait方法用于阻塞调用它的goroutine,直到计数值为0才被唤醒。
package main import ( "fmt" "sync" "time" ) var wg sync.WaitGroup func printGoroutine() { // 协程执行函数 defer wg.Done() // 每执行完1次该协程,Add中的参数减1 for i := 0; i < 5; i++ { time.Sleep(time.Second) fmt.Printf("Hello goroutine %v \n", i) } } func main() { // 主协程中运行main函数 // 开启一个协程 wg.Add(1) // Add中的参数是开启的协程数量 go printGoroutine() // 继续执行主协程代码 for i := 0; i < 5; i++ { time.Sleep(time.Second) fmt.Printf("Hello main %v \n", i) } wg.Wait() // 阻塞,直到Add中的参数变为0结束该主协程 } /* 输出: Hello main 0 Hello goroutine 0 Hello goroutine 1 Hello main 1 Hello main 2 Hello goroutine 2 Hello goroutine 3 Hello main 3 Hello main 4 Hello goroutine 4 */
可以看到输出结果,多输出1行,主协程会等待所有的协程执行完毕才结束。
(二)锁机制
1、问题引入
进行多协程并发操作时,就会涉及到数据安全问题,比如:
package main import ( "fmt" "sync" ) var num int var wg sync.WaitGroup func sub() { defer wg.Done() for i := 0; i < 100000; i++ { num -= 1 } } func add() { defer wg.Done() for i := 0; i < 100000; i++ { num += 1 } } func main() { wg.Add(2) // 启动两个协程,分别是加1和减1两个函数 go add() go sub() // 此时等待两个协程执行完毕 wg.Wait() // 接着执行主协程的逻辑 fmt.Println(num) }
上面的输出结果按理说应该是0,因为一个函数增加,一个函数减少,但是结果且完全不是预期的结果,每次执行的结果都不一样:
PS D:\go_practice\go_tutorial\day19\MutexDemo01> go run .\main.go -36503 PS D:\go_practice\go_tutorial\day19\MutexDemo01> go run .\main.go -60812 PS D:\go_practice\go_tutorial\day19\MutexDemo01> go run .\main.go -47786 PS D:\go_practice\go_tutorial\day19\MutexDemo01> go run .\main.go 89516 PS D:\go_practice\go_tutorial\day19\MutexDemo01> go run .\main.go -97086
那么这时为什么呢?这就是因为多协程产生资源竞争的问题:
因为两个对num的操作协程是同时进行的,所以两个协程可能拿到的数字都是0,这时add函数对其进行加法操作,假如加到6000,而减法也在不断的对num操作,获取的num可能就是6000;相反add的num也有可能获取的正是sub函数的值就是负数。所以num的值是不可掌控的。 那么如何解决这个问题呢?
在Go中可以使用锁,锁分为:
- 互斥锁
- 读写锁
2、互斥锁
互斥锁在并发程序中对共享资源进行访问控制,是成对出现的,即Lock和Unlock。只有当一个协程获取到锁时它才能执行后面的代码块。
package main import ( "fmt" "sync" ) var num int var wg sync.WaitGroup var lock sync.Mutex // 声明一个互斥锁变量 func sub() { defer wg.Done() for i := 0; i < 100000; i++ { lock.Lock() // 加锁,当前只有该协程执行 num -= 1 lock.Unlock() // 解锁 } } func add() { defer wg.Done() for i := 0; i < 100000; i++ { lock.Lock() // 读数据时加锁,因为此时可能还会出现资源竞争 num += 1 lock.Unlock() } } func main() { wg.Add(2) // 启动两个协程,分别是加1和减1两个函数 go add() go sub() // 此时等待两个协程执行完毕 wg.Wait() // 接着执行主协程的逻辑 fmt.Println(num) }
在之前的代码中假如互斥锁即可解决问题,那么为什么在读的地方也要加上互斥锁呢?因为还有可能出现资源竞争。互斥锁的语法:
... var lock sync.Mutex // 声明一个互斥锁变量 func main() { ... lock.Lock() // 加锁,当前只有该协程执行 ... 代码块 ... lock.Unlock() // 解锁 }
互斥锁针对于上述都是写操作,可以很安全的解决这个问题,但是如果是读写混合的话,它的力度比较大,容易影响程序的性能。对于读操作并不会对数据产生更改的影响,所以完全可以多人同时并发的读,而上述的互斥锁同一时刻只能一个协程去读或者去写。所以这种问题可以使用读写锁来解决。那么下面使用一个例子来演示读写锁,一边读数据,一边写数据。
2、读写锁
读写锁是针对于读写操作的互斥锁,它与普通互斥锁的不同在于它可以分别针对读操作于写操作进行加锁于解锁。它允许任意个读操作同时进行,但是在同一时刻它只允许有一个写操作在进行,并且在写操作被进行的过程中,读操作也是不被允许的。也就是说.读写锁中多个写操作之间是互斥的,并且写操作与读操作之间也是互斥的-但是.多个读操作之间却不存在互斥关系。这样可以大大改善程序的性能。
使用语法:
var rwlock sync.RWMutex // 声明一个读写锁变量 func read() { rwlock.RLock() .... ... rwlock.RUnlock() } func write() { rwlock.Lock() .... ... rwlock.Unlock() }
下面是读写锁的一个实例:
package main import ( "fmt" "sync" "time" ) var mapNum = make(map[int]int, 50) var wg sync.WaitGroup var rwlock sync.RWMutex // 声明一个读写锁变量 func write() { defer wg.Done() rwlock.Lock() fmt.Printf("写入数据\n") time.Sleep(time.Second * 2) fmt.Printf("写入结束\n") rwlock.Unlock() } func read() { defer wg.Done() rwlock.RLock() fmt.Printf("读取数据\n") time.Sleep(time.Second) fmt.Printf("读取结束\n") rwlock.RUnlock() } func main() { // 启动10个读,10个写的协程 wg.Add(22) for i := 0; i < 20; i++ { go write() } for i := 0; i < 2; i++ { go read() } // 此时等待20个协程执行完毕 wg.Wait() } /* 写入数据 写入结束 读取数据 读取数据 读取结束 读取结束 写入数据 写入结束 写入数据 写入结束 写入数据 写入结束 写入数据 */
从输出中可以看到写数据时先写入再结束,而读取数据可同时多个并发。