zoukankan      html  css  js  c++  java
  • 并发安全

    并发安全

    何为并发安全,就是多个并发体在同一段时间内访问同一个共享数据,共享数据能被正确处理。

    并发不安全的后果

    并发不安全最典型的案例就是卖票超售,设想有一家电影院,有两个售票窗口,售票员售票时候先看一下当前剩余票数是否大于0,如果大于0则售出票。
    用伪代码就是如下:

    # 售票操作(一张票)
    # 如果票数大于0
    totalNum = getTotalNum()
    if totalNum > 0
        # 则售出一张票
        totalNum = totalNum - 1
    else
        failedToSold()
    

    看上去也没有什么问题,流程图如下:
    jpg
    此时票数剩下一张票,两个售票窗口同时来了顾客,两个售票人都看了一下剩余票数还有一张,不约而同地收下顾客的钱,余票还剩一张,但是却售出了两张票,就会出现致命的问题。
    jpg

    如何做到并发安全

    目前最最主流的办法就是加锁就行操作,其实售票的整个操作同时间内只能一个人进行,在我看来归根到底加锁其实就是让查询和售票两个步骤原子化,只能一块执行,不能被其他程序中断,让这步操作变成串行化。下面就介绍一下使查询和售票原子化的常见程序操作:

    锁的做法就是每次进入这段变量共享的程序片段,都要先获取一下锁,如果获取成功则可以继续执行,如果获取失败则阻塞,直到其他并发体把锁给释放,程序得到执行调度才可以执行下去。
    锁本质上就是让并发体创建一个程序临界区,临界区一次只能进去一个并发体,伪代码示意如下:

    lock()
    totalNum = getTotalNum()
    if totalNum > 0
        # 则售出一张票
        totalNum = totalNum - 1
    else
        failedToSold()
    unlock()
    

    顺带一提的是锁可以分为写锁与排它锁,一般如无特殊说明,一般锁都是指写锁。

    读锁与写锁

    读锁也叫共享锁,写锁也叫排它锁,锁的概念被发明了之后,人们就想着如果我很多个并发体大部分时间都是读,如果就把变量读取的时候也要建立临界区,那就有点太大题小做了。于是人们发明了读锁,一个临界区如果加上了读锁,其他并发体执行到相同的临界区都可以加上读锁,执行下去,但不能加上写锁。这样就保证了可以多个并发体并发读取而又不会互相干扰。

    队列

    队列也是解决并发不安全的做法。多个并发体去获取队列里的元素,然后进行处理,这种做法和上锁其实大同小异,本质都是把并发的操作串行化,同一个数据同一个时刻只能交给一个并发体去处理。
    伪代码:

    # 第一个获取到队列的元素就可以进行下去
    isCanSold = canSoldList.pop()
    totalNum = getTotalNum()
    if totalNum > 0
        # 则售出一张票
        totalNum = totalNum - 1
    else
        failedToSold()
    

    CAS

    CAS(compare and swap),先比对,然后再进行交换,和数据库里的乐观锁的做法很相似。

    乐观锁

    数据库里的乐观锁并不是真的使用了锁的机制,而是一种程序的实现思路。
    乐观锁的想法是,每次拿取数据再去修改的时候很乐观,认为其他人不会去修改这个数据,表另外维护一个额外版本号的字段。
    查数据的时候记录下该数据的版本号,如果成功修改的话,会修改该数据的版本号,如果修改的时候版本号和查询的时候版本号不一致,则认为数据已经被修改过,会重新尝试查询再次操作。
    设我们表有一个user表,除了必要的字段,还有一个字段version,表如下:

    id username money version
    1 a 10 100
    2 b 20 100

    这时候我们需要修改a的余额-10元,执行事务语句如下:

    while
        select @money = money, @version = version from user where username = a;
        if @money < 10
            print('余额成功')
            break
        # 扣费前的预操作
        paied()
        # 实行扣费
        update user set money = money - 10, version = version + 1 where username = a and version = @version
        # 影响条数等于1,证明执行成功
        if @@ROWCOUNT == 1
            print('扣费成功')
            break
        else
            rollback
            print('扣费失败,重新进行尝试')
        
    

    乐观锁的做法就是使用版本的形式,每次写数据的时候会比对一下最开始的版本号,如果不同则证明有问题。
    CAS的做法也是一样的,在代码里面的实现稍有一点不同,由于SQL每条语句都是原子性,查询对应版本号的数据再更新的这个条件是原子性的。

    update user set money = money - 10, version = version + 1 where username = a and version = @version
    

    但是在代码里面两条查询和赋值两个语句不是原子性的,需要有特定的函数让cpu底层把两个操作变成一个原子操作,在go里面有atomic包支持实现,是这样实现的:

    for {
        user := getUserByName(A)
        version := user.version
        paied()
        if atomic.CompareAndSwapInt32(&user.version, version, version + 1) {
            user.money -= 10
        } else {
            rollback()
        }
    }
    

    atomic.CompareAndSwapInt32需要依次传入要比较变量的地址,旧变量的值,修改后变量的值,函数会判断旧变量的值是否与现在变量的地址是否相同,相同则把新变量的值写入到该变量。
    CAS的好处是不需要程序去创建临界区,而是让CPU去把两个指令变成原子性操作,性能更好,但是如果变量会被频繁更改的话,重试的次数变多反而会使得效率不如加锁高。

  • 相关阅读:
    dotnet 控制台读写 Sqlite 提示 no such table 找不到文件
    dotnet 控制台读写 Sqlite 提示 no such table 找不到文件
    dotnet 控制台 Hangfire 后台定时任务
    dotnet 控制台 Hangfire 后台定时任务
    dotnet 获取指定进程的输入命令行
    dotnet 获取指定进程的输入命令行
    PHP sqrt() 函数
    PHP sinh() 函数
    PHP sin() 函数
    PHP round() 函数
  • 原文地址:https://www.cnblogs.com/s-b-b/p/9008041.html
Copyright © 2011-2022 走看看