返回介绍

6.4 锁住共享资源

发布于 2024-10-11 12:39:00 字数 5038 浏览 0 评论 0 收藏 0

Go 语言提供了传统的同步 goroutine 的机制,就是对共享资源加锁。如果需要顺序访问一个整型变量或者一段代码, atomicsync 包里的函数提供了很好的解决方案。下面我们了解一下 atomic 包里的几个函数以及 sync 包里的 mutex 类型。

6.4.1 原子函数

原子函数能够以很底层的加锁机制来同步访问整型变量和指针。我们可以用原子函数来修正代码清单 6-9 中创建的竞争状态,如代码清单 6-13 所示。

代码清单 6-13 listing13.go

01 // 这个示例程序展示如何使用 atomic 包来提供
02 // 对数值类型的安全访问
03 package main
04
05 import (
06   "fmt"
07   "runtime"
08   "sync"
09   "sync/atomic"
10 )
11
12 var (
13   // counter 是所有 goroutine 都要增加其值的变量
14   counter int64
15
16   // wg 用来等待程序结束
17   wg sync.WaitGroup
18 )
19
20 // main 是所有 Go 程序的入口
21 func main() {
22   // 计数加 2,表示要等待两个 goroutine
23   wg.Add(2)
24
25   // 创建两个 goroutine
26   go incCounter(1)
27   go incCounter(2)
28
29   // 等待 goroutine 结束
30   wg.Wait()
31
32   // 显示最终的值
33   fmt.Println("Final Counter:", counter)
34 }
35
36 // incCounter 增加包里 counter 变量的值
37 func incCounter(id int) {
38   // 在函数退出时调用 Done 来通知 main 函数工作已经完成
39   defer wg.Done()
40
41   for count := 0; count < 2; count++ {
42     // 安全地对 counter 加 1
43     atomic.AddInt64(&counter, 1)
44
45     // 当前 goroutine 从线程退出,并放回到队列
46     runtime.Gosched()
47   }
48 }

对应的输出如代码清单 6-14 所示。

代码清单 6-14 listing13.go 的输出

Final Counter: 4

现在,程序的第 43 行使用了 atmoic 包的 AddInt64 函数。这个函数会同步整型值的加法,方法是强制同一时刻只能有一个 goroutine 运行并完成这个加法操作。当 goroutine 试图去调用任何原子函数时,这些 goroutine 都会自动根据所引用的变量做同步处理。现在我们得到了正确的值 4。

另外两个有用的原子函数是 LoadInt64StoreInt64 。这两个函数提供了一种安全地读和写一个整型值的方式。代码清单 6-15 中的示例程序使用 LoadInt64StoreInt64 来创建一个同步标志,这个标志可以向程序里多个 goroutine 通知某个特殊状态。

代码清单 6-15 listing15.go

01 // 这个示例程序展示如何使用 atomic 包里的
02 // Store 和 Load 类函数来提供对数值类型
03 // 的安全访问
04 package main
05
06 import (
07   "fmt"
08   "sync"
09   "sync/atomic"
10   "time"
11 )
12
13 var (
14   // shutdown 是通知正在执行的 goroutine 停止工作的标志
15   shutdown int64
16
17   // wg 用来等待程序结束
18   wg sync.WaitGroup
19 )
20
21 // main 是所有 Go 程序的入口
22 func main() {
23   // 计数加 2,表示要等待两个 goroutine
24   wg.Add(2)
25
26   // 创建两个 goroutine
27   go doWork("A")
28   go doWork("B")
29
30   // 给定 goroutine 执行的时间
31   time.Sleep(1 * time.Second)
32
33   // 该停止工作了,安全地设置 shutdown 标志
34   fmt.Println("Shutdown Now")
35   atomic.StoreInt64(&shutdown, 1)
36
37   // 等待 goroutine 结束
38   wg.Wait()
39 }
40
41 // doWork 用来模拟执行工作的 goroutine, 
42 // 检测之前的 shutdown 标志来决定是否提前终止
43 func doWork(name string) {
44   // 在函数退出时调用 Done 来通知 main 函数工作已经完成
45   defer wg.Done()
46
47   for {
48     fmt.Printf("Doing %s Work\n", name)
49     time.Sleep(250 * time.Millisecond)
50
51     // 要停止工作了吗?
52     if atomic.LoadInt64(&shutdown) == 1 {
53       fmt.Printf("Shutting %s Down\n", name)
54       break
55     }
56   }
57 }

在这个例子中,启动了两个 goroutine,并完成一些工作。在各自循环的每次迭代之后,在第 52 行中 goroutine 会使用 LoadInt64 来检查 shutdown 变量的值。这个函数会安全地返回 shutdown 变量的一个副本。如果这个副本的值为 1,goroutine 就会跳出循环并终止。

在第 35 行中, main 函数使用 StoreInt64 函数来安全地修改 shutdown 变量的值。如果哪个 doWork goroutine 试图在 main 函数调用 StoreInt64 的同时调用 LoadInt64 函数,那么原子函数会将这些调用互相同步,保证这些操作都是安全的,不会进入竞争状态。

6.4.2 互斥锁

另一种同步访问共享资源的方式是使用互斥锁( mutex )。互斥锁这个名字来自互斥(mutual exclusion)的概念。互斥锁用于在代码上创建一个临界区,保证同一时间只有一个 goroutine 可以执行这个临界区代码。我们还可以用互斥锁来修正代码清单 6-9 中创建的竞争状态,如代码清单 6-16 所示。

代码清单 6-16 listing16.go

01 // 这个示例程序展示如何使用互斥锁来
02 // 定义一段需要同步访问的代码临界区
03 // 资源的同步访问
04 package main
05
06 import (
07   "fmt"
08   "runtime"
09   "sync"
10 )
11
12 var (
13   // counter 是所有 goroutine 都要增加其值的变量
14   counter int
15
16   // wg 用来等待程序结束
17   wg sync.WaitGroup
18
19   // mutex 用来定义一段代码临界区
20   mutex sync.Mutex
21 )
22
23 // main 是所有 Go 程序的入口
24 func main() {
25   // 计数加 2,表示要等待两个 goroutine
26   wg.Add(2)
27
28   // 创建两个 goroutine
29   go incCounter(1)
30   go incCounter(2)
31
32   // 等待 goroutine 结束
33   wg.Wait()
34   fmt.Printf("Final Counter: %d\\n", counter)
35 }
36
37 // incCounter 使用互斥锁来同步并保证安全访问,
38 // 增加包里 counter 变量的值
39 func incCounter(id int) {
40   // 在函数退出时调用 Done 来通知 main 函数工作已经完成
41   defer wg.Done()
42
43   for count := 0; count < 2; count++ {
44     // 同一时刻只允许一个 goroutine 进入
45     // 这个临界区
46     mutex.Lock()
47     {
48       // 捕获 counter 的值
49       value := counter
50
51       // 当前 goroutine 从线程退出,并放回到队列
52       runtime.Gosched()
53
54       // 增加本地 value 变量的值
55       value++
56
57       // 将该值保存回 counter
58       counter = value
59     }
60     mutex.Unlock()
61     // 释放锁,允许其他正在等待的 goroutine
62     // 进入临界区
63   }
64 }

counter 变量的操作在第 46 行和第 60 行的 Lock()Unlock() 函数调用定义的临界区里被保护起来。使用大括号只是为了让临界区看起来更清晰,并不是必需的。同一时刻只有一个 goroutine 可以进入临界区。之后,直到调用 Unlock() 函数之后,其他 goroutine 才能进入临界区。当第 52 行强制将当前 goroutine 退出当前线程后,调度器会再次分配这个 goroutine 继续运行。当程序结束时,我们得到正确的值 4 ,竞争状态不再存在。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文