Go 的并发与并行
概念
- 并发:多个任务执行的过程同时发生,同一时刻可能有一个或多个任务在运行
- 并行:同一时刻有多个任务同时运行
如果CPU只有一个核心,那么每个线程通过轮流占用时间片实现并发;如果CPU有多个核心,那么每个核心都可以执行一个线程,允许多个线程并行实现并发。
协程
- 线程:由操作系统管理,属于内核态,栈MB级别,创建/销毁开销较大
- 协程:用户态概念,用于执行轻量级任务,栈KB级别,创建/销毁开销较小
我们可以创建大量协程,每个协程用于完成一个特定的任务。所有创建的协程会被分配到系统的几个线程上,协程结束时线程不会被销毁,而是用于继续执行其它协程,避免了重复创建线程的开销。
Goroutine
在 Go 中,我们可以通过关键字go创建协程:
go <func_name>(<args>)
<func_name>为一个函数名<args>为传入的参数- 即在普通的函数调用语法前添加
go关键字
或者定义匿名函数:
go func(<arg_list>){
// do something...
}(<args>)
<arg_list>为定于函数时定义的参数列表<args>为传递给函数的参数
协程间通信
Go 提倡通过通信共享内存,而不是通过共享内存实现通信。
我们可以通过创建通道实现协程间通信
- 无缓冲通道:信息发送与接受是同步的,只有等待发送协程和接收协程就绪时,通信才能完成
- 有缓冲通道:允许一个协程发送的信息被存储在缓冲队列中,接收协程在需要接收时从队列中读取缓冲信息
通道创建语法: make(chan <type>, [size])
<type>为需要发送的数据类型[size](可选)为缓冲区大小,即存储类型为<type>的对象个数- 例如:
src := make(chan int)创建一个无缓冲的int通道,名为src。
通道释放: 创建的每个通道在使用结束后必须通过close(<channel_name>)释放,可以结合defer执行。
向通道发送数据: <channel_name> <- <object>
<channel_name>为通道名称<object>为要发送的对象
从通道中读取数据:
- 读取一个对象:
<variable> := <- <channel_name>从通道<channel_name>中读取一个对象到变量<variable> - 使用循环读取所有对象:
for <variable> := range <channel_name>{}每次迭代读取一个对象到变量<variable>
并发安全
对于多个线程/协程访问同一块内存/对象的情况,需要使用锁保证并发安全,即任意时刻只有一个线程允许操作对象。多个线程同时操作一个对象将造成未定义行为。
- 定义锁:
lock sync.Mutex定义一个名为lock的Mutex锁 - 锁定:
lock.Lock() - 解锁:
lock.Unlock()
阻塞
可以使用time.Sleep(<sleep_time>)阻塞当前线程
等待
如果需要等待一组Goroutine完成,我们可以通过sync.WaitGroup定义一个计数器
- 声明计数器
<wg_name>:var <wg_name> sync.WaitGroup - 增加计数:
.Add(n)增加n - 减少计数:
.Done()减少1
然后通过.Done()方法等待计数器到达0,注意:该方法将阻塞当前线程
- 用于在协程中调用
- 可以和
defer结合使用,以在协程结束时对计数器递减