并发编程
并发 vs 并行
举个形象点的例子
- 并发可以理解为一边吃饭,一边喝水,因为人只有一个嘴一个咽喉,所以同一时刻饭和水只能有一样进入,二者只能交替进行
- 并行可以理解为一边走路一边吃东西,因为走路是靠腿脚,吃东西是靠嘴,二者不相干,相当于两个独立的线程,因而可以同时进行
Go语言实现了并发性能提高的调度模型,通过高效的调度,可以充分发挥多核优势,高效运行,可以说Go语言就是为并发而生的
Goroutine
Go语言中实现高并发有一个重要概念叫协程
- 线程属于内核态,它的创建、切换、停止都属于很重的系统操作,比较消耗资源,消耗在MB级别
- 协程属于用户态,可以理解为轻量级的线程,协程的创建和切换由Go语言本身去完成,比线程消耗资源要少很多,消耗在KB级别
快速打印goroutine 0~4
这里我们可以通过在调用的函数前加上 go 关键字来开启一个协程来运行,
在主函数最后加上了一个 time.Sleep 函数用来保证子协程运行结束前 主线程不退出
最终输出
可以看到是乱序的,也就是说goroutine 0~4是通过并行进行输出的
CSP(Communicating Sequential Processes)
说完协程,再来说说协程之间的通信
Go语言是提倡通过通信共享内存而不是通过共享内存而实现通信
- 像左图通过channel将协程进行连接,就像是传输队列,遵循先入先出,能保证收发的顺序
- 而像右图通过共享内存实现通信,需要通过互斥量对内存进行加锁,也就是需要获取临界区的权限,这样在一定程度上会影响程序的性能。(基本上只要需要去获取锁,都会多少影响到性能)
所以通过以上两种方式,GO语言为了保证性能,选择了通过通信实现共享内存
Channel
Channel是一种引用类型,它的创建需要使用 make 关键字
Channel又分为无缓冲通道和有缓冲通道
- 无缓冲通道就像是快递员送快递到楼下,打电话叫我们来拿快递,过程是同步进行的,不见不散。但这样快递员必须等我们下楼拿完快递才会去送出下一份快递,等所有人来拿完才能完成工作
- 有缓冲通道可以理解为快递员将快递放到驿站,然后通知我们来拿,这样过程就是异步进行的了,快递员在通知完所有人来拿快递后工作就结束了,至于我们什么时候来拿就影响不到他了,效率明显提升,而这个驿站就相当于是缓冲通道,当然如果缓冲通道也就是驿站满了,快递员还是要等待驿站的快递被取走才能继续向里面添加新的快递
下面我们定义两个协程
A 子协程发生0~9数字
B 子协程计算输入数字的平方
主协程输出最后的平方数
在这里我们定义的两个通道,dest作为传输最终结果的通道采用了有缓冲通道,因为考虑到主协程作为消费者可能消费速度没有那么快,为避免消息阻塞,因而添加了缓冲
输出结果
并发安全 Lock
现在我们进行一个测试,对变量执行2000次+1操作,5个协程并发执行
首先测试不加锁的情况
可以看到结果并不一定正确(可能会正确,但那是偶然)
此时我们加上锁
可以看到,此时结果就都是正确的了
至于为什么不加锁会出现这种问题,这算是一种并发安全问题,可能会出现多个协程读取到同一个x值,然后均对其进行+1操作
例如协程1读取到x此时为50,准备将其进行+1操作,使其变为51,但在其写入51值之前,协程2也读取了x的值为50,也对50进行+1操作,这样在协程1执行完写入x=51的操作后,协程2又重复执行了写入x=51的操作。诸如此类的操作便会导致x最终的值可能会低于预期的结果
WaitGroup
前面我们为了保证在协程执行结束前主协程不退出,都采用了调用time.Sleep函数的方法
但我们并不知道子协程执行所需的一个确切时间,因此就无法精确的设定Sleep的时间
为了解决这个问题,Go语言中提供了WaitGroup
当我们启动了n个协程任务,计数器会加上n,每执行完一个协程,计数器会减1,然后调用Wait函数来阻塞,等待其他协程执行完,当计数器为0则表示所有并发任务执行完成
这里我们再回头看之前快速打印goroutine 0~4的例子,我们就可以用计数器来优化了
那么到这里有关Go语言的并发编程问题就结束了
标签:协程,语言,并发,缓冲,编程,快递,Go,cnblog From: https://www.cnblogs.com/xuanprogram/p/17405874.html