Python 线程间的同步机制1:Lock锁和Semaphore信号量

1.信号量:Semaphore

信号量的本质是一个计数器,某个线程调用信号量的.acquire()方法时,如果当前计数器>0,则计数器-1,某个线程调用.release()方法时,计数器+1。

acquire(blocking = True,timeout=None)

如果当前计数器不为0,acquire()返回True,如果计数器值为0,acquire()会被阻塞。设置blockingFalse,代表不阻塞直接返回False,设置timeout可以指定最大阻塞时间(为了方便阐述,不讨论blockingtimeout参数)。

release()

调用release使计数器+1。BoundedSemphore对象实例化时可指定MAX值,如果计数器值达到MAX,则后续的所有release都被阻塞。

我们可以给想要限制执行的代码块前面加入.acquire()代码,这样就可以限制最多有多少个线程可以执行该代码块,你想要执行,你就得先acquire,什么?计数器为0了?等着吧,等某个线程release之后,计数器不为0,你就可以进去了。

2.最常用的互斥锁:Lock

当一个线程调用Lock锁对象的.acquire方法,如果锁是unlocked状态,该线程会lock锁,如果锁已经是locked状态,则.acquire()会被阻塞。
当一个线程调用锁对象的release方法,如果锁已经是unlocked状态,会报错。如果锁是locked状态,会打开锁。

我们通常会在进入可能发生竞态的临界区前执行锁的acquire方法,在执行完临界区的代码后执行锁的release方法。这样就保证了同时最多只有一个线程执行临界区的代码。

我最开始是通过廖雪峰的python教程入门的,很感谢廖老师的无私奉献,廖老师讲到线程的锁时是这么说的:

如果我们要确保balance计算正确,就要给change_it()上一把锁,当某个线程开始执行change_it()时,我们说,该线程因为获得了锁,因此其他线程不能同时执行change_it(),只能等待,直到锁被释放后,获得该锁以后才能改。由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,不会造成修改的冲突。

应该是方便初学者理解,廖老师使用了“持有该锁”这样的描述,但实际上这样的描述是有问题的。我们这里所说的Lock并不会被某个线程持有,如果说锁被某个线程持有,那么理应只有调用acquire()拿到了锁的那个线程才能释放锁,但实际上,任何线程都可以调用release把锁释放,并不限于那个acquire了锁的线程。

这里的锁对于所有线程是一视同仁的,当某个线程成功acquire()仅仅是把锁从unlocked状态变为locked状态而已。如果不进入临界区的话,其它线程的执行并不受任何影响,但你如果要进入临界区,那你得先执行一个lock.acquire(),如果锁是locked状态,那么不好意思,你被阻塞了。

我们强调过:任何线程都可以调用release把锁释放。当一个线程执行完acquire进入临界区后,锁变为locked态,按理说直到它退出临界区调用release之前,别的线程都无法执行acquire,也就无法进入临界区。此时如果另一个线程有个刁民,它非要调用一个release把锁打开,会有什么后果?某一个本应被阻塞的acquire()就被放行了,临界区里出现了第二个线程,可能会出现竞态。

最后:
1.使用锁的时候要记得release。可以把release放在try...finally 的finally block中,这样保证无论如何都会release。也可以使用with语句,锁对象实现了上下文管理器协议,使用with语句时Lock对象会在其__enter__中调用acquire并且在__exit__中调用release。
如果由于某些异常导致release方法没有调用,其他线程中被acquire阻塞的倒霉蛋就只能永远等下去(死锁)。

2.个人认为Lock和MAX值为1的BoundedSemphore信号量很相似。锁为locked态对应BoundedSemphore计数器为0,此时所有acquire()都被阻塞。任何线程都可以执行release使锁进入unlocked态(计数器+1);
锁为unlocked态对应BoundedSemphore计数器为1,此时调用release会报错。而此时如果调用acquire不会阻塞,但会立刻改变锁的状态为locked态(计数器归零)。

你可能感兴趣的:(学习之路)