当多个线程同时读写同一份共享资源的时候,可能会引起冲突。 这时候,我们需要引入线程“同步”机制,即各位线程之间要有个先来后到,不能一窝蜂挤上去抢作一团。 线程同步的真实意思和字面意思恰好相反。 线程同步的真实意思,其实是“排队”:几个线程之间要排队,一个一个对共享资源进行操作,而不是同时进行操作。
Python threading模块提供了Lock/RLock、Condition、queue、Event等对象来实现线程同步。
1. Lock/RLock对象
Lock是比较低级的同步原语,当被锁定以后不属于特定的线程。一个所有两种状态:locked和unlocked。如果锁处于unlocked状态,acquire()方法将其修改为locked并立即返回;如果锁已处于locked状态,则阻塞当前线程并等待其他线程释放锁,然后将其修改为locked并立即返回。release()方法用来将锁的状态由locked修改为unlocked并立即返回,如果锁已经处于unlocked状态,调用该方法将抛出异常。
可重入锁RLock对象也是一种常用的线程同步原语,可以被同一个线程acquire()多次。当处于locked状态时,某线程拥有该锁;当处于unlocked状态时,该锁不属于任何线程。
RLock对象的acquire() / release()调用对可以嵌套,仅当最后一个或者最外层release()执行结束后,锁才被设置为unlocked。
Lock对象成员如下:
方法 | 描述 |
---|---|
acquire() | 获得锁。该方法等待锁被解锁,将其设置为locked并返回True。 |
release() | 释放锁。当锁被锁定时,将其重置为解锁并返回。如果锁未锁定,则会引发RuntimeError。 |
locked() | 如果锁被锁定,返回True。 |
例1:使用RLock/Lock实现线程同步:
import threading
import time
# 自定义线程类
class MyThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
# 重写线程代码
def run(self):
global x
# 获得锁
lock.acquire()
for i in range(3):
x = x + i
time.sleep(2)
print(x)
# 释放锁
lock.release()
# 创建锁
lock = threading.RLock()
# lock = threading.Lock()
t1 = []
for i in range(10):
# 创建线程
t = MyThread()
t1.append(t)
x = 0
for i in t1:
# 启动线程
i.start()
2. Condition对象
使用Condition对象可以在某些事件触发后才处理数据,可以用于不同线程之间的通信或通知,以实现更高级别的同步。Condition对象除了具有acquire() / release()方法之外,还有wait()、notify()和notify_all()等方法。
方法 | 描述 |
---|---|
acquire() | 获取底层锁。此方法等待底层锁被解锁,将其设置为locked并返回True。 |
notify(n=1) | 在此条件下最多唤醒n个等待的任务(默认为1)。如果没有任务在等待,则该方法是no-op。必须在调用此方法之前获取锁,并在调用后不久释放锁。如果使用未锁定的锁调用,则会引发RuntimeError错误。 |
locked() | 如果获得了底层锁,则返回True。 |
notify_all() | 唤醒所有在此条件下等待的任务。此方法的作用类似于notify(),但会唤醒所有等待的任务。必须在调用此方法之前获取锁,并在调用后不久释放锁。如果使用未锁定的锁调用,则会引发RuntimeError错误。 |
release() | 释放底层锁。当在未锁定的锁上调用时,将引发RuntimeError。 |
wait() | 等待通知。如果调用此方法时调用任务没有获得锁,则会引发RuntimeError。这个方法释放底层锁,然后阻塞它,直到它被notify()或notify_all()调用唤醒。一旦被唤醒,条件将重新获得锁,该方法将返回True。 |
wait_for(predicate) | 等待predicate变为true。predicate必须是可调用的,其结果将被解释为布尔值。最后一个值是返回值。 |
例2:使用Condition对象实现线程同步:
import threading
# 生产者类
class Producer(threading.Thread):
def __init__(self, thread_name):
threading.Thread.__init__(self, name=thread_name)
# 重写线程代码
def run(self):
global x
# 获得锁
con.acquire()
if x == 20:
# 等待通知
con.wait()
else:
print("\nProducer: ", end=' ')
for i in range(20):
print(x, end=' ')
x = x + 1
print(x)
con.notify()
# 释放锁
con.release()
# 消费者类
class Consumer(threading.Thread):
def __init__(self, thread_name):
threading.Thread.__init__(self, name=thread_name)
# 重写线程代码
def run(self):
global x
# 获得锁
con.acquire()
if x == 0:
# 等待通知
con.wait()
else:
print("\nConsumer: ", end=' ')
for i in range(20):
print(x, end=' ')
x = x-1
print(x)
con.notify()
# 释放锁
con.release()
# 创建锁
con = threading.Condition()
x = 0
p = Producer('Producer')
c = Consumer('Consumer')
p.start()
c.start()
p.join()
c.join()
print('After Producer and Consumer all done:', x)
3. queue对象
queue模块实现多生产者、多消费者队列。当信息必须在多个线程之间安全地交换时,它在线程编程中特别有用。此模块中的Queue类实现所有必需的锁定语义。queue模块提供了Queue、LifoQueue或PriorityQueue对象。
队列对象(Queue、LifoQueue或PriorityQueue)提供以下描述的公共方法:
方法 | 描述 |
---|---|
qsize() | 返回队列的大致大小。注意,qsize() > 0不保证后续get()不会阻塞,qsize() < maxsize也不保证put()不会阻塞。 |
empty() | 如果队列为空,返回True,否则返回False。如果empty()返回True,则不能保证对put()的后续调用不会阻塞。类似地,如果empty()返回False,则不能保证对get()的后续调用不会阻塞。 |
full() | 如果队列已满,返回True,否则返回False。如果full()返回True,则不能保证对get()的后续调用不会阻塞。类似地,如果full()返回False,则不能保证对put()的后续调用不会阻塞。 |
put(item, block=True, timeout=None) | 将项放入队列。如果可选的参数block=True, timeout=None(缺省值),则在空闲插槽可用之前,如果有必要,将阻塞。如果timeout是一个正数,那么它将阻塞最多的超时秒,如果在这段时间内没有可用的空闲插槽,则引发完整的异常。否则(block为false),如果一个空闲插槽立即可用,则将一个项放到队列中,否则引发完全异常(在这种情况下忽略超时)。 |
put_nowait(item) | 相当于put(item, False)。 |
get(block=True, timeout=None) | 从队列中删除并返回一个项。如果可选的block=true, timeout=None(缺省值),则在项目可用之前,如果有必要,将阻塞。如果timeout是一个正数,那么它将阻塞最多的超时秒,如果在这段时间内没有可用的项,则引发空异常。否则(block为false),返回一个立即可用的项,否则引发空异常(在这种情况下忽略超时)。 |
get_nowait() | 等价于get(False)。提供了两种方法来支持跟踪已加入队列的任务是否已被守护进程使用者线程完全处理。 |
task_done() | 指示已完成先前排队的任务。由队列使用者线程使用。对于用于获取任务的每个get(),后续对task_done()的调用告诉队列任务上的处理已经完成。如果join()当前处于阻塞状态,那么在处理完所有项之后,它将继续运行(这意味着对于已经放入队列()的每个项,都收到了task_done()调用)。如果调用次数超过放置在队列中的项的次数,则引发ValueError。 |
join() | 阻塞,直到获取和处理队列中的所有项。每当向队列添加项时,未完成任务的数量就会增加。每当使用者线程调用task_done()来指示检索了该项并完成了对该项的所有工作时,计数就会下降。当未完成任务的计数降为零时,join()解块。 |
例3:使用Queue对象实现线程同步:
def worker():
while True:
item = q.get()
if item is None:
break
do_work(item)
q.task_done()
q = queue.Queue()
threads = []
for i in range(num_worker_threads):
t = threading.Thread(target=worker)
t.start()
threads.append(t)
for item in source():
q.put(item)
# block until all tasks are done
q.join()
# stop workers
for i in range(num_worker_threads):
q.put(None)
for t in threads:
t.join()
4. Event对象
Event对象是一种简单的线程同步通信技术,一个线程设置Event对象,另一个线程等待Event对象。
方法 | 描述 |
---|---|
wait() | 等待事件被设置。如果事件被设置,立即返回True。否则阻塞,直到另一个任务调用set()。 |
set() | 设置事件。所有等待事件设置的任务将立即被唤醒。 |
clear() | 清除(取消)事件。等待on wait()的任务现在将阻塞,直到再次调用set()方法。 |
is_set() | 如果设置了事件,则返回True。 |
例4:使用Event对象实现线程同步:
import threading
# 自定义线程类
class MyThread(threading.Thread):
def __init__(self, thread_name):
threading.Thread.__init__(self, name=thread_name)
# 重写线程代码
def run(self):
global my_event
if my_event.isSet():
my_event.clear()
# 等待通知
my_event.wait()
print(self.getName())
else:
print(self.getName())
my_event.set()
# 创建锁
my_event = threading.Event()
my_event.set()
t1 = []
for i in range(10):
t = MyThread(str(i))
t1.append(t)
for t in t1:
t.start()