Go 语言并发编程系列教程(二):Go 协程实现原理和使用示例

Go 并发编程原理

Go 语言的协程实现被称之为 goroutine,由 Go 运行时管理,在 Go 语言中通过协程实现并发编程非常简单:我们可以在一个处理进程中通过关键字 go 启用多个协程,然后在不同的协程中完成不同的子任务,这些用户在代码中创建和维护的协程本质上是用户级线程,Go 语言运行时会在底层通过调度器将用户级线程交给操作系统的系统级线程去处理,如果在运行过程中遇到某个 IO 操作而暂停运行,调度器会将用户级线程和系统级线程分离,以便让系统级线程去处理其他用户级线程,而当 IO 操作完成,需要恢复运行,调度器又会调度空闲的系统级线程来处理这个用户级线程,从而达到并发处理多个协程的目的。此外,调度器还会在系统级线程不够用时向操作系统申请创建新的系统级线程,而在系统级线程过多的情况下销毁一些空闲的线程,这个过程和 PHP-FPM 的工作机制有点类似,实际上这也是很多进程/线程池管理器的工作机制,这样一来,可以保证对系统资源的高效利用,避免系统资源的浪费。

以上,就是 Go 语言并发编程的独特实现模型。

协程简单示例

下面通过一个简单的示例来演示如何在 Go 语言中通过协程进行并发编程,我们在 add.go 中编写一个加法函数 add 并通过协程的方式来调用它:

package main

import "fmt"

func add(a, b int) {
    var c = a + b;
    fmt.Printf("%d + %d = %d", a, b, c)
}

func main() {
    go add(1, 2)
}

嗯,就是这么简单,在这段代码中包含了两个协程,一个是显式的,通过 go 关键字声明的这条语句,表示启用一个新的协程来处理加法运算,另一个是隐式的,即 main 函数本身也是运行在一个主协程中,该协程和调用 add 函数的子协程是并发运行的两个协程,就好比从 go 关键字开始,从主协程中叉出一条新路。和之前不使用协程的方式相比,由此也引入了不确定性:我们不知道子协程什么时候执行完毕,运行到了什么状态。在主协程中启动子协程后,程序就退出运行了,这就意味着包含这两个协程的处理进程退出了,所以,我们运行这段代码,不会看到子协程里运行的打印结果,因为还没来得及执行它们,进程就已经退出了。另外,我们也不要试图从 add 函数返回处理结果,因为在主协程中,根本获取不到子协程的返回值,从子协程开始执行起就已经和主协程没有任何关系了,返回值会被丢弃。

如果要显示出子协程的打印结果,一种方式是在主协程中等待足够长的时间再退出,以便保证子协程中的所有代码执行完毕:

package main

import (
    "fmt"
    "time"
)

func add(a, b int) {
    var c = a + b;
    fmt.Printf("%d + %d = %d\n", a, b, c)
}

func main() {
    go add(1, 2)
    time.Sleep(1e9)
}

这里,我们通过 time.Sleep(1e9) 让主协程等待 1s 后退出,这样,运行 go run add.go 就可以看到打印结果了:

不过,这种方式过于简单粗暴,对于加法运算,1s 肯定够了(而且根本不需要这么长时间),但是如果是数据库连接、发送邮件之类的难以预估时间的操作呢?这种方式就不合适了,我们需要一种更精准的方式在子协程执行完毕后,立即退出主协程,这就涉及到协程间的通信,我们将在下一篇教程中重点讨论这一块,并且通过协程间通信来重写这段代码。

并发执行示例

目前为止,我们仅仅演示了 Go 语言协程的启动和简单使用,但是通过上述代码还不足以验证协程是并发执行的,接下来,我们通过下面这段代码来验证协程的并发执行:

package main

import (
    "fmt"
    "time"
)

func add(a, b int) {
    var c = a + b;
    fmt.Printf("%d + %d = %d\n", a, b, c)
}

func main() {
    for i := 0; i < 10; i++ {
        go add(1, i)
    }
    time.Sleep(1e9)
}

很简单,我们给协程代码外层套上一个循环就可以了,这样一来,就同时启动了 10 个子协程,由于它们是并发执行的,执行的先后顺序无法保证,所以我们就看到了这样的打印结果:

如果你还不放心,可以在 add 函数最后添加如下这段代码:

time.Sleep(3e9)

表示每个子协程中执行完打印语句睡眠 3 秒再退出,如果不是并发执行,那么肯定至多只能打印一条结果出来,但实际情况是,仍然是打印 10 条结果,并且没有任何延迟,证明这些加法运算是并发执行的。

学院君 has written 1346 articles

Laravel学院院长,终身学习者

积分:187602 等级:P12 职业:手艺人 城市:杭州

3 条回复

  1. 学院君 学院君 says:
    @ zhaolm

    我也没说一次创建啊 由于 for 循环迭代速度远远快于协程启动速度 所以跟同时启动没什么区别

  2. zhaolm zhaolm says:

    在for循环里面创建的子协程 不是循环一次 创建一个子协程吗
    道理上好像是有先后顺序的哈 不应该是一次创建10个的吧

登录后才能进行评论,立即登录?