当前位置: 首页 > 科技观察

如何掌握编程世界的锁

时间:2023-03-16 11:46:01 科技观察

1.共享变量惹麻烦我们是一个典型的丛林世界,人口众多,资源匮乏。为了争夺有限的资源,每个人都在自己能跑的CPU时间片里拼命工作,经常为了一个变量的修改被打趴下。100纳秒前,我幸运的占用了CPU,从内存中读取了一个变量x==100,我给它加了1,休息片刻后,我打算把它写回内存,没想到发现:内存中的x变成了102,估计是有什么不协调的线程在我休息的时候也读取并修改了x。有很多好心的帖子在骂我:不要回信!但是回写内存是我的命令,你不让我执行,让我退出?我只能毫不客气的把101写入内存,把不符合我逻辑的值102覆盖掉,这样我就可以执行下一条指令了。你看,单线程的逻辑正确不代表多线程并发运行时的逻辑也可以正确。这种事情经常发生,程序不能一直正确运行,引起人类的强烈不满,小道消息说他们在考虑干掉我们,换编程语言。但是改变编程语言有什么用呢?只要有共享变量,在多线程读写的时候就会出现不一致的情况。除非你取消共享变量,让每个线程只访问一个函数中的局部变量,否则我们每个线程都会有一份这些局部变量的副本,函数结束后它们就会被销毁,所以线程是隔离的,是安全的。消除共享变量说起来容易做起来难。在许多人类使用的语言中,例如C++和Java,大多数共享变量都是对象的字段。如果你想把字段去掉,只留下函数,就没有必要那种存在了,类似于函数式编程,一切都是函数。有时候很羡慕函数式的世界,那种无状态应该是一种很美妙的感觉。2.来抢,既然线程的共享变量无法消除,那就另想办法,线程参议院的家伙们哼了半天,终于公布了一个方案:锁!任何线程,只要你要操作一个共享变量,不好意思,先申请锁,拿到锁读取x的值,修改x的值,将x写回内存,***释放锁,让别人玩。Senate设计的锁很简单,类似于一个boolean变量,booleanlock=false。谁能先把这个变量改成true,就说明已经拿到了锁。兄弟们快来抢吧!当我运行它时,我会检查变量lock是否可以设置为true。如果被其他家伙抢走了(已经成真了),我就在****循环,拼命抢,除非我的时间片到了,逼我让出CPU,但我不会堵,我还在处于就绪状态,等待下一次调度,进入CPU继续抢。看到有人改成false,我赶紧行动,终于抢到了。我迅速将锁更改为true。这把锁现在是我的了。赶紧去干活,干完活记得把lock改成false,让其他小伙伴去抢。我想正是因为这个***循环的特性,参议院才将其命名为“自旋锁”!各位评委,大家可能已经想到了,假设有两个线程,都读lock==False,把锁改成true,那么谁拥有锁呢?参议院的领导们已经考虑过这个问题了,他们已经和操作系统讨论过(听说也有硬件)。这个检查lockis是否为false,而把lock设置为true的操作实际上是合并的,调用test_and_set(lock),操作系统郑重承诺,这是一个不可分割的原子操作,当这个test_and_set被执行时,总线被锁定,othersMemory无法访问,即使多个CPU在执行,也不会乱七八糟。有兴趣的可以看看下面的实现,否则直接忽略跳过:3.改进有了自旋锁,至少可以保证程序的正确运行,大家玩的很开心。有一天我遇到了一个递归函数。很喜欢递归,因为逻辑简单,只要递归层次不要太深,栈溢出就好。这个递归函数需要获取一个自旋锁,做一些事情,然后继续调用自己,类似这样:我第一次调用doSomething,获取一个自旋锁,然后第二次调用doSomething,同样获取一个自旋锁,但是第一次调用的时候这个锁已经持有,现在只需要等待第二次调用!这就尴尬了,我进退不得,我把自己弄进了僵局!看这个自旋锁虽然可以实现互斥访问,但是不能重入同一个函数(简称不可重入)!很快把这个问题反馈给参议院,很快修改方案就下来了:每次成功申请锁后,都要记录谁申请了,还要用一个计数器记录重入的次数。下次持有锁的人再次申请锁时,它只是在计数器上加一个。释放时也是如此,计数器减1,等于0则真正释放锁。重入是这样解决的,但是让那么多线程拼命往那儿抢也不是办法,空CPU的消耗也是巨大的浪费。于是参议院发布了一把新锁,ReentrantLock。这个锁可以重入。如果你不能抓住它,就不要进入循环。就乖乖的在等待队列里等着,别人释放锁的时候通知你。抢。(在Java中,最初是synchronized关键字,可以用在方法或代码块上,后来改进为更灵活的ReentrantLock。)很快,有线程抱怨我是第一个申请锁的.,为什么隔壁的老王先拿到锁呢?好吧,我必须添加一个是否公平的参数。另一个线程说,我不耐烦了。申请锁的时候只想等5秒。如果我在5秒内没有拿到锁,我就放弃。你能支持吗?然后添加另一个参数:等待时间。4.发扬锁带来的甜蜜的一般体验,各种需求接踵而至:(1)有时候做一件事情需要多个线程获取同一个锁,那怎么办?没关系,signal在创建信号量时,必须指定一个整数(比如10),表示最多10个线程同时获取锁:Semaphorelock=newSemaphore(10);当然,每个线程都需要调用lock。aquire(),lock.release()应用/释放锁。(2)一个线程要写共享变量,但是同时有几个线程要读共享变量,怎么办?写的时候可以加锁,不能读的时候只允许一个线程?你不得不使用一个读写锁ReadWriteLock,为了保证可重入,参很贴心地实现了ReentrantReadWriteLock。(3)一个线程需要等待其他线程完成后才能工作。我应该怎么办?CountDownLatch来救援并创建一个计数器。当一个线程完成时,它从计数器中减去1。如果计数器为0,则一直耐心等待的线程才能启动。(4)还有几个线程必须互相等待,就像百米赛跑,大家准备开门放水,不,是开始,所以奖励一个CyclicBarrier.【本文为专栏作家“刘欣”原创稿件,转载请通过作者微信获取授权公众号coderising】点此查看该作者更多好文