解决并发方法
当通过多个协程去操作同一个内存空间时,此时如果不加以处理的话,会出现并发操作同一空间的错误信息,我们应实现这些协程之间进行同步执行,一个一个地去操作这些空间;
var resMap = make(map[int]int)
// 计算阶乘
func calc(a int) {
var tem int = 1
for i := 1; i <= a; i++ {
tem *= i
}
resMap[a] = tem
}
func main() {
for i := 1; i < 10; i++ {
go calc(i)
}
fmt.Println(resMap)
}
不出意外的话,会报下面的错:
-
互斥锁
- 在操作系统中,在解决多个进程互斥问题中,通过加锁的方式实现,如下图:
- 摘自王道考研笔记
golang中,通过使用Mutex互斥锁来控制同一时刻只需要一个协程被执行,当互斥锁被放开后,才允许后面的协程执行,使用如下:
var (
resMap = make(map[int]int)
lock sync.Mutex // 定义全局变量:互斥锁
)
func calc(a int) {
lock.Lock() // 开启互斥锁,其他协程被阻塞
var tem int = 1
for i := 1; i <= a; i++ {
tem *= i
}
resMap[a] = tem
lock.Unlock() // 关闭锁,其他协程执行
}
func main() {
for i := 1; i < 10; i++ {
go calc(i)
}
time.Sleep(time.Second * 2)
fmt.Println(resMap) // 正常打印
}
弊端
首先这里要声明一个问题,就是所有这里启动的协程都是会基于主线程的执行,如果主线程执行完毕之后,不论开启的协程是否执行完毕,所以的执行都会结束。所以,大家都会注意到一个地方,就是在我们的主函数中一直都有一个sleep函数来阻塞主线程的执行,这就是为了防止这里提到的问题,但是这种方式我之前也讲过只是在写demo的时候使用,同时通过这个demo我们也可以看出,我们无法去获取到开启协程什么时候才结束,只能手动去添加休眠让主线程阻塞,说明通过加互斥锁的方式,虽然可以解决进程互斥的问题,但是没有解决如何获取协程结束的时间。
channel管道
本质上是一个数据结构-队列:FIFO。它是线程安全的,多个协程访问管道不需要加锁。
channel是有类型的,存放的类型必须与声明时候指定的类型一致。
声明方式
var intChan chan int
注意:
在使用管道时,需要先通过make开辟空间,才能正常使用;
使用管道
// TODO: write your codes
var intChan = make(chan int, 1)
intChan <- 10
fmt.Println(intChan, <-intChan)
// output: 0xc000150000 10
注意:
向管道添加数据或者取数据的时候,如果管道已经装满或者管道里面没有数据时,会出现死锁的现象
// TODO: write your codes
var intChan = make(chan int, 1)
intChan <- 10
//intChan <- 12 // fatal error: all goroutines are asleep - deadlock! 出现死锁
fmt.Println(intChan, <-intChan)
<-intChan // fatal error: all goroutines are asleep - deadlock! 出现死锁
遍历管道
// 循环遍历channel
var mapChan = make(chan map[string]string, 5)
map1 := make(map[string]string)
map1["userName"] = "张三"
map1["age"] = "12"
mapChan <- map1
map2 := make(map[string]string)
map2["telNum"] = "123456789"
map2["sex"] = "男"
mapChan <- map2
close(mapChan) // 这里需要关闭管道,不然还是会出现死锁的问题
// 使用内置函数close可以关闭channel,当channel关闭后,就不能再向channel写数据了,但是仍然可以从该channel读取数据,
// 在for range管道时,当遍历到最后的时候,发现由管道没有关闭,程序会认为由可能由数据继续写入,因此就会等待,如果程序没有数据写入,就会出现死锁。
// 方式1: 使用range
for v := range mapChan {
fmt.Println(v)
}
// 方式2:使用for循环
for {
if v, ok := <- mapChan; ok {
// 输出v
}
}
注意:
在遍历管道的时候,我们这里先进行了关闭管道的操作,首先我们要明确的是,关闭管道不会影响我们读取管道数据,也就是我们可以继续读取数据,但是此时是不能再进行向管道里面添加数据;除此之外,我们关闭管道之后,当我们取出所有数据之后,程序就不会再继续去读取管道数据,如果没有关闭管道,程序就会认为还会向管道中插入数据,最终导致出现死锁。
管道和协程结合使用
这里通过一个案例来实现:
- 通过write方法实现对一块空间数据的添加;
- 通过read方法实现对这块空间数据的读取;
- 为了更快的完成这两个任务,我们希望在写入数据的时候,可以进行读取数据,所以就要引申出协程;
分析:
在处理这个案例的时候,我们首先会开启两个协程来处理同步进行的过程,这个都没有什么问题;但是我们之前一直都有一个问题还没有解决,就是主线程和开启的其他协程的执行时长不一致,那么这里我们就通过创建一个管道来解决这个问题。如下图所示,我们创建一个exitChan管道来判断协程是否执行完毕。
代码:
// 写入数据
func write(intChan chan int) {
for i := 0; i < 20; i++ {
intChan <- i
fmt.Printf("写入数据:%v\n", i)
}
// 记得要关闭管道,不然在后面遍历的时候就会 寄~
close(intChan)
}
// 读取数据
func read(intChan chan int, exitChan chan bool) {
for {
if v, ok := <-intChan; ok {
fmt.Printf("读取到数据:%v\n", v)
}
}
exitChan <- true
close(exitChan) // 这里也是同理
}
func main() {
fmt.Println("开始任务")
var intChan = make(chan int, 20)
var exitChan = make(chan bool, 1)
go write(intChan)
go read(intChan, exitChan)
if _, ok := <-exitChan; ok {
fmt.Println("结束任务")
return
}
}
案例
统计2-20000000之间的素数,下面使用了两种方案,对应的耗时如下
start := time.Now().Unix()
primarySlice := make([]int, 0)
// 传统方式
for i := 2; i <= 20000000; i++ {
if isPrimary(i) {
primarySlice = append(primarySlice, i)
}
}
//for _, v := range primarySlice {
// fmt.Printf("%d\v", v)
//}
end := time.Now().Unix()
fmt.Printf("start: %d, end: %d, 最终耗时: %d\n", start, end, end-start)
// 耗时: 42秒
import (
"fmt"
"math"
"runtime"
"time"
)
// 统计2~2000之间的素数
// 判断是否为素数
func isPrimary(n int) bool {
for i := 2; i <= int(math.Sqrt(float64(n))); i++ {
if n%i == 0 {
return false
}
}
return true
}
func add(intChan chan int, num int) {
for i := 2; i <= num; i++ {
intChan <- i
}
close(intChan)
}
func getRes(intChan, resChan chan int, exitChan chan bool) {
for {
v, ok := <-intChan
if !ok {
break
}
if isPrimary(v) {
resChan <- v
}
}
exitChan <- true
// 这里就不用执行关闭管道操作了,因为我们会开启多个协程来处理这些素数的,
//要是这里就关闭了,那么其他协程就不能放内容进exitChan管道了
}
func main() {
// 获取CPU数量
var cNum = runtime.NumCPU()
fmt.Println("当前系统CPU数量: ", cNum)
// 设置程序运行时使用CPU数量
//runtime.GOMAXPROCS(2)
start := time.Now().Unix()
// 创建对应的管道
var intChan = make(chan int, 1000)
var primaryChan = make(chan int, 2000)
// 开启四个协程
var exitChan = make(chan bool, 4)
go add(intChan, 2000)
// 开启四个协程
var count int
for {
if count == 4 {
break
}
count++
fmt.Println("开启协程:", count)
go getRes(intChan, primaryChan, exitChan)
}
// 开启一个协程来
go func() {
for i := 0; i < 4; i++ {
<-exitChan
fmt.Print("读取管道完毕\n")
}
close(primaryChan)
end := time.Now().Unix()
fmt.Printf("start: %d, end: %d, 最终耗时: %d\n", start, end, end-start)
}()
for {
v, ok := <-primaryChan
if !ok {
break
}
fmt.Printf("素数:%d\n", v)
}
}
// 耗时: 18秒
最后
这里总共就介绍了两种解决并发的方案,一种是使用互斥锁,另一种是使用管道,这是go的一大特色,这里也列举了一个比较典型的例子来说明了go通过开启协程来提高执行速度,通过对比发现,这速度提高的非常出色。