Go 编程实例【互斥锁 Mutexes】

基础实例

在前面的例子中,我们看到了如何使用原子操作来管理简单的计数器。

对于更加复杂的情况,我们可以使用一个互斥锁来在 Go 协程间安全的访问数据。

// mutexes.go
package main

import (
	"fmt"
	"math/rand"
	"runtime"
	"sync"
	"sync/atomic"
	"time"
)

func main() {
    
    

	// 在我们的例子中,`state` 是一个 map。
	var state = make(map[int]int)

	// 这里的 `mutex` 将同步对 `state` 的访问。
	var mutex = &sync.Mutex{
    
    }

	// 为了比较基于互斥锁的处理方式和我们后面将要看到的其他
	// 方式,`ops` 将记录我们对 state 的操作次数。
	var ops int64 = 0

	// 这里我们运行 100 个 Go 协程来重复读取 state。
	for r := 0; r < 100; r++ {
    
    
		go func() {
    
    
			total := 0
			for {
    
    

				// 每次循环读取,我们使用一个键来进行访问,
				// `Lock()` 这个 `mutex` 来确保对 `state` 的
				// 独占访问,读取选定的键的值,`Unlock()` 这个
				// mutex,并且 `ops` 值加 1。
				key := rand.Intn(5)
				mutex.Lock()
				total += state[key]
				mutex.Unlock()
				atomic.AddInt64(&ops, 1)

				// 为了确保这个 Go 协程不会在调度中饿死,我们
				// 在每次操作后明确的使用 `runtime.Gosched()`
				// 进行释放。这个释放一般是自动处理的,像例如
				// 每个通道操作后或者 `time.Sleep` 的阻塞调用后
				// 相似,但是在这个例子中我们需要手动的处理。
				runtime.Gosched()
			}
		}()
	}

	// 同样的,我们运行 10 个 Go 协程来模拟写入操作,使用
	// 和读取相同的模式。
	for w := 0; w < 10; w++ {
    
    
		go func() {
    
    
			for {
    
    
				key := rand.Intn(5)
				val := rand.Intn(100)
				mutex.Lock()
				state[key] = val
				mutex.Unlock()
				atomic.AddInt64(&ops, 1)
				runtime.Gosched()
			}
		}()
	}

	// 让这 10 个 Go 协程对 `state` 和 `mutex` 的操作
	// 运行 1 s。
	time.Sleep(time.Second)

	// 获取并输出最终的操作计数。
	opsFinal := atomic.LoadInt64(&ops)
	fmt.Println("ops:", opsFinal)

	// 对 `state` 使用一个最终的锁,显示它是如何结束的。
	mutex.Lock()
	fmt.Println("state:", state)
	mutex.Unlock()
}

运行这个程序,显示我们对已进行了同步的 state 执行了 3,500,000 次操作。

[root@bogon test]# go run mutexes.go
ops: 3789768
state: map[0:36 1:58 2:93 3:56 4:37]
[root@bogon test]# 

knowledge

runtime.Gosched() 什么意思?

runtime.Gosched() 是 Go 语言中的一个函数,它的作用是让出当前 goroutine 的执行权,使其他 goroutine 有机会运行。

在 Go 语言中,多个 goroutine 可以并发运行,但是它们的执行时间是无法预测的。当某个 goroutine 执行了长时间的计算或者阻塞操作时,其他 goroutine 就可能会被长时间地挂起,无法得到执行。为了避免这种情况,我们可以在适当的时候主动让出 goroutine 的执行权,让其他 goroutine 有机会运行。

runtime.Gosched() 函数就是实现这个目的的函数之一。

当调用 runtime.Gosched() 函数时,当前 goroutine 会被挂起,让其他 goroutine 有机会执行。注意,runtime.Gosched() 函数只是让出当前 goroutine 的执行权,并不能保证其他 goroutine 一定会得到执行。具体来说,当调用 runtime.Gosched() 函数时,调度器会重新调度所有可运行的 goroutine,并根据一定的策略选择一个 goroutine 进行执行。

需要注意的是,runtime.Gosched() 函数不会阻塞当前 goroutine 的执行,它只会让出当前 goroutine 的执行权。因此,在某些情况下,我们需要在调用 runtime.Gosched() 函数之前加上一些适当的等待操作,以确保当前 goroutine 已经完成了一定的工作。

例如:

package main

import (
	"fmt"
	"runtime"
	"time"
)

func main() {
    
    
	go func() {
    
    
		for i := 0; i < 10; i++ {
    
    
			fmt.Println("goroutine 1:", i)
			time.Sleep(time.Millisecond * 100)
			// 让出当前 goroutine 的执行权
			runtime.Gosched()
		}
	}()
	go func() {
    
    
		for i := 0; i < 10; i++ {
    
    
			fmt.Println("goroutine 2:", i)
			time.Sleep(time.Millisecond * 100)
			// 让出当前 goroutine 的执行权
			runtime.Gosched()
		}
	}()
	// 等待一段时间,确保两个 goroutine 都能有机会运行
	time.Sleep(time.Second)
}

[root@bogon test]# go run main.go 
goroutine 2: 0
goroutine 1: 0
goroutine 1: 1
goroutine 2: 1
goroutine 2: 2
goroutine 1: 2
goroutine 1: 3
goroutine 2: 3
goroutine 2: 4
goroutine 1: 4
goroutine 1: 5
goroutine 2: 5
goroutine 2: 6
goroutine 1: 6
goroutine 1: 7
goroutine 2: 7
goroutine 2: 8
goroutine 1: 8
goroutine 1: 9
goroutine 2: 9
[root@bogon test]# 

在上述代码中,我们创建了两个 goroutine,它们分别输出一些信息并睡眠一段时间。

在每个 goroutine 中,我们都使用了 runtime.Gosched() 函数让出当前 goroutine 的执行权,以确保两个 goroutine 能够交替地运行。在最后,我们使用 time.Sleep() 函数等待一段时间,以确保两个 goroutine 都能有机会运行。

猜你喜欢

转载自blog.csdn.net/weiguang102/article/details/129750346