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

三种语言的线程实现机制

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

posted @ 2019-07-29 23:03  stackupdown  阅读(355)  评论(0编辑  收藏  举报