GO语言临界资源安全问题的深入理解

目录
  • 一、临界资源
  • 二、临界资源安全问题
  • 三、临界资源安全问题的解决
  • 四、写在最后

一、临界资源

临界资源: 指并发环境中多个进程/线程/协程共享的资源。

但是在并发编程中对临界资源的处理不当, 往往会导致数据不一致的问题。

示例代码:

package main
​
import (
    "fmt"
    "time"
)
​
func main()  {
    a := 1
    go func() {
        a = 2
        fmt.Println("子goroutine。。",a)
    }()
    a = 3
    time.Sleep(1)
    fmt.Println("main goroutine。。",a)
}

我们通过终端命令来执行:

能够发现一处被多个goroutine共享的数据。

二、临界资源安全问题

并发本身并不复杂,但是因为有了资源竞争的问题,就使得我们开发出好的并发程序变得复杂起来,因为会引起很多莫名其妙的问题。

如果多个goroutine在访问同一个数据资源的时候,其中一个线程修改了数据,那么这个数值就被修改了,对于其他的goroutine来讲,这个数值可能是不对的。

举个例子,我们通过并发来实现火车站售票这个程序。一共有100张票,4个售票口同时出售。

我们先来看一下示例代码:

package main
​
import (
    "fmt"
    "math/rand"
    "time"
)
​
//全局变量
var ticket = 10 // 100张票
​
func main() {
    /*
    4个goroutine,模拟4个售票口,4个子程序操作同一个共享数据。
     */
    go saleTickets("售票口1") // g1,100
    go saleTickets("售票口2") // g2,100
    go saleTickets("售票口3") //g3,100
    go saleTickets("售票口4") //g4,100
​
    time.Sleep(5*time.Second)
}
​
func saleTickets(name string) {
    rand.Seed(time.Now().UnixNano())
    //for i:=1;i<=100;i++{
    //  fmt.Println(name,"售出:",i)
    //}
    for { //ticket=1
        if ticket > 0 { //g1,g3,g2,g4
            //睡眠
            time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
            // g1 ,g3, g2,g4
            fmt.Println(name, "售出:", ticket)  // 1 , 0, -1 , -2
            ticket--   //0 , -1 ,-2 , -3
        } else {
            fmt.Println(name,"售罄,没有票了。。")
            break
        }
    }
}
​

我们为了更好的观察临界资源问题,每个goroutine先睡眠一个随机数,然后再售票,我们发现程序的运行结果,还可以卖出编号为负数的票。

分析:

我们的卖票逻辑是先判断票数的编号是否为负数,如果大于0,然后我们就进行卖票,只不过在卖票钱先睡眠,然后再卖,假如说此时已经卖票到只剩最后1张了,某一个goroutine持有了CPU的时间片,那么它再片段是否有票的时候,条件是成立的,所以它可以卖票编号为1的最后一张票。但是因为它在卖之前,先睡眠了,那么其他的goroutine就会持有CPU的时间片,而此时这张票还没有被卖出,那么第二个goroutine再判断是否有票的时候,条件也是成立的,那么它可以卖出这张票,然而它也进入了睡眠。。其他的第三个第四个goroutine都是这样的逻辑,当某个goroutine醒来的时候,不会再判断是否有票,而是直接售出,这样就卖出最后一张票了,然而其他的goroutine醒来的时候,就会陆续卖出了第0张,-1张,-2张。

这就是临界资源的不安全问题。某一个goroutine在访问某个数据资源的时候,按照数值,已经判断好了条件,然后又被其他的goroutine抢占了资源,并修改了数值,等这个goroutine再继续访问这个数据的时候,数值已经不对了。

三、临界资源安全问题的解决

要想解决临界资源安全的问题,很多编程语言的解决方案都是同步。通过上锁的方式,某一时间段,只能允许一个goroutine来访问这个共享数据,当前goroutine访问完毕,解锁后,其他的goroutine才能来访问。

我们可以借助于sync包下的锁操作。

示例代码:

package main
​
import (
    "fmt"
    "math/rand"
    "time"
    "sync"
)
​
//全局变量
var ticket = 10 // 100张票
​
var wg sync.WaitGroup
var matex sync.Mutex // 创建锁头
​
func main() {
    /*
    4个goroutine,模拟4个售票口,4个子程序操作同一个共享数据。
     */
    wg.Add(4)
    go saleTickets("售票口1") // g1,100
    go saleTickets("售票口2") // g2,100
    go saleTickets("售票口3") //g3,100
    go saleTickets("售票口4") //g4,100
    wg.Wait()              // main要等待。。。
​
    //time.Sleep(5*time.Second)
}
​
func saleTickets(name string) {
    rand.Seed(time.Now().UnixNano())
    defer wg.Done()
    //for i:=1;i<=100;i++{
    //  fmt.Println(name,"售出:",i)
    //}
    for { //ticket=1
        matex.Lock()
        if ticket > 0 { //g1,g3,g2,g4
            //睡眠
            time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
            // g1 ,g3, g2,g4
            fmt.Println(name, "售出:", ticket) // 1 , 0, -1 , -2
            ticket--                         //0 , -1 ,-2 , -3
        } else {
            matex.Unlock() //解锁
            fmt.Println(name, "售罄,没有票了。。")
            break
        }
        matex.Unlock() //解锁
    }
}
​

运行结果:

四、写在最后

在Go的并发编程中有一句很经典的话:不要以共享内存的方式去通信,而要以通信的方式去共享内存。

在Go语言中并不鼓励用锁保护共享状态的方式在不同的Goroutine中分享信息(以共享内存的方式去通信)。而是鼓励通过channel将共享状态或共享状态的变化在各个Goroutine之间传递(以通信的方式去共享内存),这样同样能像用锁一样保证在同一的时间只有一个Goroutine访问共享状态。

当然,在主流的编程语言中为了保证多线程之间共享数据安全性和一致性,都会提供一套基本的同步工具集,如锁,条件变量,原子操作等等。Go语言标准库也毫不意外的提供了这些同步机制,使用方式也和其他语言也差不多。

到此这篇关于GO语言临界资源安全问题的深入理解的文章就介绍到这了,更多相关GO语言临界资源安全 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详解go语言中并发安全和锁问题

    首先可以先看看这篇文章,对锁有些了解 [锁]详解区分 互斥锁.⾃旋锁.读写锁.乐观锁.悲观锁 Mutex-互斥锁 Mutex 的实现主要借助了 CAS 指令 + 自旋 + 信号量 数据结构: type Mutex struct { state int32 sema uint32 } 上述两个加起来只占 8 字节空间的结构体表示了 Go语言中的互斥锁 状态: 在默认情况下,互斥锁的所有状态位都是 0,int32 中的不同位分别表示了不同的状态: 1位表示是否被锁定 1位表示是否有协程已经被唤醒 1

  • 浅析MongoDB之安全认证

    一.MongoDB的用户和角色权限简介 为了强制开启用户访问控制(用户验证),则需要在MongoDB实例启动时使用选项--auth或在指定启动配置文件中添加auth=true. 启用访问控制:MongoDB使用的是基于角色的访问控制(Role-Based Access Control,RBAC)来管理用户对实例的访问.通过对用户授予一个或多个角色来控制用户访问数据库资源的权限和数据库操作的权限,在对用户分配角色之前,用户无法访问实例. 角色:在MongoDB中通过角色对用户授予相应数据库资源的操

  • 解析golang中的并发安全和锁问题

    1. 并发安全 package main import ( "fmt" "sync" ) var ( sum int wg sync.WaitGroup ) func test() { for i := 0; i < 5000000; i++ { sum += 1 } wg.Done() } func main() { // 并发和安全锁 wg.Add(2) go test() go test() wg.Wait() fmt.Println(sum) } 上面

  • golang线程安全的map实现

    网上找的协程安全的map都是用互斥锁或者读写锁实现的,这里用单个协程来实现下,即所有的增删查改操作都集成到一个goroutine中,这样肯定不会出现多线程并发访问的问题. 基本思路是后台启动一个长期运行的goroutine,阻塞的接受自己channel中的请求req,req分为不同的请求,比如读key,写key等,然后在这个goroutine中进行各种操作. 例: Get方法向readSig(channel)中发送一条请求.请求是readReq的指针,当run方法接收到信号时,读取底层map,将

  • GO语言临界资源安全问题的深入理解

    目录 一.临界资源 二.临界资源安全问题 三.临界资源安全问题的解决 四.写在最后 一.临界资源 临界资源: 指并发环境中多个进程/线程/协程共享的资源. 但是在并发编程中对临界资源的处理不当, 往往会导致数据不一致的问题. 示例代码: package main ​ import ( "fmt" "time" ) ​ func main() { a := 1 go func() { a = 2 fmt.Println("子goroutine.."

  • C语言编程计算信噪比SNR理解学习

    目录 概念 计算方法 相关认知 Taprint中的信噪比 实例 概念 这里面的信号指的是来自设备外部需要通过这台设备进行处理的电子信号,噪声是指经过该设备后产生的原信号中并不存在的无规则的额外信号(或信息),并且该种信号并不随原信号的变化而变化. 计算方法 信噪比的计量单位是dB,其计算方法是10lg(Ps/Pn),其中Ps和Pn分别代表信号与噪声的有效功率,也可以换算成电压幅值的比率关系:20Lg(Vs/Vn),Vs和Vn分别代表信号和噪声电压的"有效值". 在音频放大器中,我们希望

  • R语言编程数学分析重读微积分理解极限算法

    目录 1 状态变化 2 极限语言 3 序列与函数 4 极限常数 圆周率 π 自然对数e 5 洛必达法则 1 状态变化 若将数学整体划分为三类,则可概括为代数.几何与分析.对于前两者,我们很早就建立了直观的概念,对于空间结构及其性质的研究,即为几何:以数为核心的研究领域,即为代数. 而分析则具备更多的非数学的内涵,所以初学者往往难以看透数学分析所指向的数学本质,如果望文生义,会更倾向于将"分析"理解为一门数学技巧,而非数学领域. 我们最先接触数学分析时,是将其等同为微积分的.可以认为微积

  • C语言特殊符号的补充理解

    续接符 反斜杠"",他有两种最常见的功能,一就是续航功能,二就是我们提到反斜杠就会很敏感的转义字符. if("1==a&&2==b&&3==c) { printf("hello\n"); } 有以上的代码我们可以等价于 if("1==a&&\ 2==b&&\ 3==c) { printf("hello\n"); } 这里反斜线就起到了一个连接上下两行的代码,在咱看

  • C语言利用面试真题理解指针的使用

    目录 前言 笔试题一 笔试题二 笔试题三 笔试题四 笔试题五 笔试题六 笔试题七 笔试题八 前言 大家好~我又来了,今天给大家带来的是指针的几道笔试题,希望能够加强大家对指针知识的把握,指针就应该这样学! 笔试题一 #include<stdio.h> int main() { int a[5] = { 1 , 2 , 3 , 4 , 5 }; int* ptr = (int*) (&a + 1); printf("%d, %d", *(a + 1), *(ptr -

  • C语言结构体的一些理解

    前言 最忙碌的一周已然结束,疲惫之余想和大家聊一聊对C语言中结构体的认识.水平不足,如有问题请大家指正. 一.对结构体的理解 我们知道C语言中有基本数据类型: 1.整(数)型: int:基本整型,用于存储整数,占4个字节,默认值为0 short:短整型,占2个字节 long:长整型,占4个字节 long long:双长整型,占8个字节,比较少用 2.浮点型 float:单精度浮点型,占4个字节 double:双精度浮点型,占8个字节 3.字符型 char:字符型,用于存储单字符,占1个字节. (

  • 关于c语言中回调函数的理解

    前言 在计算机程序设计中,回调函数,或简称回调,是指通过函数参数传递到其它代码的,某一块可执行代码的引用.这一设计允许了底层代码调用在高层定义的子程序. 这段话不是那么好理解,不同语言实现回调的方式有些许不同.其实可以这样理解,回调就是在一个函数中调用另外一个函数. 在c语言中,回调是使用函数指针来实现的. 函数指针--顾名思义,是指向一个函数的指针.通常函数指针有两个方面的用途,一个是转换表(jump table),另一个是作为参数传递给一个函数. 下面是两个函数指针的声明 int(*f)(i

  • C语言 自定义类型全面系统理解

    目录 一.结构体 1.结构体的声明 局部结构体变量 全局结构体变量 2.特殊声明 3.结构体的自引用 4.结构体变量的初始化 5.结构体内存对齐  6.修改默认对齐数 7.结构体传参 传址调用原因: 二.位段 举例: 分析: 跨平台问题: 三.枚举 枚举类型的定义: 枚举的优点  四.联合 1.联合类型的定义 2.联合的特点  使用案例: 分析: 3.联合大小的计算  举例: 分析: 一.结构体 结构体是不同类型变量的集合体 1.结构体的声明 struct Book { char name[20

  • 基于C语言sprintf函数的深入理解

    printf 可能是许多程序员在开始学习C语言时接触到的 第二个函数(我猜第一个是main),说起来,自然是老朋友了,可是,你对这个老朋友了解多吗?你对它的那个孪生兄弟sprintf了解多吗?在将各种类 型的数据构造成字符串时,sprintf的强大功能很少会让你失望.由于sprintf跟printf在用法上几乎一样,只是打印的目的地不同而已,前者打印到字符串中,后者则直接在命令行上输出.这也导致sprintf比printf有用得多.所以本文着重介绍sprintf,有时也穿插着用用 pritnf.

  • 深入理解javascript中的立即执行函数(function(){…})()

    javascript和其他编程语言相比比较随意,所以javascript代码中充满各种奇葩的写法,有时雾里看花,当然,能理解各型各色的写法也是对javascript语言特性更进一步的深入理解. ( function(){-} )()和( function (){-} () )是两种javascript立即执行函数的常见写法,最初我以为是一个括号包裹匿名函数,再在后面加个括号调用函数,最后达到函数定义后立即执行的目的,后来发现加括号的原因并非如此.要理解立即执行函数,需要先理解一些函数的基本概念.

随机推荐