并发编程——线程——锁
并发编程中避免不了在同一时间对同一数据的更改,因此,对锁的使用变得尤为重要,什么时间、什么场景该用什么类型的锁都是有讲究的,接下来介绍几种常见的锁。
死锁现象
问题产生需求,在学新的锁之前先来看看我们学的互斥锁有什么缺点。
所谓死锁,就是指两个或两个以上的进程或线程在执行过程中因争夺资源而造成的一种互相等待的现象,若无外力的作用,它们都将无法推进下去。
此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程或线程称为死锁进程或死锁线程。
来看一个简单的例子:
import time
from threading import Thread, Lock
mutexA = Lock()
mutexB = Lock()
class MyThread(Thread):
def run(self):
self.func1()
self.func2()
def func1(self):
mutexA.acquire()
print('%s 拿到A锁' % self.name)
mutexB.acquire()
print('%s 拿到B锁' % self.name)
mutexB.release()
print('%s 释放B锁' % self.name)
mutexA.release()
print('%s 释放A锁' % self.name)
def func2(self):
mutexB.acquire()
print('%s 拿到B锁' % self.name)
time.sleep(2)
mutexA.acquire()
print('%s 拿到A锁' % self.name)
mutexA.release()
print('%s 释放A锁' % self.name)
mutexB.release()
print('%s 释放B锁' % self.name)
if __name__ == '__main__':
for i in range(10):
thread = MyThread()
thread.start()
输出结果为:
Thread-1 拿到A锁
Thread-1 拿到B锁
Thread-1 释放B锁
Thread-1 释放A锁
Thread-1 拿到B锁
Thread-2 拿到A锁
第一个启动的线程因为比其它九个线程快一点,所以在执行func1函数的时候会先拿到A锁,紧接着拿到B锁,然后释放B锁,再之后释放A锁,此时线程2会拿到线程1释放的A锁,线程1会拿到func2中的B锁,此时,线程1需要线程2手中的A锁,线程2需要线程1手中的B锁,两个谁也不让谁,造成了死锁。
递归锁
在python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。
先来看一下RLock的源码:
class _RLock:
"""This class implements reentrant lock objects.
这个类实现可重入锁对象。
A reentrant lock must be released by the thread that acquired it.
可重入锁必须由获取它的线程释放。
Once a thread has acquired a reentrant lock, the same thread may acquire it again without blocking; the thread must release it once for each time it has acquired it.
一旦一个线程获得了一个可重入锁,同一个线程就可以在不阻塞的情况下再次获得它;线程每次获得它时必须释放它一次。
"""
def __init__(self):
self._block = _allocate_lock()
self._owner = None
self._count = 0
RLock内部维护着一个Lock和一个count变量,count记录了acquire的次数,从而使得资源可以被多次acquire,直到一个线程所有的acquire都被release,其他线程才能获得资源。
上面的例子如果使用RLock代替Lock,则不会发生死锁,二者的区别是:递归锁可以连续acquire多次,而互斥锁只能acquire一次。
import time
from threading import RLock
from threading import Thread
mutexA = mutexB = RLock()
class MyThread(Thread):
def run(self) -> None:
self.function1()
self.function2()
def function1(self):
mutexA.acquire()
print(self.name, "acquire mutex A.")
mutexB.acquire()
print(self.name, "acquire mutex B.")
mutexB.release()
print(self.name, "release mutex B.")
mutexA.release()
print(self.name, "release mutex A.")
def function2(self):
mutexB.acquire()
print(self.name, "acquire mutex B.")
time.sleep(1)
mutexA.acquire()
print(self.name, "acquire mutex A.")
mutexA.release()
print(self.name, "release mutex A.")
mutexB.release()
print(self.name, "release mutex B.")
if __name__ == '__main__':
for _ in range(5):
thread = MyThread()
thread.start()
输出结果为:
Thread-1 acquire mutex A.
Thread-1 acquire mutex B.
Thread-1 release mutex B.
Thread-1 release mutex A.
Thread-1 acquire mutex B.
Thread-1 acquire mutex A.
Thread-1 release mutex A.
Thread-1 release mutex B.
Thread-2 acquire mutex A.
Thread-2 acquire mutex B.
Thread-2 release mutex B.
Thread-2 release mutex A.
Thread-3 acquire mutex A.
Thread-3 acquire mutex B.
Thread-3 release mutex B.
Thread-3 release mutex A.
Thread-4 acquire mutex A.
Thread-4 acquire mutex B.
Thread-4 release mutex B.
Thread-4 release mutex A.
Thread-4 acquire mutex B.
Thread-4 acquire mutex A.
Thread-4 release mutex A.
Thread-4 release mutex B.
Thread-2 acquire mutex B.
Thread-2 acquire mutex A.
Thread-2 release mutex A.
Thread-2 release mutex B.
Thread-3 acquire mutex B.
Thread-3 acquire mutex A.
Thread-3 release mutex A.
Thread-3 release mutex B.
Thread-5 acquire mutex A.
Thread-5 acquire mutex B.
Thread-5 release mutex B.
Thread-5 release mutex A.
Thread-5 acquire mutex B.
Thread-5 acquire mutex A.
Thread-5 release mutex A.
Thread-5 release mutex B.
Process finished with exit code 0
信号量
信号量也是一把锁,可以指定信号量,对比互斥锁同一时间只能有一个任务抢到锁去执行,信号量同一时间可以有多个任务拿到锁去执行。
来看一下信号量的源码:
class Semaphore:
"""This class implements semaphore objects.
这个类实现信号量对象。
Semaphores manage a counter representing the number of release() calls minus the number of acquire() calls, plus an initial value.
信号量管理表示release()调用数减去acquire()调用数加上初始值的计数器。
The acquire() method blocks if necessary until it can return without making the counter negative.
如果需要,acquire()方法将阻塞,直到它可以返回而不使计数器为负为止。
If not given, value defaults to 1.
如果未给定,则默认值为1。
"""
# After Tim Peters' semaphore class, but not quite the same (no maximum)
# 在Tim Peters的信号量类之后,但不完全相同(没有最大值)
def __init__(self, value=1):
if value < 0:
raise ValueError("semaphore initial value must be >= 0")
self._cond = Condition(Lock())
self._value = value
如果说互斥锁是合租房屋的人去抢一个卫生间,那么信号量就相当于一群路人争抢公共厕所,公共厕所有多个位置,这意味着同一时间可以有多个人上公共厕所,但公共厕所容纳的人数是一定的,这便是信号量的大小。
import time
import random
import threading
from threading import Thread
from threading import Semaphore
def function():
sp.acquire()
print(threading.currentThread().getName(), "get Semaphore.")
time.sleep(random.randint(1, 3))
sp.release()
if __name__ == '__main__':
sp = Semaphore(5)
for _ in range(15):
thread = Thread(target=function)
thread.start()
输出结果为:
Thread-1 get Semaphore.
Thread-2 get Semaphore.
Thread-3 get Semaphore.
Thread-4 get Semaphore.
Thread-5 get Semaphore.
Thread-7 get Semaphore.
Thread-6 get Semaphore.
Thread-8 get Semaphore.
Thread-9 get Semaphore.
Thread-10 get Semaphore.
Thread-11 get Semaphore.
Thread-12 get Semaphore.
Thread-13 get Semaphore.
Thread-14 get Semaphore.
Thread-15 get Semaphore.
Semaphore管理一个内置的计数器,每当调用acquire()时内置计数器-1,调用release() 时内置计数器+1,当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。
Event
线程的一个关键特性是每个线程都是独立运行且状态不可预测。
如果程序中的其他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。
为了解决这些问题,就需要使用threading库中的Event对象。
老规矩,还是先来看一下源码:
class Event:
"""Class implementing event objects.
类实现事件对象。
Events manage a flag that can be set to true with the set() method and reset to false with the clear() method.
事件管理一个标志,这个标志可以用set()方法设置为true,用clear()方法重置为false。
The wait() method blocks until the flag is true.
wait()方法阻塞直到标志为true。
The flag is initially false.
标志最初为false。
"""
# After Tim Peters' event class (without is_posted())
# 在Tim Peters的事件类之后(没有is_posted())
def __init__(self):
self._cond = Condition(Lock())
self._flag = False
def _reset_internal_locks(self):
# private! called by Thread._reset_internal_locks by _after_fork()
self._cond.__init__(Lock())
def is_set(self):
"""Return true if and only if the internal flag is true."""
# 当且仅当内部标志为true时返回true。
return self._flag
isSet = is_set
def set(self):
"""Set the internal flag to true.
将内部标志设置为true。
All threads waiting for it to become true are awakened.
所有等待它成真的线程都被唤醒。
Threads that call wait() once the flag is true will not block at all.
一旦标志为true,调用wait()的线程就不会阻塞。
"""
with self._cond:
self._flag = True
self._cond.notify_all()
def clear(self):
"""Reset the internal flag to false.
将内部标志重置为false。
Subsequently, threads calling wait() will block until set() is called to set the internal flag to true again.
随后,调用wait()的线程将阻塞,直到调用set()再次将内部标志设置为true。
"""
with self._cond:
self._flag = False
def wait(self, timeout=None):
"""Block until the internal flag is true.
阻止,直到内部标志为真。
If the internal flag is true on entry, return immediately.
如果输入时内部标志为true,请立即返回。
Otherwise, block until another thread calls set() to set the flag to true, or until the optional timeout occurs.
否则,阻塞直到另一个线程调用set()将标志设置为true,或者直到出现可选超时。
When the timeout argument is present and not None, it should be a floating point number specifying a timeout for the operation in seconds (or fractions thereof).
当timeout参数存在而不是无时,它应该是一个浮点数,以秒(或其小数)为单位指定操作超时。
This method returns the internal flag on exit, so it will always return True except if a timeout is given and the operation times out.
此方法在退出时返回内部标志,因此,除非给定超时和操作超时,否则它将始终返回true。
"""
with self._cond:
signaled = self._flag
if not signaled:
signaled = self._cond.wait(timeout)
return signaled
Event对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。
在初始情况下,Event对象中的信号标志被设置为假。
如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。
一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。
如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行。
event.isSet():返回event的状态值;
event.wait():如果 event.isSet()==False将阻塞线程;
event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;
event.clear():恢复event的状态值为False。
应用场景:有多个工作线程尝试链接MySQL,我们想要在链接前确保MySQL服务正常才让那些工作线程去连接MySQL服务器,如果连接不成功,都会去尝试重新连接,那么我们就可以采用threading.Event机制来协调各个工作线程的连接操作。
import time
import random
import threading
from threading import Event
from threading import Thread
def conn_mysql():
count = 1
while not event.is_set():
if count > 3:
raise TimeoutError("链接超时")
print(threading.currentThread().getName(), "第%s次尝试链接..." % count)
event.wait(0.5)
count += 1
print(threading.currentThread().getName(), "链接成功")
def check_mysql():
print(threading.currentThread().getName(), "正在检查MySQL。")
time.sleep(random.randint(1, 4))
event.set()
if __name__ == '__main__':
event = Event()
conn1 = Thread(target=conn_mysql)
conn2 = Thread(target=conn_mysql)
check = Thread(target=check_mysql)
conn1.start()
conn2.start()
check.start()
输出结果如下,
链接成功:
Thread-1 第1次尝试链接...
Thread-2 第1次尝试链接...
Thread-3 正在检查MySQL。
Thread-2 第2次尝试链接...
Thread-1 第2次尝试链接...
Thread-1 第3次尝试链接...
Thread-2 第3次尝试链接...
Thread-1 链接成功
Thread-2 链接成功
Process finished with exit code 0
链接超时:
Thread-1 第1次尝试链接...
Thread-2 第1次尝试链接...
Thread-3 正在检查MySQL。
Thread-1 第2次尝试链接...
Thread-2 第2次尝试链接...
Thread-1 第3次尝试链接...
Thread-2 第3次尝试链接...
Exception in thread Thread-2:
Traceback (most recent call last):
File "D:\MiniConda\lib\threading.py", line 926, in _bootstrap_inner
self.run()
File "D:\MiniConda\lib\threading.py", line 870, in run
self._target(*self._args, **self._kwargs)
File "G:/Python/PythonFullStack/第四模块_网络编程进阶+数据库开发/并发编程/多线程/13.Event.py", line 23, in conn_mysql
raise TimeoutError("链接超时")
TimeoutError: 链接超时
Exception in thread Thread-1:
Traceback (most recent call last):
File "D:\MiniConda\lib\threading.py", line 926, in _bootstrap_inner
self.run()
File "D:\MiniConda\lib\threading.py", line 870, in run
self._target(*self._args, **self._kwargs)
File "G:/Python/PythonFullStack/第四模块_网络编程进阶+数据库开发/并发编程/多线程/13.Event.py", line 23, in conn_mysql
raise TimeoutError("链接超时")
TimeoutError: 链接超时
定时器
顾名思义,就是定时执行某个指令集。
from threading import Timer
def hello():
print("Hello World!")
if __name__ == '__main__':
for _ in range(7):
thread = Timer(1, hello)
thread.start() # after 1 seconds, "hello, world" will be printed
输出结果为:
hello, world
hello, world
hello, world
hello, world
hello, world
hello, world
hello, world