Go语言入门—17锁

233 阅读4分钟

「这是我参与2022首次更文挑战的第12天,活动详情查看:2022首次更文挑战」。

在并发编程中,永远离不开的就是多个线程并发操作同一个资源的安全性,在go语言中也一样,如果有多个goroutine并发操作同一资源,就需要加锁控制并发的安全性。

代码示例:

package main

import (
	"fmt"
	"time"
)

var sum = 0

func main() {
	go add()
	go add()
	time.Sleep(time.Second)
	fmt.Println(sum)
}

func add() {
	for i := 0; i < 10000; i++ {
		sum += 1
	}
}

假设有以上代码,在该代码中,定义了一个add函数,在该函数内对全局变量sum进行一万次加 1 ,然后在 main 函数中启动两个 goroutine 调用该函数,则理想结果下是全局变量在每一个 goroutine 中加一万次,两个 goroutine 就一共加了20000次,所以最后的 sum 的结果就应该是20000,但是在上面的代码中最后得到的 sum 结果并不是每一次都是 20000, 这就是在并发编程中出现的访问临界资源(上面代码中的临界资源就是全局变量 sum )的安全性问题。

运行结果:

image-20220130200026866

如果需要保证上面的代码在多个 goroutine 访问临界资源时的安全性,就需要使用到并发编程中的锁。

在 go 语言的并发编程中,用于保证并发安全性常用的锁有互斥锁和读写锁。

互斥锁

互斥锁是一种常用的保证并发安全的锁,使用互斥锁可以保证在同一时间只能有一个 goroutine 能够访问共享资源,在 go 语言中使用 sync 包下的 Mutex 来实现互斥锁。

互斥锁定义:

var lock sync.Mutex

加锁:

lock.Lock()

解锁:

lock.Unlock()

使用互斥锁修改上面代码,使得代码能够正常运行。

package main

import (
	"fmt"
	"sync"
	"time"
)

var sum = 0
var lock sync.Mutex			// 定义互斥锁

func main() {
	go add()
	go add()
	time.Sleep(time.Second)
	fmt.Println(sum)
}

func add() {
	for i := 0; i < 10000; i++ {
		lock.Lock()			// 加锁
		sum += 1
		lock.Unlock()		// 解锁
	}
}

上述代码使用了互斥锁,首先定义一个互斥锁 lock ,然后在 add 方法循环中进行累加之前和之后分别进行加锁和解锁,这样就可以保证在同一时刻永远只会有一个 goroutine 进行累加,其他的 goroutine 等待,当一个 goroutine 执行完并解锁之后另一个 goroutine 才会继续加锁并执行,这样就不会出现同时多个 goroutine 累加导致最后结果与预期不一致的问题。代码经过这样修改之后运行得到的 sum 就永远都是20000。

读写锁

互斥锁是完全互斥的,在实际的开发过程中,可能会出现某个 goroutine 仅仅只是读取共享资源的值,并不会对资源进行修改,这样的话使用互斥锁会降低代码的运行性能,所以这时候就需要读写锁,读写锁就是在读取资源时不会对其加锁,而是在修改资源的时候才会对其加锁。

假设有两个 goroutine ,分别是 A 和 B

  • 如果 A 获取到读写锁的读锁,B 再获取读锁会直接获得,不需要等待 A 解锁。
  • 如果 A 获取到读写锁的读锁,B 再获取写锁就会等待。
  • 如果 A 获取到读写锁的写锁,B 再获取写锁就会等待。
  • 如果 A 获取到读写锁的写锁,B 再获取读锁就会等待。

代码示例:

package main

import (
	"fmt"
	"sync"
	"time"
)

var x = 0
var rwLock sync.RWMutex
var wg sync.WaitGroup

func main() {
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go read()
	}
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go write()
	}
	wg.Wait()
}

// 读操作
func read() {
	rwLock.RLock()									// 加读锁
	fmt.Println(x)									// 读操作
	rwLock.RUnlock()								// 解锁
	wg.Done()
}

func write() {
	rwLock.Lock()									// 加写锁
	x += 1											// 写操作
	time.Sleep(time.Second)							// 假设写操作需要 1 秒
	rwLock.Unlock()									// 解锁
	wg.Done()
}

在上面代码中,定义两个函数,分别是读函数 read 和写函数 write ,在 read 函数中进行读锁的加锁和解锁,在 write 函数中进行写锁的加锁和解锁,然后在 main 函数中分别启动 10 个 goroutine 执行 read 和 write 函数,根据最后打印的时间间隔可以发现,如果是获取到了读锁,则再次获取读锁就会很快打印出结果,如果是再次获取写锁则会等待读锁解锁之后才会获取成功。

关注专栏,持续更新

公众号:CodeJR,关注查看更多文章与干货,回复golang领取golang学习电子书。