首页 > > > 分布式初探——面包店算法与多线程锁


import threadingimport time# 全体线程取的号,存放在公共区域number = [ for _ in range(5)]# 表示线程i是否还没有号码,True表示还没有号码,False表示已经获得号码entering = [False for _ in range(5)]# 获取资源,打印出当前获取资源的线程iddef achieve_resource(num):print('Now is no.{} thread achieve the resource'.format(num))time.sleep(1)def lock(num):# True表示还没有拿到号码entering[num] = True# 取号,等于当前最大号码的下一位number[num] = max(number) + 1# 修改状态,表示已经取过号了entering[num] = Falsefor i in range(5):# 一直等待到i线程取号为止while entering[i]:pass# 如果i线程的号码比自己小,则等待# 如果号码一样,则线程id小的优先while ((number[i] != ) and (number[num], num) < (number[i], i)):pass# 解锁,即将号码销毁def unlock(num):number[num] =# 发起一个抢占资源的请求def start_request(num):while True:lock(num)achieve_resource(num)unlock(num)if __name__ == "__main__":threads = []for i in range(5):t = threading.Thread(target=start_request, args=(i,))t.start()

线程之间保持顺序,并且抢占的顺序不是固定的存在随机性,说明我们的算法生效,起到了效果。但如果了解一点Python当中GIL机制,会发现在Python当中,即使我们起多线程去获取一个资源,一般也不会导致线程不安全。这是因为Python是解释型语言,同一时刻,同一个Python解释执行的进程,只会执行一个线程。就好像Python是run在一个单核的CPU上,我们起再多线程,同时也只能执行一个。
面包店算法本身理解起来并不算难,在操作系统的课本里就有相关内容。反而是它背后涉及到的知识更加困难一些,我们试着简单来了解一下。
首先,面包店算法究竟起到什么作用呢?我们通过加锁也可以实现多个线程抢占引发的问题,相比之下,它有什么特点呢,我们为什么要了解这样一个算法呢?
按照维基百科里的说法是:它最大的特点是可以不需要依赖硬件的原子操作,也就是说可以纯软件实现。
这一句话虽然简单,但是理解起来并不容易。首先,什么是原子操作呢,它和硬件又有什么关联呢?
这里的原子和数据库事务原则里的原子性是一个意思,指的是某个指令或者是某些指令要么不做,要么全做,不允许出现中间状态。我们先来看最简单的单CPU的情况,对于单CPU,一次只能执行一个线程,单条指令的执行天然就是原子的。因为同一时刻只会有一个线程在执行,不会存在抢占的情况。我们只需要保证指令的执行顺序,就可以保证原子操作的正确性。
对于多CPU的场景而言,就无法保证了,因为可能会出现多个线程或进程同时读写同一块内存的情况。这种情况下显然没有办法保证操作的正确性。为了解决这个问题,一个解决方案是加上硬件锁。所谓的硬件锁就是当某个线程读写内存的时候,将总线的电平拉低,从而锁住系统总线,防止其他线程读写内存。

这种方式显然太简单粗暴了,并且极端情况下会丧失大量性能,我们想要通过多线程或多进程来提升系统并发能力的初衷就达不到了。
针对这种情况,又提出了缓存锁。
因为在CPU当中除了内存之外,还会好几块缓存。我们可以将CPU的原子操作挪到CPU中的某一块缓存当中执行,这样我们就只需要锁住某一块缓存就行了,就可以不用锁住整块内存了。
当然由于缓存和内存存在数据交互,所以实际的操作要复杂得多,我们不需要探究那么深入,只需要在原理上了解即可。
上面说的总线索和缓存锁都需要CPU硬件角度提供支持才行,硬件支持的锁相比之下使用起来要复杂一些,而且限制较多,并不是所有系统都能够支持。因此,通过软件实现的锁更加普遍,下面再介绍一种通过软件实现的多线程锁——自旋锁。
自旋锁看着名字新奇,但是含义很简单。意思是在多线程的场景当中,当一个线程无法获取到临界区资源的时候,不是挂起等待,而是会一直保持执行,反复查询锁变量是否可用。也就是说是一种忙等待。
这样做的好处是线程不需要被挂起,可以减少操作系统执行过程当中的上下文切换带来的开销。
在Java当中,自旋锁有一个非常著名的实现算法,叫做CAS(compare and swap)。翻译过来的意思是比较和替换,在Java很多并发场景当中都用到了CAS的技术,也是Java程序员经常问的问题之一。
CAS的原理很简单,我们会对于每一个操作的对象设置一个期望值,当我们从内存中读取到的结果和期望值不一致的时候,我们会自旋再次读取。如果和期望值一致,说明此时其他线程没有造成干扰,那么就进行修改。
我们直接来看Java内部的实现代码:
public final int getAndAddInt(Object o, Long offset, int delta) {int v;do {v = getIntVolatile(o, offset);} while (!compareAndSwapInt(o, offset, v, v+delta));return v;}

来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69966956/viewspace-3169834/ ,如需转载,请注明出处,否则将追究法律责任。