• 【操作系统】不同语言的线程实现机制对比及数据库锁问题


    三种语言的线程实现机制

    C/C++

    不同Linux系统下提供的底层线程有好几种,其中POSIX类型的线程最为常用。POSIX线程是pthread开头的一系列C语言API,数据结构包括mutex,condition等。C语言多线程不是必须的,只在一些场合适用,而例如单线程的服务器如redis和nginx也能达到较高的并发量。

    python

    由于python的GIL锁,提供的thread都是实质上单核的多线程,但是在IO密集型应用中仍然可以酌情使用。异步模型的普及也影响了python的特性,至少在3.4+版本,python就有了coroutine,而3.5又多了两个新的关键字async,await,用来代替之前的coroutine装饰器和yield from写法。

    Java

    Java很适合编写网络编程、多线程应用,它的多线程是原生的。基于Java的多线程和NIO,有知名的Netty框架。Java里常见的底层多线程结构包括Thread,Condition,Semaphore,Lock。Java的synchronized也是一种互斥机制的实现,叫做管程(monitor)。而且它实质上是一个可重入锁,即当前线程嵌套使用synchronized的时候不会发生死锁。顺带一提,Spark应用级别也有await和ssc.await的API。类似于Python,由于Node.js原来也是单线程的,吞吐会受到耗时计算的影响,Node.js v10.5.0 发布之前就是这种情况,在这一版本增加了对多线程的支持。

    并发问题模型

    生产者-消费者模型

    生产者-消费者模型,它抽象了两个实体,生产者负责生产多个资源,而消费者会消费生产者生产的资源。例如,在一种优化的日志处理模式中,主线程负责接收数据,而日志线程负责取出缓冲区的数据,写入磁盘。这里就是一个生产者和消费者。一般的,生产者和消费者不能单独存在,因为生产者会遇到一个瓶颈,就是它生产的东西已经占满了空间,不能再生产了,同理消费者也会受到限制。

    生产者-消费者模型的问题解决要用互斥量。

        此外,还有哲学家就餐问题,读者-写者模型

    哲学家就餐问题延伸出信号量(Semaphore)。

    读者-写者模型延伸出读写锁。

    数据库中的加锁问题

    1. 两段锁协议会导致死锁吗?

    两阶段锁协议,整个事务分为两个阶段,前一个阶段为加锁,后一个阶段为解锁。在加锁阶段,事务只能加锁,也可以操作数据,但不能解锁,直到事务释放第一个锁,就进入解锁阶段,此过程中事务只能解锁,也可以操作数据,不能再加锁。两阶段锁协议使得事务具有较高的并发度,因为解锁不必发生在事务结尾。它的不足是没有解决死锁的问题,因为它在加锁阶段没有顺序要求。如两个事务分别申请了A, B锁,接着又申请对方的锁,此时进入死锁状态。

    现代的数据库系统中,预防死锁的方法包括了这一方法:一次封锁法。一次封锁法要求事务必须一次性将所有要使用的数据全部加锁,否则就不能继续执行。因此,一次封锁法遵守两段锁协议,但两段锁并不要求事务必须一次性将所有要使用的数据全部加锁,这一点与一次性封锁不同,这就是遵守两段锁协议仍可能发生死锁的原因所在。 

    两段协议中的死锁:

    A访问S1,S2

    B访问S2,S1

    在A:S1->S2和B:S2->S1的中间重叠的时候会发生死锁。

    解决方法是构造一个树形图(怀疑能不能构造)

    读写锁相容性矩阵

    解决锁粒度的问题

    在进行锁授予判断时,采用如下的相容性矩阵进行判断。

        IS     IX     S     X

    IS     Y     Y    Y    N

    IX     Y     Y    N     N

    S     Y     N     Y     N

    X     N     N     N     N

    2.

    树形协议(Tree Protocol),假设数据项的集合满足一个偏序关系,访问数据项必须按此偏序关系的先后进行。如di->dj,则要想访问dj,必须先访问di。这种偏序关系导出一个有向无环图(DAG),因此称为树形协议。树形协议的规则有:

    树形协议只有独占锁;

    事务T第一次加锁可以对任何数据项进行;

    此后,事务T对数据项Q的加锁前提是持有Q的父亲数据项的锁;

    对数据项的解锁可以随时进行;

    数据项被事务T加锁并解锁之后,就不能再被事务T加锁。

    树形协议的优点是并发度好,因为可以较早地解锁。并且没有死锁,因为其加锁都是顺序进行的。

    缺点是对不需要访问的数据进行不必要的加锁。

    3.多版本机制

    锁是针对集中式数据管理设计的,缺点是降低了事务的并发,并且锁本身有开销。在分布式系统,尤其是读多写少的系统中,采用多版本机制更合适。每个数据项都有多个副本,每个副本都有一个时间戳,根据多版本并发控制协议(MVCC)维护各个版本。([1])

    4.乐观锁与悲观锁

    MVCC又称为乐观锁,它在读取数据项时,不加锁;在更新数据项时,直到最后要提交时,才会加锁。这与CAS(Compare and Swap)的机制很类似,为了提高并发度,它更新数据前,会将数据拷贝一份,进行一系列修改,并且拷贝的同时,会记录当前的版本号(时间戳),当修改完毕,即将提交时,再检查此时的版本号是否与刚才记录的一致,如果不一致,则表明数据项被其他事务修改,当前事务的修改被取消。否则,正式提交修改,并增加版本号。

    与MVCC相对,基于锁的并发控制机制称为悲观锁,因为它认为其他事务修改自己正在使用的数据项的概率很高,因此对数据项加锁以阻塞其他事务的读和写。

    参见:

    不考虑并发的情况下,更新库存代码如下:

        /**
         * 更新库存(不考虑并发)
         * @param productId
         * @return
         */
        public boolean updateStockRaw(Long productId){
            ProductStock product = query("SELECT * FROM tb_product_stock WHERE product_id=#{productId}", productId);
            if (product.getNumber() > 0) {
                int updateCnt = update("UPDATE tb_product_stock SET number=number-1 WHERE product_id=#{productId}", productId);
                if(updateCnt > 0){    //更新库存成功
                    return true;
                }
            }
            return false;
        }

    多线程并发情况下,会存在超卖的可能。

    悲观锁

    /**
         * 更新库存(使用悲观锁)
         * @param productId
         * @return
         */
        public boolean updateStock(Long productId){
            //先锁定商品库存记录
            ProductStock product = query("SELECT * FROM tb_product_stock WHERE product_id=#{productId} FOR UPDATE", productId);
            if (product.getNumber() > 0) {
                int updateCnt = update("UPDATE tb_product_stock SET number=number-1 WHERE product_id=#{productId}", productId);
                if(updateCnt > 0){    //更新库存成功
                    return true;
                }
            }
            return false;
        }

    乐观锁

        /**
         * 下单减库存
         * @param productId
         * @return
         */
        public boolean updateStock(Long productId){
            int updateCnt = 0;
            while (updateCnt == 0) {
                ProductStock product = query("SELECT * FROM tb_product_stock WHERE product_id=#{productId}", productId);
                if (product.getNumber() > 0) {
                    updateCnt = update("UPDATE tb_product_stock SET number=number-1 WHERE product_id=#{productId} AND number=#{number}", productId, product.getNumber());
                    if(updateCnt > 0){    //更新库存成功
                        return true;
                    }
                } else {    //卖完啦
                    return false;
                }
            }
            return false;
        }

    引用自[2].
    5.

    使用sleep的时候虽然不会占用CPU时间,但是它是对线程资源的浪费,因为对线程本身而言,它本来可以用来处理其他的事情,却堵在这个线程这里没有做任何事情

    References:

    [1] 两阶段锁协议 https://www.cnblogs.com/zszmhd/p/3365220.html

    [2] FX_SKY https://www.jianshu.com/p/f5ff017db62a

    [3] 后台学习:分布式与存储 https://zhuanlan.zhihu.com/c_1068210975941074944

  • 相关阅读:
    SpringMVC+bootstrap-fileinput文件上传插件使用入门
    [Java]实现Comparable接口不严谨导致Comparison method violates its general contract!
    2021寒假ACM集训队第一次训练-搜索(一)
    第八届“图灵杯”NEUQ-ACM程序设计竞赛个人赛-热身赛
    2021蓝桥杯第三次训练赛
    2021年蓝桥杯第二次训练赛
    2021年蓝桥杯第一次训练赛
    HDU 1312 Red and Black
    HDU 1010 Tempter of the Bone
    HDU 3500 Fling
  • 原文地址:https://www.cnblogs.com/wangzming/p/11267106.html
Copyright © 2020-2023  润新知