Goroutine
Goroutine 是 Go 语言中的一个重要概念,它是一种轻量级的线程,可以在不创建新的操作系统线程的情况下,实现并发和并行编程。
每个 Goroutine 都是一个独立的执行线程,它们共享相同的内存地址空间,并通过 Go 语言内置的协程(coroutine)机制来实现协作和切换。当一个 Goroutine 执行阻塞操作时(例如 I/O 操作),Go 运行时会自动将其挂起,并切换到其他活跃的 Goroutine 执行。当阻塞操作完成时,被挂起的 Goroutine 会被重新激活,继续执行。
以下我们通过一些代码来展示 Goroutine 的奇妙之处。
go
首先,给出如下一份原始代码。main 函数里有 foo 和 bar 两个函数,都实现了输出 0 到 14 的功能。
package main
import "fmt"
func main() {
foo()
bar()
}
func foo() {
for i := 0; i < 15; i++ {
fmt.Println("Foo:", i)
}
}
func bar() {
for i := 0; i < 15; i++ {
fmt.Println("Bar:", i)
}
}
运行代码,结果如下。
Foo: 0
Foo: 1
Foo: 2
Foo: 3
Foo: 4
……………………
Bar: 10
Bar: 11
Bar: 12
Bar: 13
Bar: 14
Goroutine 的使用呢,很简单,我们在 main函数里的 foo 和 bar 函数前添加 go 关键字即可,这表示我们定义了两个独立的 Goroutine。
func main() {
go foo()
go bar()
}
然而,当我们尝试运行这份修改后的代码时,它却没有任何输出,这是为什么呢?
这是因为在这个例子中,主 Goroutine(也就是 main 函数) 只是启动了两个子 Goroutine(foo 和 bar),然后立即退出了,没有任何代码来等待子 Goroutine 完成,子 Goroutine 也没有与主 Goroutine 进行任何交互,所以程序会立即结束,没有任何输出。
因此,我们决定尝试用 sync 包下的 sync.WaitGroup 来解决这个问题。
sync.WaitGroup
sync.WaitGroup 是 Go 语言中用来等待一组 Goroutine 完成执行的工具,它可以帮助多个 Goroutine 协调工作。sync.WaitGroup 具体来说是一个计数器,通过 Add 方法,它可以记录有多少个 Goroutine 正在等待。当一个 Goroutine 完成了它的工作后,它可以调用 Done 方法来将计数器减 1。同时,主 Goroutine 可以通过调用 Wait 方法来等待所有的 Goroutine 完成,并在所有 Goroutine 完成后继续执行。
我们利用这个新工具,将代码修改如下。
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
wg.Add(2)
go foo()
go bar()
wg.Wait()
}
func foo() {
for i := 0; i < 15; i++ {
fmt.Println("Foo:", i)
}
wg.Done()
}
func bar() {
for i := 0; i < 15; i++ {
fmt.Println("bar:", i)
}
wg.Done()
}
目前,我们虽然解决了程序没有输出的问题,但是同样的一份代码,多次运行的结果却各不相同,这又是为什么呢?
这是因为 Go 语言中的并发是异步的,并且不保证代码的执行顺序。在这份代码中,两个 Goroutine(foo 和 bar)都会循环打印出这 15 个数字。但由于 Goroutine 是异步执行的,所以它们可能会同时执行,也可能会交替执行。另外需要我们注意的一点,由于 Go 语言中的并发是基于抢占式调度器实现的,所以 Goroutine 的执行可能会被其他 Goroutine 抢占,导致它们的执行顺序也可能会有所不同。因此,程序在每一次运行后,最终的输出结果可能会有所不同。
既然如此,为了确保代码的执行结果的一致性,我们尝试使用另外一个同步工具 sync.Mutex。
sync.Mutex
sync.Mutex 是 Go 语言中的一种互斥锁,它可以用于保护共享资源的并发访问。互斥锁是一种同步机制,它确保在同一时刻只有一个 Goroutine 可以访问共享资源。如果一个 Goroutine 想要访问共享资源,它必须先获取互斥锁,然后在访问完资源后释放互斥锁。其他 Goroutine 必须等待,直到互斥锁被释放,才能访问共享资源。
利用sync.Mutex,我们将代码修改如下。
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
var mux sync.Mutex
func main() {
wg.Add(2)
go foo()
go bar()
wg.Wait()
}
func foo() {
mux.Lock()
for i := 0; i < 15; i++ {
fmt.Println("Foo:", i)
}
mux.Unlock()
wg.Done()
}
func bar() {
time.Sleep(3 * time.Millisecond)
for i := 0; i < 15; i++ {
fmt.Println("Bar:", i)
}
wg.Done()
}
注意到代码中,我们还在 bar 函数中添加了一句话 time.Sleep(3 * time.Millisecond)
,这是为了保证先执行 foo 这个子 Goroutine,做到让程序每一次运行的结果真正一致。
Go 语言中的 time.Sleep 函数用于让当前的 Goroutine 暂停指定的时间。在上面的代码中,time.Sleep(3 * time.Millisecond) 表示让当前的 Goroutine 暂停 3 毫秒,然后再继续执行后面的代码。需要注意的是,time.Sleep 函数并不是一个阻塞函数,它只是让当前的 Goroutine 暂停指定的时间,而不会阻塞其他的 Goroutine。
分析这份代码,我们通过 time.Sleep 函数,使得一开始资源即使被 bar 这个 Goroutine 抢占,也会先“睡”上三毫秒,把辛苦抢来的资源拱手让给 foo。而在 foo 函数里,我们通过设置 mux 这个互斥锁,使得 foo 在执行的时候“独享”这份资源。即便三毫秒过后,bar 醒来了,但它也会因为没有拿到 mux 这个互斥锁而必须等待。如此一来,便保证了程序每次运行的结果做到真正一致。