Demo代码和引用知识点都参考自《理解Python并发编程一篇就够了 - 线程篇》--董伟明或作者个人公众号Python之美, 《Python Cookbook》和廖雪峰Python3教程。
GIL
由于CPython全局解释锁,Python利用多线程进行CPU密集的计算型任务时,可能性能会降低。
GIL是必须的,这是Python设计的问题:Python解释器是非线程安全的。这意味着当从线程内尝试安全的访问Python对象的时候将有一个全局的强制锁。 在任何时候,仅仅一个单一的线程能够获取Python对象或者C API。每100个字节的Python指令解释器将重新获取锁,这(潜在的)阻塞了I/O操作。因为锁,CPU密集型的代码使用线程库时,不会获得性能的提高(但是当它使用之后介绍的多进程库时,性能可以获得提高。
利用多线程计算斐波那契数。
# -*- coding: utf-8 -*-
# 导入相关依赖
from datetime import datetime
import threading
import time
# 记录时间装饰器
def log_time(func):
def wrapper(*args, **kwargs):
# start_time = datetime.now()
start_time = time.time()
res = func(*args, **kwargs)
# end_time = datetime.now()
end_time = time.time()
# print('cost %ss' % (end_time - start_time).seconds)
print('cost %ss' % (end_time - start_time))
return res
return wrapper
# 计算斐波那契数方法
def fib(n):
if n <= 2:
return 1
return fib(n-1) + fib(n-2)
# 单线程计算两次
@log_time
def single_thread():
fib(33)
fib(33)
# 多线程执行
@log_time
def multi_thread():
for _ in range(2):
t = threading.Thread(target=fib, args=(33, ))
t.start()
main_thread = threading.currentThread()
for t in threading.enumerate():
if t is main_thread:
continue
t.join()
single_thread()
multi_thread()
cost 3.089695453643799s
cost 3.232300281524658s
不幸的是运行了几次,始终得到结果multi_thread()
耗时没有远大于single_thread()
,但也没有达到提高性能的目的。
虽然有GIL,Python多线程仍可用于I/O密集型的场景。
多线程的同步
1.信号量Semaphore
在多线程编程中,为了防止不同的线程同时对一个公用的资源(比如全部变量)进行修改,需要进行同时访问的数量(通常是1)。信号量同步基于内部计数器,每调用一次acquire(),计数器减1;每调用一次release(),计数器加1.当计数器为0时,acquire()调用被阻塞。
不同于线程池,之前的子线程释放后会创建新的线程,而不是重用线程池里的线程。
# -*- coding: utf-8 -*-
# 导入相关依赖
import threading
import random
import time
sema = threading.Semaphore(3)
def foo(tid):
# 利用with,代替acquire()和release()
with sema:
current_thread_name = threading.currentThread().name
print('%s: %s acquire sema' % (current_thread_name, tid))
wt = random.random()
time.sleep(wt)
print('%s: %s release sema.' % (current_thread_name, tid))
for i in range(5):
thread_name = 'thread_' + str(i)
t = threading.Thread(name=thread_name, target=foo, args=(i, ))
t.start()
创建信号量时限制了访问资源的线程数量为3,同时最多只有3个线程执行,3次acquire()
后,计数器成0,线程调用acquire()
被阻塞,等到某个线程release()
后,之后的线程才能acqure()
,实际还是创建了5个线程。
2.同步锁Lock
互斥锁,相当于信号量为1。在廖雪峰Python教程 -- 多线程中也解释了Lock()。
若没有加上互斥锁。
# -*- coding: utf-8 -*-
# 导入相关依赖
import threading
import time
balance = 0 # 余额
def deposit(money):
global balance
balance += money
def excute_method(money):
for i in range(100000):
deposit(money)
t_1 = threading.Thread(name='t_1', target=excute_method, args=(1, ))
t_2 = threading.Thread(name='t_2', target=excute_method, args=(1, ))
t_1.start()
t_2.start()
t_1.join()
t_2.join()
print('balance is %s' % balance)
最终的结果远小于100000*2。
balance is 37564
因为在deposit()
中balance += money
,CPU执行时实际会被拆分成
temp = balance + money
balance = temp
对于每个线程,temp
是成员变量,而balance
是公有的,在多线程执行时,t_1
和t_2
交替运行,会导致balance
最终的值异常,所以需要对balance
加锁。
之前的Demo是两个线程同时循环累加100000次,下面是500个线程同时加1。
# -*- coding: utf-8 -*-
# 导入相关依赖
import threading
import time
lock = threading.Lock()
balance = 0
def deposit(money):
global balance
with lock:
# balance += money
temp = balance + money
time.sleep(0.01) # 使线程有时间切换
balance = temp
threads = []
for i in range(500):
t = threading.Thread(target=deposit, args=(1, ))
t.start()
threads.append(t)
for t in threads:
t.join()
print('balance is %s' % balance)
最终的到的结果始终是500。
balance is 500
3.可重入锁RLock
...
4.条件变量Condition
一个线程等待特定条件,而另一个线程发出特定条件满足的信号。最好说明的例子就是「生产者/消费者」模型:
以及下面的事件Event,队列Queue都将用生产者/消费者模型来举例。
Condition
: 主要的方法是wait()
,notifyAll()
,notify()
,同时还提供了类似Lock()
的acquire()
和release()
。
wait()
: 创建了一个名为waiter的锁,并且设置锁的状态为locked,进入阻塞状态,直至收到一个notify()
通知。这个waiter锁用于线程间的通讯。
notify()
、notifyAll()
: 释放waiter锁,唤醒线程。
# -*- coding: utf-8 -*-
# 导入相关依赖
import threading
import time
# 生产者
def producer(cond):
current_thread_name = threading.currentThread().name
with cond:
time.sleep(0.1)
print('%s: make resource available.' % current_thread_name)
cond.notifyAll() # 唤醒消费者线程
# 消费者
def consumer(cond):
current_thread_name = threading.currentThread().name
with cond:
cond.wait() # 创建了一个名为waiter的锁,并且设置锁的状态为locked。这个waiter锁用于线程间的通讯。
print('%s: Resource is available to consumer' % current_thread_name)
cond = threading.Condition()
p1 = threading.Thread(name='p1', target=producer, args=(cond, ))
c1 = threading.Thread(name='c1', target=consumer, args=(cond, ))
c2 = threading.Thread(name='c2', target=consumer, args=(cond, ))
c1.start()
c2.start()
time.sleep(0.1)
p1.start()
生产者发出的消息被消费者接收到了,但需要注意的是,在这个Demo中消费者线程需要比生产者线程先执行获取waiter锁,否则会出现问题。(可能是因为consumer
还没获取到锁,而producer
已经执行了notifyAll()
唤醒操作?)
问题1: 感觉上述例子太过于简单,没有很好的说明Condition
的用法,且notifyAll()
不是很常用?
参考网上的示例写了另一个Demo,如下:
import threading
import time
import queue
condition = threading.Condition()
# products = 0 # 改为产品队列
products = queue.Queue(10)
count = 20 # 最多生产20个
# done = False # 结束标志
class Producer(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
global condition, products, count, done
while count > 0:
if condition.acquire():
if not products.full():
products.put(1)
print("Producer(%s):deliver one, now products:%s" %(self.name, products.qsize()))
condition.notify()
count -= 1
else:
print("Producer(%s):already 10, stop deliver, now products:%s" %(self.name, 0))
condition.wait()
condition.release()
time.sleep(0.5)
# done = True
print('break producer')
class Consumer(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
global condition, products, done
# while not done:
while True:
if condition.acquire():
if not products.empty():
n = products.get()
time.sleep(0.5)
print("Consumer(%s):consume one, now products:%s" % (self.name, n))
condition.notify()
else:
print("Consumer(%s):only 0, stop consume, products:%s" % (self.name, 0))
condition.wait()
condition.release()
time.sleep(0.5)
print('break consumer')
threads = []
for p in range(0, 5):
p = Producer()
p.start()
threads.append(p)
for c in range(0, 10):
c = Consumer()
c.start()
threads.append(c)
for t in threads:
t.join()
print('end main')
5.事件Event
一个线程发送/传递事件,另外的线程等待事件的触发。
可用于线程间的通信,主线程对子线程的控制。Python中使用threading.Event协调线程的运行,该博客举了一个利用Event来协调线程运行的场景,感觉比下面的Demo举例好。
Event
主要有set()
、 clear()
和wait()
方法。
原文中Event
的例子是无限循环且不会退出的,稍作修改。
# -*- coding: utf-8 -*-
# 导入相关依赖
import threading
import time
import random
def producer(event, l):
current_thread_name = threading.currentThread().name
count = 10
while count > 0:
n = random.randint(10, 100)
l.append(n)
print('%s: %s appended to list' % (current_thread_name, n))
count -= 1
event.set()
# 若该处不设置time.sleep()则可能生产者执行后,消费者执行时只会pop最新的
# 可能有之前的没有pop出来,但又append了新的元素。
time.sleep(0.1)
def consumer(event, l):
current_thread_name = threading.currentThread().name
while 1:
event_is_set = event.wait(2) # 设置超时时间,超时后break
if event_is_set:
try:
n = l.pop()
print('%s: %s poped from list' % (current_thread_name , n))
event.clear() # 清空事件状态
except IndexError: # 为了让刚启动时容错
pass
else:
break
event = threading.Event()
l = []
p1 = threading.Thread(name='p1', target=producer, args=(event, l))
c1 = threading.Thread(name='c1', target=consumer, args=(event, l))
c2 = threading.Thread(name='c2', target=consumer, args=(event, l))
p1.start()
c1.start()
c2.start()
问题2: Event和Condition的异同?各自用于什么场景。《Python CookBook》 12.2 提及了Event和Condition。
event 对象最好单次使用,就是说,你创建一个event 对象,让某个线程等待这个
对象,一旦这个对象被设置为真,你就应该丢弃它。尽管可以通过clear() 方法来重
置event 对象,但是很难确保安全地清理event 对象并对它重新赋值。很可能会发生错
过事件、死锁或者其他问题(特别是,你无法保证重置event 对象的代码会在线程再
次等待这个event 对象之前执行)。如果一个线程需要不停地重复使用event 对象,你
最好使用Condition 对象来代替。
6.队列Queue
队列是线程,进程安全的,是很常见的并发编程时用到的数据结构。
Queue
主要有put()
、get()
、join()
、empty()
等方法。
put()
: 往队列里添加一项。
get()
: 从队列中取出一项。
empty()
: 判断队列是否为空。
join()
: 阻塞直至队列中项目执行完毕。
task_done()
: 在某一项任务完成时调用。
ps:multiprocessing
模块也有Queue
,但他不支持join()
和task_done()
,可以使用模块下的JoinableQueue
。
使用队列模拟生产者/消费者模型:
# -*- coding: utf-8 -*-
# 导入相关依赖
import threading
import queue
import time
import random
def double(n):
return n * 2
q = queue.Queue()
def producer():
count = 15
current_thread_name = threading.currentThread().name
while count > 0:
n = random.randint(10, 100)
q.put((double, n))
# time.sleep(0.5) # 若在这里有个耗时操作则该Demo的消费者会直接break
print('%s: put %s in to queue.' % (current_thread_name, n))
count -= 1
def consumer():
current_thread_name = threading.currentThread().name
while 1:
if q.empty():
break
task, arg = q.get()
res = task(arg)
time.sleep(0.1) # 耗时操作让线程可以切换
print('%s: result is %s.' % (current_thread_name, res))
q.task_done()
p1 = threading.Thread(name='p1', target=producer)
c1 = threading.Thread(name='c1', target=consumer)
c2 = threading.Thread(name='c2', target=consumer)
# c1.setDaemon(True)
# c2.setDaemon(True)
p1.start()
c1.start()
c2.start()
问题3: 在上述Demo中,若生产者线程阻塞了,那消费者线程不就先启动然后直接break了?(1.尝试把break去掉,让消费者线程一直执行,为让其正常结束,将消费者线程设置为守护线程,并在最后对队列进行join()
阻塞保证正常执行。2.利用Event和Queue, Condition和Queue。)
除了普通的队列外还有优先级队列PriorityQueue()
,会按传入的优先级来get()
并返回。
7.线程池
通过线程池来创建并重复利用和销毁线程,避免过多的创建销毁线程所产生的花销。
内置线程池的map()
:
from multiprocessing.pool import ThreadPool
pool = ThreadPool(3)
pool.map(lambda x : x * 2, [1, 2, 3])
利用队列简单实现线程池:
# 导入相关依赖
import threading
import time
import queue
class Worker(threading.Thread):
def __init__(self, q):
super().__init__()
self._q = q
self.daemon = True # 守护线程
self.start()
def run(self):
while 1:
f, args, kwargs = self._q.get()
res = f(*args, **kwargs)
print('%s: result is %s.' % (self.name, res))
self._q.task_done()
class CostumePool():
def __init__(self, pool_size):
self._q = queue.Queue(poo_size)
for _ in range(pool_size):
Worker(self._q)
def add_task(self, f, *args, **kwargs):
self._q.put((f, args, kwargs))
def wait_complete(self):
self._q.join()
def double(n):
return n * 2
pool = CostumePool(3)
for i in range(10):
pool.add_task(double, i)
time.sleep(0.1)
pool.wait_complete()
最多只会创建3个子线程,当前任务执行完后复用,执行新的任务。