Go语言sync包与锁实现限制线程对变量的访问
作者:C语言中文网 发布时间:2024-02-21 13:52:29
Go语言中 sync 包里提供了互斥锁 Mutex 和读写锁 RWMutex 用于处理并发过程中可能出现同时两个或多个协程(或线程)读或写同一个变量的情况。
为什么需要锁
锁是 sync 包中的核心,它主要有两个方法,分别是加锁(Lock)和解锁(Unlock)。
在并发的情况下,多个线程或协程同时其修改一个变量,使用锁能保证在某一时间内,只有一个协程或线程修改这一变量。
不使用锁时,在并发的情况下可能无法得到想要的结果,如下所示:
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 的值依次递增输出,然而实际结果却是下面这样子的。
537
995
996
997
538
999
1000
通过运行结果可以看出 a 的值并不是按顺序递增输出的,这是为什么呢?
协程的执行顺序大致如下所示:
从寄存器读取 a 的值;
然后做加法运算;
最后写到寄存器。
按照上面的顺序,假如有一个协程取得 a 的值为 3,然后执行加法运算,此时又有一个协程对 a 进行取值,得到的值同样是 3,最终两个协程的返回结果是相同的。
而锁的概念就是,当一个协程正在处理 a 时将 a 锁定,其它协程需要等待该协程处理完成并将 a 解锁后才能再进行操作,也就是说同时处理 a 的协程只能有一个,从而避免上面示例中的情况出现。
互斥锁 Mutex
上面的示例中出现的问题怎么解决呢?加一个互斥锁 Mutex 就可以了。那什么是互斥锁呢 ?互斥锁中其有两个方法可以调用,如下所示:
func (m *Mutex) Lock()
func (m *Mutex) Unlock()
将上面的代码略作修改,如下所示:
package main
import (
"fmt"
"sync"
"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)
}
// 等待 1s 结束主程序
// 确保所有协程执行完
time.Sleep(time.Second)
}
运行结果如下:
goroutine 995, a=996
goroutine 996, a=997
goroutine 997, a=998
goroutine 998, a=999
goroutine 999, a=1000
需要注意的是一个互斥锁只能同时被一个 goroutine 锁定,其它 goroutine 将阻塞直到互斥锁被解锁(重新争抢对互斥锁的锁定),示例代码如下:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
ch := make(chan struct{}, 2)
var l sync.Mutex
go func() {
l.Lock()
defer l.Unlock()
fmt.Println("goroutine1: 我会锁定大概 2s")
time.Sleep(time.Second * 2)
fmt.Println("goroutine1: 我解锁了,你们去抢吧")
ch <- struct{}{}
}()
go func() {
fmt.Println("goroutine2: 等待解锁")
l.Lock()
defer l.Unlock()
fmt.Println("goroutine2: 欧耶,我也解锁了")
ch <- struct{}{}
}()
// 等待 goroutine 执行结束
for i := 0; i < 2; i++ {
<-ch
}
}
上面的代码运行结果如下:
goroutine1: 我会锁定大概 2s
goroutine2: 等待解锁
goroutine1: 我解锁了,你们去抢吧
goroutine2: 欧耶,我也解锁了
读写锁
读写锁有如下四个方法:
写操作的锁定和解锁分别是func (*RWMutex) Lock和func (*RWMutex) Unlock;
读操作的锁定和解锁分别是func (*RWMutex) Rlock和func (*RWMutex) RUnlock。
读写锁的区别在于:
当有一个 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{}{}
}
其执行结果如下:
goroutine 0 进入读操作...
goroutine 0 读取结束,值为:0
goroutine 3 进入读操作...
goroutine 1 进入读操作...
goroutine 3 读取结束,值为:0
goroutine 1 读取结束,值为:0
goroutine 4 进入写操作...
goroutine 4 写入结束,新值为:81
goroutine 4 进入读操作...
goroutine 4 读取结束,值为:81
goroutine 2 进入读操作...
goroutine 2 读取结束,值为:81
goroutine 0 进入写操作...
goroutine 0 写入结束,新值为:887
goroutine 1 进入写操作...
goroutine 1 写入结束,新值为:847
goroutine 2 进入写操作...
goroutine 2 写入结束,新值为:59
goroutine 3 进入写操作...
goroutine 3 写入结束,新值为:81
下面再来看两个示例。
【示例 1】多个读操作同时读取一个变量时,虽然加了锁,但是读操作是不受影响的。(读和写是互斥的,读和读不互斥)
package main
import (
"sync"
"time"
)
var m *sync.RWMutex
func main() {
m = new(sync.RWMutex)
// 多个同时读
go read(1)
go read(2)
time.Sleep(2*time.Second)
}
func read(i int) {
println(i,"read start")
m.RLock()
println(i,"reading")
time.Sleep(1*time.Second)
m.RUnlock()
println(i,"read over")
}
运行结果如下:
1 read start
1 reading
2 read start
2 reading
1 read over
2 read over
【示例 2】由于读写互斥,所以写操作开始的时候,读操作必须要等写操作进行完才能继续,不然读操作只能继续等待。
package main
import (
"sync"
"time"
)
var m *sync.RWMutex
func main() {
m = new(sync.RWMutex)
// 写的时候啥也不能干
go write(1)
go read(2)
go write(3)
time.Sleep(2*time.Second)
}
func read(i int) {
println(i,"read start")
m.RLock()
println(i,"reading")
time.Sleep(1*time.Second)
m.RUnlock()
println(i,"read over")
}
func write(i int) {
println(i,"write start")
m.Lock()
println(i,"writing")
time.Sleep(1*time.Second)
m.Unlock()
println(i,"write over")
}
运行结果如下:
1 write start
3 write start
1 writing
2 read start
1 write over
2 reading
来源:http://c.biancheng.net/view/4307.html


猜你喜欢
- Python np.argmin()和np.argmax()函数按照axis的要求返回最小的数/最大的数的下标numpy.argmin(a,
- 项目中要对短文本进行相似度估计,word2vec是一个很火的工具。本文就word2vec的训练以及加载进行了总结。word2vec的原理就不
- 本文实例讲述了Python计算程序运行时间的方法。分享给大家供大家参考。具体实现方法如下:import timedef start_slee
- 我就废话不多说了,还是直接看代码吧!# 利用python在内存中读写str和二进制数据from io import StringIOfrom
- SQL 标准使用 CREATE TABLE 语句创建数据表;MySQL 则实现了三种创建表的方法,支持自定义表结构或者通过复制已有的表结构来
- 目录1、系统环境,必要知识2、安装python3.6.53、安装Django4、安装uWSGI5、安装nginx6、MySQL安装配置7、编
- 我就废话不多说了,大家还是直接看代码吧~import numpy as np kernel = np.array([1, 1, 1, 2])
- ASP的强大不仅仅局限于接受和显示的交互,更多的是运用ActiveX 组件进行更强大的Web应用。那究竟ActiveX组件为何物?
- 在图片上右击设置你想放在的目录补充知识:pycharm没有sciview窗口的解决(换专业版pycharm!)只有专业版才有科学模式,too
- python的列表list可以用for循环进行遍历,实际开发中发现一个问题,就是遍历的时候删除会出错,例如l = [1,2,3,4]for
- 1、subprocess这个模块来产生子进程,并且可以连接到子进程的标准输入、输出、错误中,还可以获得子进程的返回值。2、subproces
- 目录1.数据概览分析1.1 数据概览1.2 数据分析2. 项目总体思路2.1 数据读取2.2 模型预处理(1)数据离群点处理(2)数据归一化
- 本文实例为大家分享了Python绑定方法与非绑定方法,供大家参考,具体内容如下定义:绑定方法(绑定给谁,谁来调用就自动将它本身当作第一个参数
- 我们可以通过python 来实现这样一个简单的爬虫猜密码功能。下面就看看如何使用python来实现这样一个功能。这里我们知道用户的昵称为:h
- 1,安装 安装就不多说了,除了一般的那个压缩包
- 目录1. 序列2. 列表2.1 列表的特性2.1.1 列表的连接操作符和重复操作符2.1.3 列表的索引2.1.4 列表的切片2.1.5 列
- 图片的自动编码很容易就想到用卷积神经网络做为编码-解码器。在实际的操作中,也经常使用卷积自动编码器去解决图像编码问题,而且非常有效。下面通过
- <script type="text/javascript"> </script>
- mysql创建用户并授权:格式:grant 权限 on 数据库名.表名 to 用户@登录主机 identified by "用户密
- 1. 整体思路首先我们来梳理下整体上的实现思路,首先一点:整体思路和 vhr 一模一样。考虑到有的小伙伴可能已经忘记 vhr 中前端动态菜单