golang互斥锁跟读写锁

本文详细介绍了Go语言中的并发控制工具,包括互斥锁Mutex和读写锁RWMutex。通过示例代码解释了为何需要锁来避免竞态条件,并展示了Mutex和RWMutex的使用方法及特性。Mutex保证同一时间只有一个协程修改变量,而RWMutex允许多个读取或单个写入操作。在读多写少的场景中,RWMutex能提高效率。
摘要由CSDN通过智能技术生成

golang sync包里提供了 Locker接口、互斥锁 Mutex、读写锁 RWMutex用于处理并发过程中可能出现同时两个或多个协程(或线程)读或写同一个变量的情况。

为什么需要锁

在并发的情况下,多个线程或协程同时去修改一个变量。使用锁能保证在某一时间点内,只有一个协程或线程修改这一变量,具体我们可以看示例。先看不加锁的程序(会出现多个程序同时读该变量):

package main
import (
    "fmt"
    "time"
)
func main() {
    var a = 0
    for i := 0; i < 1000; i++ {
        go func(idx int) {
            a += 1
            fmt.Println(a)
        }(i)
    }
    time.Sleep(time.Second)
}

从理论上来说,上面的函数是每次递增a的值的,所以理论上应该会有1000个不同的值输出,实际结果呢?

$ go run main.go |sort|uniq |wc -l
992
$ go run main.go |sort|uniq |wc -l
1000
$ go run main.go |sort|uniq |wc -l
992
$ go run main.go |sort|uniq |wc -l
999

这里运行了4次,获取了三个不一样的结果。如果你有精力,可以将运行的结果逐一对比,在出现wc -l的结果小于1000时,绝对出现了重复值。为什么会现这样的情况?

协程依次执行:从寄存器读取 a 的值 -> 然后做加法运算 -> 最后写到寄存器。试想,此时一个协程取出 a 的值 3,正在做加法运算(还未写回寄存器)。同时另一个协程此时去取,取出了同样的 a 的值 3。最终导致的结果是,两个协程产出的结果相同,a 相当于只增加了 1。

所以,锁的概念就是,我正在处理 a(锁定),你们谁都别和我抢,等我处理完了(解锁),你们再处理。这样就实现了,同时处理 a 的协程只有一个,就实现了同步。

注:上面的方法是多协程的,增加runtime.GOMAXPROCS(4)改为多进程多线程同样会有这样的问题。

互斥锁-Mutex

上面的示例中出现的问题怎么解决?加一个互斥锁 Mutex就OK了。哪什么是互斥锁 ?其有两个方法可以调用,如下:

func (m *Mutex) Lock()
func (m *Mutex) Unlock()

我们改下循环递增示例中的代码,如下:

package main
import (
    "fmt"
    "time"
)
func main() {
    var a = 0
    var lock sync.Mutex
    for i := 0; i < 1000; i++ {
        go func(idx int) {
            lock.Lock()
            defer lock.Unlock()
            a += 1
            fmt.Printf("goroutine %d, a=%d\n", idx, a)
        }(i)
    }
    time.Sleep(1e9)
}

修改后执行的结果总是1000个不重服的值。而且使用go语言的lock锁一般不会出现忘了解锁的情况,因类其紧跟锁定的就是defer Unlock 。

需要注意的是一个互斥锁只能同时被一个 goroutine 锁定,其它 goroutine 将阻塞直到互斥锁被解锁(重新争抢对互斥锁的锁定)
看如下代码:

package main

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

// 互斥锁性能测试:比较适合于读多、写少的情况
func main() {
	ch := make(chan struct{}, 2)
	var l sync.Mutex
	go func() {
		fmt.Println("groutine2: 等待解锁")
		l.Lock()
		fmt.Println("groutine2: 已经加锁了")
		defer l.Unlock()
		fmt.Println("goroutine2: 欧耶,我也解锁了")
		ch <- struct{}{}
	}()
	go func() {
		l.Lock()
		fmt.Println("groutine1: 已经加锁了")
		defer l.Unlock()
		fmt.Println("goroutine1: 我会锁定大概 2s")
		time.Sleep(time.Second * 2)
		fmt.Println("goroutine1: 我解锁了,你们去抢吧")
		ch <- struct{}{}
	}()
	// 等待 goroutine 执行结束
	for i := 0; i < 2; i++ {
		<-ch
	}
}

运行结果

groutine1: 已经加锁了
goroutine1: 我会锁定大概 2s
groutine2: 等待解锁
goroutine1: 我解锁了,你们去抢吧
groutine2: 已经加锁了
goroutine2: 欧耶,我也解锁了

说明1执行Lock后到2无法再次加锁互相成功阻塞了。

互斥锁的相关概念

Mutex为互斥锁,Lock()加锁,Unlock()解锁,使用Lock()加锁后,便不能再次对其进行加锁,直到利用Unlock()解锁对其解锁后,才能再次加锁.适用于读写不确定场景,即读写次数没有明显的区别,并且只允许只有一个读或者写的场景,所以该锁叶叫做全局锁。

func (m *Mutex) Unlock()用于解锁m,如果在使用Unlock()前未加锁,就会引起一个运行错误.已经锁定的Mutex并不与特定的goroutine相关联,这样可以利用一个goroutine对其加锁,再利用其他goroutine对其解锁。

互斥锁只能锁定一次,当在解锁之前再次进行加锁,便会死锁状态,如果在加锁前解锁,便会报错 panic: sync: unlock of unlocked mutex

可以换个角度理解:

  1. 同一个goroutine进行Lock会出现死锁,因为会这里会出现始终无法执行下去的情况。
  2. 另一个goroutine也执行Lock的情况下,则会阻塞等待上一个的完成。

读写锁-RWMutex

RWMutex是一个读写锁,该锁可以加多个读锁或者一个写锁,其经常用于读次数远远多于写次数的场景.

func (rw *RWMutex) Lock()

func (rw *RWMutex) Lock()写锁,如果在添加写锁之前已经有其他的读锁和写锁,则lock就会阻塞直到该锁可用,为确保该锁最终可用,已阻塞的 Lock 调用会从获得的锁中排除新的读取器,即写锁权限高于读锁,有写锁时优先进行写锁定

func (rw *RWMutex) Unlock()

func (rw *RWMutex) Unlock()写锁解锁,如果没有进行写锁定,则就会引起一个运行时错误

func (rw *RWMutex) RLock()

func (rw *RWMutex) RLock()读锁,当有写锁时,无法加载读锁,当只有读锁或者没有锁时,可以加载读锁,读锁可以加载多个,所以适用于"读多写少"的场景

func (rw *RWMutex)RUnlock()

func (rw *RWMutex)RUnlock()读锁解锁,RUnlock 撤销单次RLock 调用,它对于其它同时存在的读取器则没有效果。若 rw 并没有为读取而锁定,调用 RUnlock 就会引发一个运行时错误(注:这种说法在go1.3版本中是不对的,例如下面这个例子)。

读写锁的写锁只能锁定一次,解锁前不能多次锁定,读锁可以多次,但读解锁次数最多只能比读锁次数多一次,一般情况下我们不建议读解锁次数多余读锁次数

简单来说记住下面的:

// 写操作的锁定和解锁
func (rw *RWMutex) Lock()
func (rw *RWMutex) Unlock()
// 读操作的锁定和解锁
func (rw *RWMutex) RLock()
func (rw *RWMutex) RUnlock()

注:区别在后的Lock和Unlock前有没有R 。

我们怎么理解读写锁呢?当有一个 goroutine 获得写锁定,其它无论是读锁定还是写锁定都将阻塞直到写解锁;当有一个 goroutine 获得读锁定,其它读锁定仍然可以继续;当有一个或任意多个读锁定,写锁定将等待所有读锁定解锁之后才能够进行写锁定。所以说这里的读锁定(RLock)目的其实是告诉写锁定:有很多人正在读取数据,你给我站一边去,等它们读(读解锁)完你再来写(写锁定)。我们可以将其总结为如下三条:

  • 同时只能有一个 goroutine 能够获得写锁定。
  • 同时可以有任意多个 gorouinte 获得读锁定。
  • 同时只能存在写锁定或读锁定(读和写互斥)。
package main
import (
    "fmt"
    "math/rand"
    "sync"
)
var count int
var rw sync.RWMutex
func main() {
    ch := make(chan struct{}, 10)
    for i := 0; i < 5; i++ {
        go read(i, ch)
    }
    for i := 0; i < 5; i++ {
        go write(i, ch)
    }
    for i := 0; i < 10; i++ {
        <-ch
    }
}
func read(n int, ch chan struct{}) {
    rw.RLock()
    fmt.Printf("goroutine %d 进入读操作...\n", n)
    v := count
    fmt.Printf("goroutine %d 读取结束,值为:%d\n", n, v)
    rw.RUnlock()
    ch <- struct{}{}
}
func write(n int, ch chan struct{}) {
    rw.Lock()
    fmt.Printf("goroutine %d 进入写操作...\n", n)
    v := rand.Intn(1000)
    count = v
    fmt.Printf("goroutine %d 写入结束,新值为:%d\n", n, v)
    rw.Unlock()
    ch <- struct{}{}
}
$ go run main.go
goroutine 4 进入写操作...
goroutine 4 写入结束,新值为:81
goroutine 2 进入读操作...
goroutine 2 读取结束,值为:81
goroutine 3 进入读操作...
goroutine 3 读取结束,值为:81
goroutine 0 进入读操作...
goroutine 0 读取结束,值为:81
goroutine 1 进入读操作...
goroutine 4 进入读操作...
goroutine 4 读取结束,值为:81
goroutine 1 读取结束,值为:81
goroutine 0 进入写操作...
goroutine 0 写入结束,新值为:887
goroutine 1 进入写操作...
goroutine 1 写入结束,新值为:847
goroutine 3 进入写操作...
goroutine 3 写入结束,新值为:59
goroutine 2 进入写操作...
goroutine 2 写入结束,新值为:81

再来看两个例子:

package main

import (
	"sync"
	"time"
)

var m *sync.RWMutex

func main() {
	m = new(sync.RWMutex)
	// 多个同时读
	go read(1)
	go read(2)
	time.Sleep(2e9)
}
func read(i int) {
	println(i, "read start")
	m.RLock()
	println(i, "reading")
	time.Sleep(1e9)
	println(i, "read over")
	m.RUnlock()
	println(i, "read end")
}

结果如下

1 read start
1 reading
2 read start
2 reading
2 read over
2 read end
1 read over
1 read end

这里例子中,多个读操作同时读一个操作。虽然加了锁,但都是读是不受影响的。(读和写是互斥的,读和读不互斥)。

package main

import (
	"sync"
	"time"
)

var m sync.RWMutex
var wg sync.WaitGroup

func main() {
	wg.Add(4)
	// 写的时候啥也不能干
	go write(1)
	go read(2)
	go read(3)
	go write(4)
	wg.Wait()
}
func read(i int) {
	defer wg.Done()
	println(i, "read start")
	m.RLock()
	println(i, "reading")
	time.Sleep(1e9)
	println(i, "read over")
	m.RUnlock()
	println(i, "read end")
}
func write(i int) {
	defer wg.Done()
	println(i, "write start")
	m.Lock()
	println(i, "writing")
	time.Sleep(1e9)
	println(i, "write over")
	m.Unlock()
	println(i, "write end")
}

$ go run main.go
4 write start
4 writing
3 read start
2 read start
1 write start
4 write over
4 write end
2 reading
3 reading
3 read over
3 read end
2 read over
2 read end
1 writing
1 write over
1 write end

由于读写互斥,上面这个示例中,写开始的时候,读必须要等写进行完才能继续。不然他只能继续等待,这就像只有一个茅坑,别我蹲着,你着急也不能去抢。

参考地址:

  • http://www.361way.com/rwmutex/5984.html
  • https://studygolang.com/articles/9262
  • https://studygolang.com/pkgdoc
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值