Go语言基础之13--线程安全及互斥锁和读写锁
一、线程安全介绍
1.1 现实例子
A. 多个goroutine同时操作一个资源,这个资源又叫临界区
B. 现实生活中的十字路口,通过红路灯实现线程安全
C. 火车上的厕所(进去之后先加锁,在上厕所,不加锁两个人都进去就出问题了,出来后在解锁,别人就可以使用了),通过互斥锁来实现线程安全
D、在程序中,同一个变量多个goroutine去修改的时候,肯定是不允许同时修改的,同时修改肯定会出问题,所以当一个goroutine在修改之前需要加锁,修改结束在解锁,这样别的goroutine就可以去修改了。
1.2 实际例子
x = x +1
A. 先从内存中取出x的值
B. CPU进行计算, x+1
C. 然后把x+1的结果存储在内存中
解释:
就是两个goroutine同时去操作x(共享资源),最后的结果x并不是2,由于线程安全的问题,导致最后的结果还是等于1;
详情也如下图所示:
下面来看一个实际例子:
test1和test2函数都是在自增到1000000(对同一个变量count进行修改)
1)当test1函数和test2函数跑在同一个线程时:
package main import (
"fmt"
) var count int func test1() {
for i := ; i < ; i++ {
count++
}
} func test2() {
for i := ; i < ; i++ {
count++
}
} func main() {
test1()
test2()
fmt.Printf("count=%d\n", count)
}
执行结果如下:
因为是串行执行,所以最终结果肯定是2000000
2)当test1函数和test2函数独自起goroutine运行时:
package main import (
"fmt"
"time"
) var count int func test1() {
for i := ; i < ; i++ {
count++
}
} func test2() {
for i := ; i < ; i++ {
count++
}
} func main() {
go test1()
go test2() time.Sleep(time.Second)
fmt.Printf("count=%d\n", count)
}
执行结果如下:
解释:
可以看到当test1和test2同时运行对count(共享资源)进行修改时,就会出现冲突,最终结果也就不是2000000了
1.3 如何解决?
那么如何解决上述线程安全问题呢,就是我们接下来要学习的互斥锁。
第2章 互斥锁
2.1 互斥锁介绍
A. 同时有且只有一个线程进入临界区,其他的线程则在等待锁;
B. 当互斥锁释放之后,等待锁的线程才可以获取锁进入临界区;
C. 多个线程同时等待同一个锁,唤醒的策略是随机的;
2.2 互斥锁使用实例
package main import (
"fmt"
"sync" //互斥锁需要使用这个包。 "time"
) var count int
var mutex sync.Mutex //定义一个锁的变量(互斥锁的关键字是Mutex,其是一个结构体,传参一定要传地址,否则就不对了)
func test1() {
for i := ; i < ; i++ {
mutex.Lock() //对共享变量操作之前先加锁
count++
mutex.Unlock() //对共享变量操作完毕在解锁,这样就保护了共享的资源
}
} func test2() {
for i := ; i < ; i++ {
mutex.Lock()
count++
mutex.Unlock()
}
} func main() {
go test1()
go test2() time.Sleep(time.Second)
fmt.Printf("count=%d\n", count)
}
执行结果如下:
解释:
加锁(互斥锁)之后其实是相当于串行(对共享变量进行操作时)执行了,就算是goroutine也不例外。
2.3 互斥锁高阶实例
1)未加互斥锁代码(有问题)
package main import (
"fmt"
"sync"
) var x = func increment(wg *sync.WaitGroup) {
x = x +
wg.Done()
}
func main() {
var w sync.WaitGroup
for i := ; i < ; i++ {
w.Add()
go increment(&w)
}
w.Wait()
fmt.Println("final value of x", x)
}
执行结果:
2)添加互斥锁代码
package main import (
"fmt"
"sync"
) var x = func increment(wg *sync.WaitGroup, m *sync.Mutex) {
m.Lock()
x = x +
m.Unlock()
wg.Done()
}
func main() {
var w sync.WaitGroup
var m sync.Mutex
for i := ; i < ; i++ {
w.Add()
go increment(&w, &m)
}
w.Wait()
fmt.Println("final value of x", x)
}
执行结果:
三、读写锁
3.1 使用场景
A. 读多写少的场景;
B. 分为两种角色,读锁和写锁;
C. 当一个goroutine获取写锁之后,其他的goroutine获取写锁或读锁都会等待;
D. 当一个goroutine获取读锁之后,其他的goroutine获取写锁都会等待, 但其他
goroutine获取读锁时,都会继续获得锁.;
3.2 读写锁案例演示
package main import (
"sync"
"time"
) var rwlock sync.RWMutex //定义一个锁的变量(读写锁的关键字是RWMutex,其是一个结构体,传参一定要传地址,否则就不对了)
var wg sync.WaitGroup
var count int func writer() { //写的线程
for i := ; i < ; i++ {
// 加写锁
rwlock.Lock() //加锁写锁之后,其他goroutine就不能针对该共享变量加读锁或写锁(读取或写入)了
count++
time.Sleep( * time.Millisecond) //模拟写操作需要10ms
// 释放写锁
rwlock.Unlock()
}
wg.Done()
} func reader() { //读的线程
for i := ; i < ; i++ {
// 加读锁
rwlock.RLock() //对于读锁来说,其他goroutine依然可以对该共享变量进行读取(读锁)依然可以,但是写入不行,获取写锁需要等待。
_ = count
//fmt.Printf("count=%d\n", count)
time.Sleep( * time.Millisecond) //模拟读操作场景需要1ms
// 释放读锁
rwlock.RUnlock()
}
wg.Done()
} func main() {
wg.Add()
go writer()
for i := ; i < ; i++ {
wg.Add()
go reader() //读锁是并发的,这里加了for循环主要是为了模拟只要有1个goroutine能够读取到共享资源,其他的goroutine也可以获取到。
}
wg.Wait()
}
执行结果:
3.3 读写锁和互斥锁性能比较
针对同一个程序,我们通过比较互斥锁和读写锁的耗时来进行直观展示:
首先计算读写锁性能:
代码示例如下:
package main import (
"fmt"
"sync"
"time"
) var rwlock sync.RWMutex //定义一个锁的变量(读写锁的关键字是RWMutex,其是一个结构体,传参一定要传地址,否则就不对了)
var wg sync.WaitGroup
var count int func writer() { //写的线程
for i := ; i < ; i++ {
// 加写锁
rwlock.Lock() //加锁写锁之后,其他goroutine就不能针对该共享变量加读锁或写锁(读取或写入)了
count++
time.Sleep( * time.Millisecond) //模拟写操作需要10ms
// 释放写锁
rwlock.Unlock()
}
wg.Done()
} func reader() { //读的线程
for i := ; i < ; i++ {
// 加读锁
rwlock.RLock() //对于读锁来说,其他goroutine依然可以对该共享变量进行读取(读锁)依然可以,但是写入不行,获取写锁需要等待。
_ = count
//fmt.Printf("count=%d\n", count)
time.Sleep( * time.Millisecond) //模拟读操作场景需要1ms
// 释放读锁
rwlock.RUnlock()
}
wg.Done()
} func main() { start := time.Now().UnixNano() //开始时间
wg.Add()
go writer()
for i := ; i < ; i++ {
wg.Add()
go reader() //读锁是并发的,这里加了for循环主要是为了模拟只要有1个goroutine能够读取到共享资源,其他的goroutine也可以获取到。
}
wg.Wait()
end := time.Now().UnixNano() //结束时间
cost := (end - start) / / /
fmt.Printf("cost %d s\n", cost) }
执行结果如下:
互斥锁性能:
见如下实例:
package main import (
"fmt"
"sync"
"time"
) var mlock sync.Mutex //声明互斥锁变量
var wg sync.WaitGroup
var count int func writer_mutex() { //写的线程
for i := ; i < ; i++ {
mlock.Lock()
count++
time.Sleep( * time.Millisecond) //模拟写操作需要10ms
mlock.Unlock()
}
wg.Done()
} func reader_mutex() { //读的线程
for i := ; i < ; i++ {
mlock.Lock() //对于多个goroutine来说,互斥锁也是只有1个goroutine可以读,并不像读写锁一样,所有goroutine都可以读
_ = count
//fmt.Printf("count=%d\n", count)
time.Sleep( * time.Millisecond) //模拟读操作场景需要1ms
mlock.Unlock()
}
wg.Done()
} func main() { start := time.Now().UnixNano() //开始时间
wg.Add()
go writer_mutex()
for i := ; i < ; i++ {
wg.Add()
go reader_mutex()
}
wg.Wait()
end := time.Now().UnixNano() //结束时间
cost := (end - start) / / /
fmt.Printf("cost %d s\n", cost) }
执行结果如下:
总结:
可以看到最终的结果是同一个程序互斥锁比读写锁耗时多了9秒,主要原因是在读的时候,读写锁可以多个读线程去读,而互斥锁依然只能是一个线程去读,1比10的比例,就造成了最终这个结果。
葵花宝典:
读多写少用读写锁,读写差不多用互斥锁。
Go语言基础之13--线程安全及互斥锁和读写锁的更多相关文章
- UNIX环境高级编程——线程同步之互斥锁、读写锁和条件变量(小结)
一.使用互斥锁 1.初始化互斥量 pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER;//静态初始化互斥量 int pthread_mutex_init( ...
- 四十、Linux 线程——互斥锁和读写锁
40.1 互斥锁 40.1.1 介绍 互斥锁(mutex)是一种简单的加锁的方法来控制对共享资源的访问. 在同一时刻只能有一个线程掌握某个互斥锁,拥有上锁状态的线程能够对共享资源进行访问. 若其他线程 ...
- Go语言中的互斥锁和读写锁(Mutex和RWMutex)
目录 一.Mutex(互斥锁) 不加锁示例 加锁示例 二.RWMutex(读写锁) 并发读示例 并发读写示例 三.死锁场景 1.Lock/Unlock不是成对出现 2.锁被拷贝使用 3.循环等待 虽然 ...
- Python进阶----线程基础,开启线程的方式(类和函数),线程VS进程,线程的方法,守护线程,详解互斥锁,递归锁,信号量
Python进阶----线程基础,开启线程的方式(类和函数),线程VS进程,线程的方法,守护线程,详解互斥锁,递归锁,信号量 一丶线程的理论知识 什么是线程: 1.线程是一堆指令,是操作系统调度 ...
- APUE学习笔记——11 线程同步、互斥锁、自旋锁、条件变量
线程同步 同属于一个进程的不同线程是共享内存的,因而在执行过程中需要考虑数据的一致性. 假设:进程有一变量i=0,线程A执行i++,线程B执行i++,那么最终i的取值是多少呢?似乎一定 ...
- 线程同步 - POSIX互斥锁
线程同步 - POSIX互斥锁 概括 本文讲解POSIX中互斥量的基本用法,从而能达到简单的线程同步.互斥量是一种特殊的变量,它有两种状态:锁定以及解锁.如果互斥量是锁定的,就有一个特定的线程持有或者 ...
- 二、多线程基础-乐观锁_悲观锁_重入锁_读写锁_CAS无锁机制_自旋锁
1.10乐观锁_悲观锁_重入锁_读写锁_CAS无锁机制_自旋锁1)乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将 比较-设置 ...
- ndk学习之c++语言基础复习----C++线程与智能指针
线程 线程,有时被称为轻量进程,是程序执行的最小单元. C++11线程: 我们知道平常谈C++线程相关的东东基本都是基于之后要学习的posix相关的,其实在C++11有自己新式创建线程的方法,所以先来 ...
- node源码详解(七) —— 文件异步io、线程池【互斥锁、条件变量、管道、事件对象】
本作品采用知识共享署名 4.0 国际许可协议进行许可.转载保留声明头部与原文链接https://luzeshu.com/blog/nodesource7 本博客同步在https://cnodejs.o ...
随机推荐
- Quartz_1_简单编程式任务调度使用(SimpleTrigger)
最近在工作中,要做定时任务的更能,最开始的时候,想到的是 JavaSE 中,自带 Timer 及 TimerTask 联合使用,完成定时任务.最后发现,随着业务的复杂,JDK 中的 Timer 和 T ...
- [转]MySQL5.6.22 安装
原文路径 http://jifeng3321.iteye.com/blog/2181517?utm_source=tuicool 由于一直做银行项目,所以一直在用oracle和db2,但最近自己想 ...
- Effective Objective-C [上]
网上看到的 http://esoftmobile.com/2013/08/10/effective-objective-c/ 本文是针对<Effective Objective-C>一书的 ...
- 导出Excel解决方案之一NOPI
一.概要 导出Excel这个功能相信很多人都做过,但是实现这个功能解决方案有好几种,今天我未大家介绍一种比较新的,其实也不新了- -!它叫NPOI,可以完美操作EXCEl的导入和导出操作,让我们一起看 ...
- Linux-shell实现阳历转农历(序)
好些天没有登陆邮箱,前几天上班打开一看垃圾箱中有一封邮件让我好激动,还是国外友人的英文邮件.^_^大概内容是我早些时候写的一个阳历转农历的shell小程序,他在用的时候发现了bug,但是这个bug我在 ...
- Entity Framework Tutorial Basics(9):Entity Relationships
这篇前面已经转载出来了,地址:http://www.cnblogs.com/purplefox2008/p/5646466.html
- C++面试笔记--指针和引用
面试一:指针与引用的区别? 答案: (1)非空区别.在任何情况下都不能使用指向空值的引用.因此如果你使用一个变量并让它指向一个对象,但是该变量在某些时候也可能不指向任何对象,这时你应该把变量声明为指针 ...
- 《Spring实战》-2
装配Bean 1.装配wiring,即创建应用对象之间的协作关系的行为,者也是依赖注入的本质. 2.创建Spring配置 从Sring3.0开始,Spring容器提供了两种配置Bean的方式: XML ...
- 关于C# WinForm_Tree View的一些基本用法(摘抄)
下面是treeview的用法TreeView组件是由多个类来定义的,TreeView组件是由命名空间"System.Windows .Forms"中的"TreeView& ...
- [转]Marshaling a SAFEARRAY of Managed Structures by P/Invoke Part 4.
1. Introduction. 1.1 In parts 1 through 3 of this series of articles, I have thoroughly discussed th ...