阅读本文前,建议已掌握基本C语言语法及规范,以及充分了解Go语言基础语法,本文将介绍Go语言进阶使用方法
本文主要参考《Go语言圣经(中文版)》及第五届字节跳动青训营系列课程,部分图片来自于青训营课程,具体可参考: Go语言圣经
CSP (Communicating Sequential Processes)
Go语言使用通信共享内存 (而不是共享内存通信)
协程(Goroutine):运行在线程之上,相当于讲线程分块分时,当一个协程执行完成后,可以选择主动让出,让另一个协程运行在当前线程之上。协程并没有增加线程数量,只是在线程的基础之上通过分时复用的方式运行多个协程,而且协程的切换在用户态完成,切换的代价比线程从用户态到内核态的代价小很多。
总结:不要通过共享内存来通信,要通过通信来共享内存
Go语言协程间通信(在同一个进程内)的优势在于和线程间通信的比较
线程间通信方式:
- 分配共享变量(共享内存),多线程读取或修改
- 队列(本质上也是共享内存)
- 线程同步机制(控制线程之间的运行:如同步锁、信号量、条件变量)
数据的交换主要还是通过共享内存(共享变量或者队列)来实现,为了保证数据的安全和正确性,共享内存就必需要加锁等线程同步机制。而线程同步使用起来特别麻烦,容易造成死锁,且过多的锁会造成线程的阻塞以及这个过程中上下文切换带来的额外开销。
对比Go通过channel共享内存:使用通道channel
GO语言中,要传递某个数据给另一个goroutine(协程),可以把这个数据封装成一个对象,然后把这个对象的指针传入某个channel中,另外一个goroutine从这个channel中读出这个指针,并处理其指向的内存对象。
GO从语言层面保证同一个时间只有一个goroutine能够访问channel里面的数据,为开发者提供了一种优雅简单的工具,所以GO的做法就是使用channel来通信,通过通信来传递内存数据,使得内存数据在不同的goroutine中传递,而不是使用共享内存来通信。
即不需要共享内存加锁机制(防止deadlock),使用通道通信更加方便。
Channel
make(chan 元素类型,[缓冲大小]) //不写缓冲大小即为无缓冲通道
无缓冲通道:同步通道,进来就出去 -> 几乎没有暂存功能
有缓冲通道:通道容量表示可以存放的元素个数,队列结构(生产消费模型:队列中入队为生产,出队使用为消费)-> 有暂存功能,存放能力由通道容量决定
例:
AB为生产,M为消费
通道src用于生产者之间的信息通信,由于AB分别使用go关键字成为并行执行程序,src可以使用无缓冲通道使得AB可以立刻进行数据交流
通道dest用于生产者和消费者的信息通信,存放B协程的的运行结果。有缓冲通道可以减小生产/消费者之间运行时间差异造成的影响
defer close(通道) 作用:延迟关闭,在当前函数运行结束后从下往上寻找defer close语句执行。
并发安全 Lock加锁
隶属Sync包
主要解决的问题:多个并发程序/函数同时访问临界区(共享内存)
加锁:
lock.lock() //加锁,获取单独使用权限
/*
操作...
*/
lock.unlock() //释放权限
WithoutLock中结果不等于预期,即产生并发安全问题。有一定概率引起错误出现。
WaitGroup(任务编排等待)
隶属Sync包
使用场景:有一个主任务在执行,执行到某一点时需要并行执行三个子任务,并且需要等到三个子任务都执行完后,再继续执行主任务。那我们就需要设置一个检查点,使主任务阻塞在这,等三个子任务执行完后再放行。
阻塞方式:① time.Sleep() ② 使用waitgroup
三个操作:
-
Add(开启协程个数 int) //子任务开始,计数器+个数 -
Done() //子任务结束,计数器-1 -
Wait() //阻塞主任务直到计数器为0
计数器:用于计算当前子协程个数,如果为0则放行(所有并发任务已经完成)
var wg sync.WaitGroup //创建wg对象
wq.Add(5) //下面将有5个子协程,预先计数器+5
for ...{
go func(){
defer wg.Done() //当前并行函数执行完毕后执行Done操作
//具体操作...
}
}
wg.wait() //检测计数器结束状态