Go并发编程 | 青训营笔记

84 阅读7分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 16 天

课程内容与选题缘由

做结营大项目的时候,在思考优化的地方的时候,涉及到并发编程相关的内容。因此趁着这个复习的机会,搭配之前掘金课的go并发课程产出一篇笔记。

并发编程是什么

并发编程是指同时执行多个任务的编程模式。

通过并发编程,系统可以充分地利用多核 CPU 资源,提高程序的运行效率和吞吐量。

为什么Golang适合并发编程

Golang这门语言内置了一套轻量级的协程(goroutine)机制,可以让程序在同一线程内同时执行多个协程,从而实现并发编程。

与传统的线程相比,协程的开销更小,程序可以在同一线程内高效地切换执行任务,避免了线程切换的开销。因此Golang这门语言特别适合并发编程的开发需求。

除此之外,Golang还有如下的优势支持其用于并发编程的开发:

  • 协程间使用 Channel 通信
  • 协程的调度使用 GMP调度方式
  • 提供并发安全锁

1.png

协程间的通信

CSP( Communicating Sequential Processes)

Go语言中,协程间的通信是 通过共享内存实现通信 的,这种通信模式叫作 CSP通信模式

CSP通信模式可以提高并发程序的灵活性和可维护性,不同的协程可以通过不同的Channel进行通信,从而实现更加复杂的协程间通讯方式。另外,由于这是一种基于消息传递的通讯方式,可以充分利用CPU缓存,提高程序的性能。

这种通信模式在Go语言中的主要通过Channel通道实现。

2.png

Channel

Channel是Go语言中实现CSP模型的核心机制,不同协程之间可以通过channel进行信息传递。

Channel有两种类型,带缓存和不带缓存,不带缓存的Channel需要发送和接收同时准备好才能执行,否则会阻塞等待;而带缓存的Channel可以存储一定数量的消息,当缓存区满时会阻塞发送操作,当缓存区空时会阻塞接收操作。

由于Channel是一种内置类型,它使用了与操作系统无关的用户空间内存,因此可以在不同的协程之间快速传递消息。在这个过程中,数据的复制和传递是在用户空间中完成的,不需要进行用户态内核态之间的上下文切换,这样可以充分利用CPU的缓存,减少Cache miss,提高程序的性能。

3.png

4.png

在传统的并发编程模型中,进程线程之间的通信通常需要使用系统调用实现,这会涉及到用户态内核态之间的切换,因此会带来一定的开销和延迟。

而Go语言中使用的Channel机制是在用户态下实现的,数据的读写和传递都是在用户态下完成的,不需要涉及到用户态和内核态之间的切换,因此可以充分利用CPU的缓存,提高程序的性能。

这是因为,Channel是在Go语言运行时系统中实现的,Go语言运行时系统通过协程调度器来管理和调度协程,使得协程之间的通信可以在用户态下完成,而不需要涉及到内核态。

但是,Go语言中的协程通信机制(包括Channel)只适用于进程内的通信,也就是说,它只能在同一进程中的协程之间传递消息,而不能在不同进程之间传递消息。如果需要在不同进程之间进行通信,可以使用其他如Socket、RPC等机制。

在Go语言中,协程之间的通信通常使用Channel来实现,这可以避免上下文切换,并且非常方便、安全、高效。使用Channel时,每个协程都是通过Channel进行数据的读写和传递的,不需要显式地进行同步和互斥操作,从而避免了并发编程中常见的死锁、竞争等问题。此外,Channel还支持多路复用、非阻塞等特性,可以方便地实现更加复杂的协程通信模式。

协程的调度

GMP调度

5.png

6.png Go语言的协程机制使用的是M:N调度模型,即M个协程运行在N个线程上。

GMP 调度的主要思想是将协程和系统线程的关系解耦,使得系统线程可以高效地执行协程的调度。在 GMP 调度中,G 表示 Goroutine(协程) ,M 表示 Machine(机器) ,P 表示 Processor(处理器) ,它们的关系如下:

  • G:协程,即需要执行的任务。
  • M:机器,即系统线程,用来执行协程。
  • P:处理器,用来管理协程队列和执行调度任务,每个 M 上可以有多个 P。

在 GMP 调度中,每个 M 会对应一个 P,而一个 P 上拥有一个可以有多个协程的本地队列。当一个协程需要执行时,它会被放到某个 P 上的协程队列中。当该 P 空闲时,它会从自己的协程队列中取出一个协程执行,并在执行过程中进行协程调度,包括协程的暂停、恢复、阻塞、唤醒等操作。

7.png

GMP 调度的优点是在协程调度上具有高效性和可扩展性,因为每个 P 可以维护自己的协程队列,从而减少了竞争和锁的开销。同时,由于 GMP 调度的实现是在用户空间进行的,因此避免了频繁地进行系统调用,提高了性能和吞吐量。

Go语言运行时调度器就是 GMP调度算法的具体实现,线程P是运行时调度器的工作单元,每个线程上都有一个。Go语言中的每个进程都包含了一个或多个操作系统线程,但是操作系统线程并不直接参与Goroutine的调度和执行。相反,Goroutine由Go语言运行时调度器进行管理并分配到不同的线程上运行,以充分利用CPU资源,实现高效的并发执行。

并发锁

尽管在Go语言中使用Channel进行协程通信是安全、高效的,但并不是所有的并发场景都适合使用Channel来实现协程之间的通信。一些特殊的场景需要也需要使用共享内存来实现协程之间的数据共享和同步,此时并发锁就非常有用了。

并发锁是一种用于保护共享资源的同步机制,可以避免多个协程同时访问同一块共享内存 而引发的数据竞争和死锁等问题。在Go语言中,有两种类型的锁:读写锁和互斥锁。

读写锁允许多个协程同时读取同一块共享内存,但只允许一个协程写入共享内存。这样可以避免多个协程同时写入同一块共享内存而引发的竞争问题,提高程序的性能。

互斥锁则是一种排它锁,只允许一个协程访问共享内存,其他协程需要等待该协程释放锁后才能访问共享内存。这样可以保证同一时刻只有一个协程可以访问共享内存,避免数据的竞争和破坏。

8.png