Python内置了threading模块以提供对多线程的支持,简单示例如下。
线程
多线程
定义一个需要2s才能执行完毕的函数,以线程的方式来同时运行两个这样的函数:
import threading,time
def func(n):
print("{} running...this is func_{}".format(threading.current_thread(),n))
time.sleep(2)
print("{} over.this is func_{}".format(threading.current_thread(),n))
t1=threading.Thread(target=func,args=(1,))
t2=threading.Thread(target=func,args=(2,))
print("{}.program start.".format(threading.current_thread()))
t1.start()
t2.start()
print("{}.program over.".format(threading.current_thread()))
运行结果:
<_MainThread(MainThread, started 3640)>.program start.
<Thread(Thread-1, started 13052)> running...this is func_1
<Thread(Thread-2, started 10080)> running...this is func_2
<_MainThread(MainThread, started 3640)>.program over.
<Thread(Thread-1, started 13052)> over.this is func_1
<Thread(Thread-2, started 10080)> over.this is func_2
从运行结果中不难看出,多线程的程序会有一个主线程,即程序本身。而主线程与子线程之间的关系是并行的;并且主线程的运行不会被子线程所阻塞,因为主线程运行到”program over”时两个子线程都没有结束;但是注意到,在程序的最后,当主线程的任务完成时,整个程序必须等待所有子线程结束后它才会结束。
整个程序运行流程图如下所示:
join()
对于运行关键任务的子线程,主线程可能需要用到该子线程的运行结果,此时主线程与该子线程的关系不能再是完全的并行关系,主线程必须等待该子线程运行结束,然后主线程继续运行直至整个程序结束。考虑如下代码:
import threading,time
def func_1():
print("func_1 running...")
time.sleep(2)
print("func_1 over.")
def func_2():
print("func_2 running...")
time.sleep(4) #关键任务
print("func_2 over.generate the result.")
t1=threading.Thread(target=func_1,args=())
t2=threading.Thread(target=func_2,args=())
print("program start.")
t1.start()
t2.start()
t2.join() #主线程等待t2
print("program over.")
运行结果为:
program start.
func_1 running…
func_2 running…
func_1 over.
func_2 over.generate the result.
program over.
从运行结果不难看出,线程t1是完全独立且并行的,而主线程与t2虽然是不同的线程,但是两者的关系却是串行的,因为(假设)主线程的继续运行需要依赖于t2的运行结果,所以主线程需要等待t2的结束才能继续往下运行。线程的join()方法相当于将该线程加入到了主线程的流程中,如下图所示:
守护线程
假设有一个交互性程序,用户通过键盘与触摸操作来与程序互动。相比于在程序中不断循环检测是否有键盘操作或是触摸操作而言,为键盘操作与触摸操作分别设置一个线程用于监测事件要好得多,这即是所谓的守护线程。主线程没有等待守护线程结束的责任,反之,守护线程却有着维持主线程正常工作的义务。考虑如下代码:
import threading,time
def func_1():
print("监测键盘事件...")
while True:
pass
def func_2():
print("监测触摸事件...")
while True:
pass
t1=threading.Thread(target=func_1,args=())
t2=threading.Thread(target=func_2,args=())
print("program start.")
t1.setDaemon(True) #设置守护进程
t1.start()
t2.setDaemon(True)
t2.start()
time.sleep(2)
print("program over.")
运行结果为:
program start.
监测键盘事件…
监测触摸事件…
program over.
从结果可以看出,当主线程任务完成时,守护进程也随之终止,整个程序退出,流程图如下所示:
锁
GIL
CPython(Python解释器)在发展早期,为了多线程下的数据安全,加入了一个Global Interpreter Lock,这就导致CPython下的多线程只是一个伪实现,并不是真正的多线程。这就导致CPython并不适合完成计算密集型任务,而只适合去完成IO密集型任务。
可考虑的方案有两种,一是使用multiprocess替代threading,二是使用如JPython这样的解释器。两种方案各有优劣,前者是使用多进程,会增加进程间通信的时间;而后者在某些性能上不如CPython。
线程锁
对于关键数据或资源,在多线程下可能会出现一致性问题,为防止这种情况,可对关键资源进行操作的关键代码区进行加锁,在加锁的代码区内,只允许一个线程访问。
考虑如下情况,假设一个多线程程序要做复杂运算,其中部分运算牵涉到一个公共数据。用如下方法进行实现模拟:
线程中对公共数据的复杂运算,用+1操作并休眠1s来模拟;而线程中对公共数据的读取用print函数来模拟。于是有如下代码:
import threading,time
num=0
def complex_compute():
global num
num+=1
time.sleep(1)
def func_add():
complex_compute() #对数据的运算
print(num) #读取数据查看是否正确
t1=threading.Thread(target=func_add,args=())
t2=threading.Thread(target=func_add,args=())
print("program start.")
t1.start()
t2.start()
print("program over.")
按照期望,线程t1读取(输出)的数据应该是1,t2是2,但是程序的实际输出为两者都是2。这就是线程同时对公共数据的操作所引起的一致性问题。
加入线程锁后的代码:
def func_add():
lock.acquire() #互斥区开始
complex_compute() #安全操作
lock.release() #互斥区结束
print(num)
这样一来,程序的输出就正常了。
信号量
事件信号
用事件信号来模拟公路上红绿灯的事件:当信号灯为红灯时,所有车子都停止,当信号灯为绿灯时,车子才能前进。
import threading
import time
#事件实例
event=threading.Event()
#红绿灯
def RG_light():
#计数器设零
count=0
while True:
#2-3表示红灯
if count>1 and count<4:
print("[light]green!")
event.set()
count+=1
#计数器值最大为4,此时代表黄灯
elif count==4:
print("[light]yellow!")
event.clear()
#计数器清零
count=0
#0,1表示红灯
else:
print("[light]red!")
count+=1
time.sleep(1)
def car():
while True:
#若事件标志位被设置则说明是绿灯
if event.is_set():
print("[car]running...")
#否则是红灯
else:
print("[car]stop!")
#等待事件标志
event.wait()
print("[car]ready to go!")
time.sleep(0.5)
t1=threading.Thread(target=RG_light,args=())
t2=threading.Thread(target=car,args=())
t1.start()
t2.start()
截取部分程序输出:
线程级队列与栈
import queue
队列
队列简单示例:
q=queue.Queue(maxsize=10)
q.put(1)
q.put(2)
print(q.get())
print(q.get())
输出为1,2。
对于这种普通用法,如果队列为空时,再去进行get()操作时,程序会被阻塞,可以使用不等待的get_nowait()方法:
q=queue.Queue(maxsize=10)
q.put(1)
q.put(2)
while True:
try:
print(q.get_nowait()) #无等待地取元素
except queue.Empty as e:
print("queue empty!")
exit(0)
输出为:
带优先级的队列
此种队列会根据插入元素的优先级对元素进行排队,优先级的数值越小,优先级越高。
#优先级队列
q=queue.PriorityQueue(maxsize=10)
q.put((3,"3"))
q.put((-5,"-5"))
q.put((0,"0"))
while True:
try:
print(q.get_nowait()) #无等待地取元素
except queue.Empty as e:
print("queue empty!")
exit(0)
输出为:
栈
python中还有一种后进先出队列LifoQueue,实际上就实现了一个栈,基本示例如下:
#栈
q=queue.LifoQueue(maxsize=10)
q.put(1)
q.put(2)
while True:
try:
print(q.get_nowait()) #无等待地取元素
except queue.Empty as e:
print("queue empty!")
exit(0)
输出为:
生产者消费者模型
import queue
import time
import threading
q=queue.Queue(maxsize=10)
def Producer():
id=0
while True:
q.put("good No.{}".format(id))
id+=1
time.sleep(1)
def Comsumer(name):
while True:
good=q.get()
print("{} get {}".format(name,good))
time.sleep(1)
producer=threading.Thread(target=Producer,args=())
comsumer_1=threading.Thread(target=Comsumer,args=("C1",))
comsumer_2=threading.Thread(target=Comsumer,args=("C2",))
producer.start()
comsumer_1.start()
comsumer_2.start()
部分输出:
进程
前一篇文章讲到,由于CPython解释器中存在GIL,所以CPython的多线程并不是真正意义上的多线程,这就导致了CPython解释器不适用于计算密集型任务,而只适用于IO密集型任务。有一种解决办法就是使用多进程替代多线程。
import multiprocessing
多进程
Python的多进程与多线程的使用方法几乎一致,一个简单示例如下:
import multiprocessing
import time
import os
def func(id):
print("{} running...this is func_{}".format(os.getpid(),id))
time.sleep(1)
if __name__=="__main__":
for i in range(3):
p=multiprocessing.Process(target=func,args=(i,))
p.start()
输出为:
此处注意,同多线程情况一样,这里的21268、20604、13332是三个进程的进程ID,并且这三个ID具有相同的父进程3804,这个父进程就是pycharm。
进程间通信
队列形式
前文所讲述的队列结构是线程级别的队列,同一进程下的线程是共享一块内存区的,可以互相访问数据;但是不同进程间的内存区不是共享的,需要用手段来实现进程间通信。
线程间的数据访问
import threading
import queue
def func():
data=q.get_nowait()
print(data)
if __name__=='__main__':
q=queue.Queue(maxsize=5)
#主线程在队列中添加数据
q.put("data from main process/thread")
#子线程在队列中取数据
t=threading.Thread(target=func,args=())
t.start()
t.join()
输出为:data from main process/thread
进程间的数据访问
上述代码对进程是不起作用的,会报错。那么是不是可以使用参数传递的方式,将一个进程中的队列传递给另一个进程去访问呢?考虑如下代码:
import multiprocessing
import queue
def func(que):
data=que.get_nowait()
print(data)
if __name__=='__main__':
q=queue.Queue(maxsize=5)
#主进程在队列中添加数据
q.put("data from main process/thread")
#子进程接受父进程的队列作为参数,并取数据
t=multiprocessing.Process(target=func,args=(q,))
t.start()
t.join()
报错信息:TypeError: can’t pickle _thread.lock objects
如果一个进程要访问另一个进程队列里的数据,需要使用进程级队列,并且需要传递参数:
from multiprocessing import Process,Queue
def func(que):
data=que.get_nowait()
print(data)
if __name__=='__main__':
q=Queue(maxsize=5)
#主进程在队列中添加数据
q.put("data from main process/thread")
#子进程拥有父进程队列的副本,并取数据,子进程取完数据之后,父进程中的队列被清空
t=Process(target=func,args=(q,))
t.start()
t.join()
输出为:data from main process/thread
这里实际上并不是子进程访问了父进程队列中的数据,而是在创建子进程时,父进程把队列复制了一份交给子进程。在子进程取到数据之后,由于数据要同步,所以父进程的队列也会被清空。
管道形式
进程间另一种通信形式就是管道。
from multiprocessing import Process,Pipe
def func(conn):
conn.send("[C]hello")
print(conn.recv())
conn.close()
if __name__=='__main__':
#创建一个管道对象,返回两个端连接
p_conn,c_conn=Pipe()
#将一端的连接传给子进程
p=Process(target=func,args=(c_conn,))
p.start()
#开始通信
print(p_conn.recv())
p_conn.send("[P]hello")
p.join()
p_conn.close()
输出为:[C]hello [P]hello
管道通信类似网络编程中的socket,两端之间的收发也需要注意顺序与次数的问题,否则会造成程序的阻塞。
进程间数据共享
Python的multiprocessing模块中提供了一个Manager类,专门用于实现进程间数据共享。
from multiprocessing import Process,Manager
import os
#往字典与列表中写入PID
def func(d,l):
d[os.getpid()]=os.getpid()
l.append(os.getpid())
if __name__=='__main__':
#Manager对象中包含了一些可供不同进程访问的数据类型
manager=Manager()
#创建进程级字典与列表
_dict=manager.dict()
_list=manager.list()
p_list=[]
for i in range(3):
p=Process(target=func,args=(_dict,_list,))
p.start()
p_list.append(p)
for p in p_list:
p.join()
print(_dict)
print(_list)
输出为:
进程池
进程池限制了程序同时能够运行的进程数量,考虑如下代码:
from multiprocessing import Pool
import os
import time
def func(i):
print("第{}个进程:{}".format(i,os.getpid()))
time.sleep(1)
if __name__=='__main__':
pool=Pool(processes=2) #开设一个进程池,最多允许2个进程
#从进程池中运行五个进程
for i in range(5):
pool.apply_async(func=func,args=(i,))
pool.close()
pool.join() #等待进程池中的进程完毕
虽然程序在进程池中起了5次进程,但是因为进程池允许同时运行的进程数为2,所以进程是两个两个一组运行的,这可以从运行结果看出来。
带回调函数的进程池
在进程池中启动进程时,可以指定进程结束时调用的函数,称为回调函数。
from multiprocessing import Pool
import os
import time
def func():
print("PID:{},PPID:{}".format(os.getpid(),os.getppid()))
time.sleep(1)
def call_back(arg):
print("进程结束,回调函数PID:{}".format(os.getpid()))
if __name__=='__main__':
pool=Pool(processes=2) #开设一个进程池,最多允许2个进程
for i in range(2):
pool.apply_async(func=func,args=(),callback=call_back)
pool.close()
pool.join() #等待进程池中的进程完毕
输出为:
需要注意的是回调函数是由父进程调用的,可以从回调函数输出的PID看出来。
回调函数一个常用的用法为,在一个并行多任务系统下,当某任务触发IO操作时,系统就会切换任务,而当之前任务的IO操作结束时,调用回调函数,用于通知系统IO操作已结束,该任务可以继续运行。
协程
协程是比线程还要轻量、用户层面的微线程。协程可以在单线程中实现并发,通过用户管理的保存上下文操作来实现协程间的切换。Python的第三方库greenlet实现了对协程的支持,安装之后,一个简单的示例如下:
from greenlet import greenlet
def test1():
print(12)
gr2.switch() #保存断点切换到gr2
print(34)
def test2():
print(56)
gr1.switch() #保存断点切换到gr1
print(78)
if __name__=="__main__":
#创建协程对象
gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch() #保存断点切换到gr1
print("program over")
输出为:
greenlet中的协程切换是需要用户指定的,Python中还有一个三方库gevent,这个库实现了协程之间的自动切换。仔细考虑以下代码:
import gevent
def test1():
print("test1 running...")
gevent.sleep(0) #触发切换
print("test1 running...")
def test2():
print("test2 running...")
gevent.sleep(1) #触发切换
print("test2 running...")
def test3():
print("test3 running...")
gevent.sleep(0) #触发切换
print("test3 running...")
if __name__=="__main__":
gevent.joinall(
[gevent.spawn(test1),gevent.spawn(test2),gevent.spawn(test3)]
)
print("program over")
输出为:
当协程触发切换操作时,gevent会自动寻找无需等待的协程运行,上述代码的运行过程大致如下:
- 程序运行协程test1()
- test1()切换到test2()
- test2()切换到test3()
- test3()切换到test1()
- test1()结束,切换到test2()
- test2()仍为等待状态,切换到test3()
- test3()结束,切换到test2()
- test2()等待1s后,运行结束
协程的用法是在单个线程中实现多并发,并且当某个协程触发IO操作时或者用户显示指定时进行协程的切换。上述代码中的gevent.sleep(0)
可认为是协程IO的模拟,下面代码就粗略对比了循环方式与协程方式获取网页的速度:
import gevent
from urllib.request import urlopen
import time
from gevent import monkey #补丁,使模块能响应各种阻塞
monkey.patch_all()
url_list=[
"https://www.python.org/",
"https://www.yahoo.com/",
"https://www.github.com/"
]
def get_page(url):
res=urlopen(url)
print("GET:{}".format(url))
if __name__=="__main__":
tic = time.time()
for url in url_list:
get_page(url)
toc = time.time()
print("循环耗时:{}".format(toc - tic))
tic=time.time()
gevent.joinall(
[gevent.spawn(get_page,url_list[0]),
gevent.spawn(get_page,url_list[1]),
gevent.spawn(get_page,url_list[2])]
)
toc=time.time()
print("协程耗时:{}".format(toc-tic))
输出为: