一、线程安全介绍

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函数跑在同一个线程时:

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. )
  6.  
  7. var count int
  8.  
  9. func test1() {
  10. for i := ; i < ; i++ {
  11. count++
  12. }
  13. }
  14.  
  15. func test2() {
  16. for i := ; i < ; i++ {
  17. count++
  18. }
  19. }
  20.  
  21. func main() {
  22. test1()
  23. test2()
  24. fmt.Printf("count=%d\n", count)
  25. }

执行结果如下:

因为是串行执行,所以最终结果肯定是2000000

2)当test1函数和test2函数独自起goroutine运行时:

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "time"
  6. )
  7.  
  8. var count int
  9.  
  10. func test1() {
  11. for i := ; i < ; i++ {
  12. count++
  13. }
  14. }
  15.  
  16. func test2() {
  17. for i := ; i < ; i++ {
  18. count++
  19. }
  20. }
  21.  
  22. func main() {
  23. go test1()
  24. go test2()
  25.  
  26. time.Sleep(time.Second)
  27. fmt.Printf("count=%d\n", count)
  28. }

执行结果如下:

解释:

可以看到当test1和test2同时运行对count(共享资源)进行修改时,就会出现冲突,最终结果也就不是2000000了

1.3 如何解决?

那么如何解决上述线程安全问题呢,就是我们接下来要学习的互斥锁。

第2章 互斥锁

2.1 互斥锁介绍

A. 同时有且只有一个线程进入临界区,其他的线程则在等待锁;

B. 当互斥锁释放之后,等待锁的线程才可以获取锁进入临界区;

C. 多个线程同时等待同一个锁,唤醒的策略是随机的;

2.2 互斥锁使用实例

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "sync" //互斥锁需要使用这个包。
  6.  
  7. "time"
  8. )
  9.  
  10. var count int
  11. var mutex sync.Mutex //定义一个锁的变量(互斥锁的关键字是Mutex,其是一个结构体,传参一定要传地址,否则就不对了)
  12. func test1() {
  13. for i := ; i < ; i++ {
  14. mutex.Lock() //对共享变量操作之前先加锁
  15. count++
  16. mutex.Unlock() //对共享变量操作完毕在解锁,这样就保护了共享的资源
  17. }
  18. }
  19.  
  20. func test2() {
  21. for i := ; i < ; i++ {
  22. mutex.Lock()
  23. count++
  24. mutex.Unlock()
  25. }
  26. }
  27.  
  28. func main() {
  29. go test1()
  30. go test2()
  31.  
  32. time.Sleep(time.Second)
  33. fmt.Printf("count=%d\n", count)
  34. }

执行结果如下:

解释:

加锁(互斥锁)之后其实是相当于串行(对共享变量进行操作时)执行了,就算是goroutine也不例外。

2.3 互斥锁高阶实例

1)未加互斥锁代码(有问题)

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "sync"
  6. )
  7.  
  8. var x =
  9.  
  10. func increment(wg *sync.WaitGroup) {
  11. x = x +
  12. wg.Done()
  13. }
  14. func main() {
  15. var w sync.WaitGroup
  16. for i := ; i < ; i++ {
  17. w.Add()
  18. go increment(&w)
  19. }
  20. w.Wait()
  21. fmt.Println("final value of x", x)
  22. }

执行结果:

2)添加互斥锁代码

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "sync"
  6. )
  7.  
  8. var x =
  9.  
  10. func increment(wg *sync.WaitGroup, m *sync.Mutex) {
  11. m.Lock()
  12. x = x +
  13. m.Unlock()
  14. wg.Done()
  15. }
  16. func main() {
  17. var w sync.WaitGroup
  18. var m sync.Mutex
  19. for i := ; i < ; i++ {
  20. w.Add()
  21. go increment(&w, &m)
  22. }
  23. w.Wait()
  24. fmt.Println("final value of x", x)
  25. }

执行结果:

三、读写锁

3.1 使用场景

A. 读多写少的场景;

B. 分为两种角色,读锁和写锁;

C. 当一个goroutine获取写锁之后,其他的goroutine获取写锁或读锁都会等待;

D. 当一个goroutine获取读锁之后,其他的goroutine获取写锁都会等待, 但其他

goroutine获取读锁时,都会继续获得锁.;

3.2 读写锁案例演示

  1. package main
  2.  
  3. import (
  4. "sync"
  5. "time"
  6. )
  7.  
  8. var rwlock sync.RWMutex //定义一个锁的变量(读写锁的关键字是RWMutex,其是一个结构体,传参一定要传地址,否则就不对了)
  9. var wg sync.WaitGroup
  10. var count int
  11.  
  12. func writer() { //写的线程
  13. for i := ; i < ; i++ {
  14. // 加写锁
  15. rwlock.Lock() //加锁写锁之后,其他goroutine就不能针对该共享变量加读锁或写锁(读取或写入)了
  16. count++
  17. time.Sleep( * time.Millisecond) //模拟写操作需要10ms
  18. // 释放写锁
  19. rwlock.Unlock()
  20. }
  21. wg.Done()
  22. }
  23.  
  24. func reader() { //读的线程
  25. for i := ; i < ; i++ {
  26. // 加读锁
  27. rwlock.RLock() //对于读锁来说,其他goroutine依然可以对该共享变量进行读取(读锁)依然可以,但是写入不行,获取写锁需要等待。
  28. _ = count
  29. //fmt.Printf("count=%d\n", count)
  30. time.Sleep( * time.Millisecond) //模拟读操作场景需要1ms
  31. // 释放读锁
  32. rwlock.RUnlock()
  33. }
  34. wg.Done()
  35. }
  36.  
  37. func main() {
  38. wg.Add()
  39. go writer()
  40. for i := ; i < ; i++ {
  41. wg.Add()
  42. go reader() //读锁是并发的,这里加了for循环主要是为了模拟只要有1个goroutine能够读取到共享资源,其他的goroutine也可以获取到。
  43. }
  44. wg.Wait()
  45. }

执行结果:

3.3 读写锁和互斥锁性能比较

针对同一个程序,我们通过比较互斥锁和读写锁的耗时来进行直观展示:

首先计算读写锁性能:

代码示例如下:

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "sync"
  6. "time"
  7. )
  8.  
  9. var rwlock sync.RWMutex //定义一个锁的变量(读写锁的关键字是RWMutex,其是一个结构体,传参一定要传地址,否则就不对了)
  10. var wg sync.WaitGroup
  11. var count int
  12.  
  13. func writer() { //写的线程
  14. for i := ; i < ; i++ {
  15. // 加写锁
  16. rwlock.Lock() //加锁写锁之后,其他goroutine就不能针对该共享变量加读锁或写锁(读取或写入)了
  17. count++
  18. time.Sleep( * time.Millisecond) //模拟写操作需要10ms
  19. // 释放写锁
  20. rwlock.Unlock()
  21. }
  22. wg.Done()
  23. }
  24.  
  25. func reader() { //读的线程
  26. for i := ; i < ; i++ {
  27. // 加读锁
  28. rwlock.RLock() //对于读锁来说,其他goroutine依然可以对该共享变量进行读取(读锁)依然可以,但是写入不行,获取写锁需要等待。
  29. _ = count
  30. //fmt.Printf("count=%d\n", count)
  31. time.Sleep( * time.Millisecond) //模拟读操作场景需要1ms
  32. // 释放读锁
  33. rwlock.RUnlock()
  34. }
  35. wg.Done()
  36. }
  37.  
  38. func main() {
  39.  
  40. start := time.Now().UnixNano() //开始时间
  41. wg.Add()
  42. go writer()
  43. for i := ; i < ; i++ {
  44. wg.Add()
  45. go reader() //读锁是并发的,这里加了for循环主要是为了模拟只要有1个goroutine能够读取到共享资源,其他的goroutine也可以获取到。
  46. }
  47. wg.Wait()
  48. end := time.Now().UnixNano() //结束时间
  49. cost := (end - start) / / /
  50. fmt.Printf("cost %d s\n", cost)
  51.  
  52. }

执行结果如下:

互斥锁性能:

见如下实例:

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "sync"
  6. "time"
  7. )
  8.  
  9. var mlock sync.Mutex //声明互斥锁变量
  10. var wg sync.WaitGroup
  11. var count int
  12.  
  13. func writer_mutex() { //写的线程
  14. for i := ; i < ; i++ {
  15. mlock.Lock()
  16. count++
  17. time.Sleep( * time.Millisecond) //模拟写操作需要10ms
  18. mlock.Unlock()
  19. }
  20. wg.Done()
  21. }
  22.  
  23. func reader_mutex() { //读的线程
  24. for i := ; i < ; i++ {
  25. mlock.Lock() //对于多个goroutine来说,互斥锁也是只有1个goroutine可以读,并不像读写锁一样,所有goroutine都可以读
  26. _ = count
  27. //fmt.Printf("count=%d\n", count)
  28. time.Sleep( * time.Millisecond) //模拟读操作场景需要1ms
  29. mlock.Unlock()
  30. }
  31. wg.Done()
  32. }
  33.  
  34. func main() {
  35.  
  36. start := time.Now().UnixNano() //开始时间
  37. wg.Add()
  38. go writer_mutex()
  39. for i := ; i < ; i++ {
  40. wg.Add()
  41. go reader_mutex()
  42. }
  43. wg.Wait()
  44. end := time.Now().UnixNano() //结束时间
  45. cost := (end - start) / / /
  46. fmt.Printf("cost %d s\n", cost)
  47.  
  48. }

执行结果如下:

总结:

可以看到最终的结果是同一个程序互斥锁比读写锁耗时多了9秒,主要原因是在读的时候,读写锁可以多个读线程去读,而互斥锁依然只能是一个线程去读,1比10的比例,就造成了最终这个结果。

葵花宝典

读多写少用读写锁,读写差不多用互斥锁。

Go语言基础之13--线程安全及互斥锁和读写锁的更多相关文章

  1. UNIX环境高级编程——线程同步之互斥锁、读写锁和条件变量(小结)

    一.使用互斥锁 1.初始化互斥量 pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER;//静态初始化互斥量 int pthread_mutex_init( ...

  2. 四十、Linux 线程——互斥锁和读写锁

    40.1 互斥锁 40.1.1 介绍 互斥锁(mutex)是一种简单的加锁的方法来控制对共享资源的访问. 在同一时刻只能有一个线程掌握某个互斥锁,拥有上锁状态的线程能够对共享资源进行访问. 若其他线程 ...

  3. Go语言中的互斥锁和读写锁(Mutex和RWMutex)

    目录 一.Mutex(互斥锁) 不加锁示例 加锁示例 二.RWMutex(读写锁) 并发读示例 并发读写示例 三.死锁场景 1.Lock/Unlock不是成对出现 2.锁被拷贝使用 3.循环等待 虽然 ...

  4. Python进阶----线程基础,开启线程的方式(类和函数),线程VS进程,线程的方法,守护线程,详解互斥锁,递归锁,信号量

    Python进阶----线程基础,开启线程的方式(类和函数),线程VS进程,线程的方法,守护线程,详解互斥锁,递归锁,信号量 一丶线程的理论知识 什么是线程:    1.线程是一堆指令,是操作系统调度 ...

  5. APUE学习笔记——11 线程同步、互斥锁、自旋锁、条件变量

    线程同步     同属于一个进程的不同线程是共享内存的,因而在执行过程中需要考虑数据的一致性.     假设:进程有一变量i=0,线程A执行i++,线程B执行i++,那么最终i的取值是多少呢?似乎一定 ...

  6. 线程同步 - POSIX互斥锁

    线程同步 - POSIX互斥锁 概括 本文讲解POSIX中互斥量的基本用法,从而能达到简单的线程同步.互斥量是一种特殊的变量,它有两种状态:锁定以及解锁.如果互斥量是锁定的,就有一个特定的线程持有或者 ...

  7. 二、多线程基础-乐观锁_悲观锁_重入锁_读写锁_CAS无锁机制_自旋锁

    1.10乐观锁_悲观锁_重入锁_读写锁_CAS无锁机制_自旋锁1)乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将 比较-设置 ...

  8. ndk学习之c++语言基础复习----C++线程与智能指针

    线程 线程,有时被称为轻量进程,是程序执行的最小单元. C++11线程: 我们知道平常谈C++线程相关的东东基本都是基于之后要学习的posix相关的,其实在C++11有自己新式创建线程的方法,所以先来 ...

  9. node源码详解(七) —— 文件异步io、线程池【互斥锁、条件变量、管道、事件对象】

    本作品采用知识共享署名 4.0 国际许可协议进行许可.转载保留声明头部与原文链接https://luzeshu.com/blog/nodesource7 本博客同步在https://cnodejs.o ...

随机推荐

  1. 关于mybatis和spring复合pom的异常

    java.lang.AbstractMethodError: org.mybatis.spring.transaction.SpringManagedTransaction.getTimeout()L ...

  2. 算法Sedgewick第四版-第1章基础-2.1Elementary Sortss-001选择排序法(Selection sort)

    一.介绍 1.算法的时间和空间间复杂度 2.特点 Running time is insensitive to input. The process of finding the smallest i ...

  3. noi.ac day3t2 染色

    传送门 分析 dp[i][j]为考虑前i个位置,[i-j+1,i]中的颜色互不相同,并且ai-j与这段区间中的某一个位置颜色相同 我们枚举第i+1个位置和[i-j+1,i]中的哪一个颜色相同或者全部不 ...

  4. Luogu 2149 [SDOI2009]Elaxia的路线

    感觉这题可以模板化. 听说spfa死了,所以要练堆优化dijkstra. 首先对$x_{1},y_{1},x_{2},y_{2}$各跑一遍最短路,然后扫一遍所有边看看是不是同时在两个点的最短路里面,如 ...

  5. 使用python进行数据转码

    大数据最烦的就是数据质量差,为了把数据导入到sequoiadb中,需要要求文本是UTF-8模式的,使用enca查看文件编码是gb2312,然后是enca转utf-8报错.google了整个地球都不知道 ...

  6. Day Day Up—— ——fseek()函数的用法

    在牛客网遇到的一个程序题中用到了函数fseek()故查阅了一下该函数的功能及用法,整理如下: fseek函数功能是把文件指针指向文件的开头,需要包含头文件stdio.h 功 能: 重定位流上的文件指针 ...

  7. 使用Elasticsearch-jdbc为MySQL数据库建立索引

    elasticsearch-jdbc 环境 Ubuntu 14.04 JDK 1.8.0_66 Elasticsearch 2.3.1 Elasticsearch-jdbc 2.3.1.0 Elast ...

  8. 解决PendingIntent传递参数为空的问题

    PendingIntent pIntent = PendingIntent.getActivity(context, 0, intent,  0); 在接收端,接收的数据一直为null,在google ...

  9. matlab任务:FCM分类

    一个朋友让帮忙做图像分类,用FCM聚类算法,网上查了一下,FCM基本都是对一幅图像进行像素的分类,跟他说的任务不太一样,所要做的是将一个文件夹里的一千多幅图像进行分类.图像大概是这个样子的(是25*2 ...

  10. css css3新特性

    css  css3新特性 一.css3是什么? 我不喜欢把已有的概念从一个地方抄到另一个地方,还是喜欢如下方式. 参考百度百科: http://baike.baidu.com/link?url=z2V ...