6 min read

并发

什么是并发(concurrency)

将程序分成多个可独立执行的部分的结构化程序的设计方法,就是并发设计。采用了并发设计的应用也可以看成是一组独立执行的模块的组合

并发不是并行,并发关乎结构,并行关乎执行

并发考虑的是如何将应用划分为多个相互配合的、可独立执行的模块的问题。采用并发设计的程序并不一定是并行执行的,在不满足并行必要条件的情况下(也就是仅有一个单核CPU的情况下),即便是采用并发设计的程序,依旧不可以并行执行

go并发方案:goroutine

Go并没有使用操作系统线程作为承载分解后的代码片段(模块)的基本执行单元,而是实现了goroutine这一由Go运行时(runtime)负责调度的、轻量的用户级线程,为并发程序设计提供原生支持

goroutine的优势

  • 资源占用小,每个goroutine的初始栈大小仅为2k
  • 由go运行时而不是操作系统调度,goroutine上下文切换在用户层实现,开销更小
  • 在语言层面而不是通过标准库提供。goroutine由go关键字创建,一退出就会被回收或销毁,开发体验更佳
  • 语言内置channel作为goroutine间通信原语,为并发设计提供了强大的支撑

goroutine的基本用法

并发是一种能力,它能让你的程序可以由若干个代码片段组合而成,并且每个片段都是独立运行的

Go语言通过go关键字+方法/函数的方式创建一个goroutine。创建后,新goroutine将拥有独立的代码执行流,并与创建它的goroutine一起被Go运行时调度

go fmt.Println("I am a goroutine")
 
var ch = make(chan int)
go func(a, b int) {
  ch <- a + b
}(3, 4)
 
//
c := srv.newConn(rw)
go c.serve(connCtx)

和线程一样,一个应用内部启动的所有goroutine共享进程空间的资源,如果多个goroutine访问同一块内存数据,将会存在竞争,我们需要进行goroutine间的同步

goroutine的执行函数的返回,意味着goroutine退出

goroutine执行函数或方法即便有返回值,go也会忽略这些返回值。所以如果你想要获取goroutine执行后的返回值,需要通过goroutine间的通信来实现

goroutine间的通信

传统编程语言(C++、Java、Python)并非面向并发而生的。他们面对并发的逻辑多是基于操作系统的线程。并发的执行单元(线程)之间的通信,利用的也是操作系统提供的线程或进程间通信的原语:共享内存、信号、管道、消息队列、套接字等

这些通信原语中,使用最多最广泛的是结合了线程同步原语(比如:锁以及更为低级的原子操作)的共享内存方式,因此,传统语言的并发模式是基于内存的共享的

基于传统的共享内存并发模式很难用且易错,在大型项目和复杂程序中,需要根据线程模型对程序进行建模,同时规划线程之间的通信方式。如果选择的高效的基于共享内存的机制,要花大量时间心思设计程序间的同步机制,并且在设计同步机制的时候,还要考虑线程之间复杂的内存管理,以及如何防止死锁等

Go语言,在新并发模式设计中借鉴了著名计算机科学家 Tony Hoare 提出的CSP(Communicationing Sequential Processes, 通信书序进程)并发模型 该模型旨在简化并发程序的编写,昂并发程序的编写和编写顺序程序一样简单。输入输出应该是基本的编程原语,数据处理逻辑(也就是CSP中的P)只需要调用输入原语获取数据