Go 的并发与并行 | 青训营

165 阅读3分钟

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 提倡通过通信共享内存,而不是通过共享内存实现通信。

image.png图片来自录播

我们可以通过创建通道实现协程间通信

  • 无缓冲通道:信息发送与接受是同步的,只有等待发送协程和接收协程就绪时,通信才能完成
  • 有缓冲通道:允许一个协程发送的信息被存储在缓冲队列中,接收协程在需要接收时从队列中读取缓冲信息

通道创建语法: 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定义一个名为lockMutex
  • 锁定: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结合使用,以在协程结束时对计数器递减