lock-free和wait-free算法是指对于共享的数据并非对其加锁来控制访问,而是多个线程并行的访问。
通过该算法可以达到对共享对象并发的读写而不会破坏对象本身。
所谓lock-free是指对于线程不加锁,让系统执行所有的步骤。lock-free提到的不加锁是指不使用类似于互斥锁或者信号量之类的排他机制。
因为一旦对线程加锁的话,当线程执行中断时,那么对于这个系统来说运行也中断了。
所谓wait-free是指,不管其他线程执行什么操作,线程无论有什么操作都能在有限的步骤里面完成。
所以对于算法来说达到lock-free不一定能达到wait-free,但是达到wait-free的算法一定是lock-free的。
简述
在多线程编程中,对于共享资源的访问最传统的做法就是加锁。互斥锁和信号量本质上都是在代码层面的某一段逻辑上加上排他机制,从而达到对于共享资源的访问不造成破坏性的结果。假如某个线程需要获得已经被其他线程先占有的锁,那么在那个锁释放之前,这个线程的工作会陷入停止状态。
很多情况下,我们都不希望看到线程的运行停止。首先,阻塞中的线程无法做任何事情。其次,如果线程要处理的事务优先级很高乃至要实时处理的话,我们也不希望看到线程被阻塞。再者,当多个资源被锁的时候,就容易出现死锁、活锁或者优先顺序颠倒等问题。最后,使用锁的地方,如果对加锁的逻辑颗粒度很大的话会导致并行处理的机会会减少,如果加锁颗粒度太细又容易产生bug而不得不小心设计,最后陷入死胡同。
wait-free的数据结构
使用wait-free的数据结构的应用程序中,与其将原来使用互斥的算法改造为wait-free的算法,不如直接使用基于wait-free算法开发的stack、queue、set和map。例如,在Java 5以后,java.util.concurrent包中就引入了wait-free的数据结构。通过直接使用这些wait-free的数据结构,编写线程的异步数据访问也将变得很容易。
案例
例如,在银行的柜台有个存钱的程序。每个线程相当于一个ATM。当金钱存入的时候,需要将当前余额读出来,然后加上要存入的金额算出新的余额。如果通过锁来实现的话,当一台ATM在计算的时候,为了让其他ATM不能同时变更余额,需要加锁。
否则的话,如果同时更新将导致数据错误。如果通过lock-free来实现的话,需要一个管理所有存入请求的独立线程,然后创建一个wait-free的队列。
所有ATM异步的将存入金额的请求放入队列中而无需加任何的锁。管理所有存入请求的独立线程从队列中依次取出请求,更新账户余额。
通过以上方式,无需单独实现lock-free的存钱算法,编程也更加便捷。同时,该方法因为队列是wait-free的,所以不仅仅实现了lock-free也实现了wait-free。对于余额的更新如果需要N并发的话,只需要创建N个wait-free的队列,然后根据账号对N取余放入对应队列中即可。
CAS Compare and Swap
在实现lock-free和wait-free算法时,需要CPU专用的管理指令来完成。其中最重要的就是Compare and Swap(简称CAS)。在Java中,在java.util.concurrent.atomic包中类方法compareAndSet来实现。其中使用到了内存地址、旧值和新值。
如果该地址所保存的值和旧值相同则替换为新值,如果不是的话则什么都不做。然后将处理成功与否的结果返回。所以需要CPU支持该方法。目前Intel的芯片中有该功能。通过该功能实现了从内存中读出数据,进行更新后写入的时候其他线程无法同时更新的算法。
继续拿前面的银行柜台来举例,我们换个算法来实现。ATM将当前余额读出、计算再写入这三个步骤可以类比为CPU的CAP操作。
进行这三个步骤的时候,没有其他线程更新该值的话则认为操作成功。如果在进行这三步的时候,第一步读出的金额和第三步更新要用新值去替换的金额不一致的情况下,命令直接失败,然后操作回滚到第一步重新执行。
所有的ATM都是遵循这个方法,在成功之前都反复执行这三步。这个算法是lock-free的但是不是wait-free的。因为当其他ATM进行操作的时候,会影响当前ATM的操作,导致可能要反复执行步骤。