基于调度器的内部算法,一个正运行的 goroutine 在工作结束前,可以被停止并重新调度。调度器这样做的目的是防止某个 goroutine 长时间占用逻辑处理器。当 goroutine 占用时间过长时,调度器会停止当前正运行的 goroutine,并给其他可运行的 goroutine 运行的机会。
下图从逻辑处理器的角度展示了这一场景。在第 1 步,调度器开始运行 goroutine A,而goroutine B 在运行队列里等待调度。之后,在第 2 步,调度器交换了 goroutine A 和 goroutine B。由于 goroutine A 并没有完成工作,因此被放回到运行队列。之后,在第 3 步,goroutine B 完成了它的工作并被系统销毁。这也让 goroutine A 继续之前的工作。
如果两个或者多个 goroutine 在没有互相同步的情况下,访问某个共享的资源,并试图同时读和写这个资源,就处于相互竞争的状态,这种情况被称作竞争状态(race candition)。竞争状态的存在是让并发程序变得复杂的地方,十分容易引起潜在问题。对一个共享资源的读和写操作必须是原子化的,换句话说,同一时刻只能有一个 goroutine 对共享资源进行读和写操作。
Go 语言有一个特别的工具,可以在代码里检测竞争状态:
go build -race // 用竞争检测器标志来编译程序
./example // 运行程序
一种修正代码、消除竞争状态的办法是,使用 Go 语言提供的锁机制,来锁住共享资源,从而保证 goroutine 的同步状态。下面介绍两种go语言提供的锁。
原子函数 :
原子函数能够以很底层的加锁机制来同步访问整型变量和指针。
// 这个示例程序展示如何使用 atomic 包来提供
// 对数值类型的安全访问
package main
import (
"fmt"
"runtime"
"sync"
"sync/atomic"
)
var (
// counter 是所有 goroutine 都要增加其值的变量
counter int64
// wg 用来等待程序结束
wg sync.WaitGroup
)
// main 是所有 Go 程序的入口
func main() {
// 计数加 2,表示要等待两个 goroutine
wg.Add(2)
// 创建两个 goroutine
go incCounter(1)
go incCounter(2)
// 等待 goroutine 结束
wg.Wait()
// 显示最终的值
fmt.Println("Final Counter:", counter)
}
// incCounter 增加包里 counter 变量的值
func incCounter(id int) {
// 在函数退出时调用 Done 来通知 main 函数工作已经完成
defer wg.Done()
for count := 0; count < 2; count++ {
// 安全地对 counter 加 1
atomic.AddInt64(&counter, 1)
// 当前 goroutine 从线程退出,并放回到队列
runtime.Gosched()
}
}
程序的第 43 行使用了 atmoic 包的 AddInt64 函数。这个函数会同步整型值的加法,方法是强制同一时刻只能有一个 goroutine 运行并完成这个加法操作。当 goroutine 试图去调用任何原子函数时,这些 goroutine 都会自动根据所引用的变量做同步处理。另外两个有用的原子函数是 LoadInt64 和 StoreInt64。这两个函数提供了一种安全地读和写一个整型值的方式。
互斥锁
另一种同步访问共享资源的方式是使用互斥锁(mutex)。互斥锁这个名字来自互斥(mutual exclusion)的概念。互斥锁用于在代码上创建一个临界区,保证同一时间只有一个 goroutine 可以执行这个临界区代码。
// 定义一段需要同步访问的代码临界区
// 资源的同步访问
package main
import (
"fmt"
"runtime"
"sync"
)
var (
// counter 是所有 goroutine 都要增加其值的变量
counter int
// wg 用来等待程序结束
wg sync.WaitGroup
// mutex 用来定义一段代码临界区
mutex sync.Mutex
)
// main 是所有 Go 程序的入口
func main() {
// 计数加 2,表示要等待两个 goroutine
wg.Add(2)
// 创建两个 goroutine
go incCounter(1)
go incCounter(2)
// 等待 goroutine 结束
wg.Wait()
fmt.Printf("Final Counter: %d\\n", counter)
}
// incCounter 使用互斥锁来同步并保证安全访问,
// 增加包里 counter 变量的值
func incCounter(id int) {
// 在函数退出时调用 Done 来通知 main 函数工作已经完成
defer wg.Done()
for count := 0; count < 2; count++ {
// 同一时刻只允许一个 goroutine 进入
// 这个临界区
mutex.Lock()
{
// 捕获 counter 的值
value := counter
// 当前 goroutine 从线程退出,并放回到队列
runtime.Gosched()
// 增加本地 value 变量的值
value++
// 将该值保存回 counter
counter = value
}
mutex.Unlock()
// 释放锁,允许其他正在等待的 goroutine
// 进入临界区
}
}
对 counter 变量的操作在第 46 行和第 60 行的 Lock()和 Unlock()函数调用定义的临界区里被保护起来。使用大括号只是为了让临界区看起来更清晰,并不是必需的。同一时刻只有一个 goroutine 可以进入临界区。之后,直到调用 Unlock()函数之后,其他 goroutine 才能进入临界区。当第 52 行强制将当前 goroutine 退出当前线程后,调度器会再次分配这个 goroutine 继续运行。
本文内容来自《go语言实战》