go核心04-并发:Go的并发方案实现方案是怎样的?

95 阅读3分钟

这种将程序分成多个可独立执行的部分的结构化程序的设计方法,就是并发设计。

并行(parallelism),指的就是在同一时刻,有两个或两个以上的任务(这里指进程)的代码在处理器上执行。

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

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 c = make(chan int)
go func(a, b int) {
    c <- a + b
}(3,4)
 
// $GOROOT/src/net/http/server.go
c := srv.newConn(rw)
go c.serve(connCtx)

goroutine 的使用代价很低,Go 官方也推荐你多多使用 goroutine。而且,多数情况下,我们不需要考虑对 goroutine 的退出进行控制:goroutine 的执行函数的返回,就意味着 goroutine 退出

goroutine 间的通信

并发的执行单元(线程)之间的通信,利用的也是操作系统提供的线程或进程间通信的原语,比如:共享内存、信号(signal)、管道(pipe)、消息队列、套接字(socket)等。

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

Go 语言从设计伊始,就将解决上面这个传统并发模型的问题作为 Go 的一个目标,并在新并发模型设计中借鉴了著名计算机科学家Tony Hoare提出的 CSP(Communicating Sequential Processes,通信顺序进程)并发模型。

比如我们上面提到的获取 goroutine 的退出状态,就可以使用 channel 原语实现:

func spawn(f func() error) <-chan error {
    c := make(chan error)

    go func() {
        c <- f()
    }()

    return c
}

func main() {
    c := spawn(func() error {
        time.Sleep(2 * time.Second)
        return errors.New("timeout")
    })
    fmt.Println(<-c)
}

这个示例在 main goroutine 与子 goroutine 之间建立了一个元素类型为 error 的 channel,子 goroutine 退出时,会将它执行的函数的错误返回值写入这个 channel,main goroutine 可以通过读取 channel 的值来获取子 goroutine 的退出状态。

虽然 CSP 模型已经成为 Go 语言支持的主流并发模型,但 Go 也支持传统的、基于共享内存的并发模型,并提供了基本的低级别同步原语(主要是 sync 包中的互斥锁、条件变量、读写锁、原子操作等)。

虽然 CSP 模型已经成为 Go 语言支持的主流并发模型,但 Go 也支持传统的、基于共享内存的并发模型,并提供了基本的低级别同步原语(主要是 sync 包中的互斥锁、条件变量、读写锁、原子操作等)。

过,对于局部情况,比如涉及性能敏感的区域或需要保护的结构体数据时,我们可以使用更为高效的低级同步原语(如 mutex),保证 goroutine 对数据的同步访问。

此文章为3月Day22学习笔记,内容来源于极客时间《Tony Bai · Go 语言第一课》。