由于Python的GIL全局解释器锁存在,多线程未必是CPU密集型程序的好的选择。
多进程可以完全独立的进程环境中运行程序,可以较充分地利用多处理器。
但是进程本身的隔离带来的数据不共享也是一个问题。而且线程比进程轻量级。
multiprocessing
Process类
Process 类遵循了hread 类的API, 减少了学习难度
先看一个例子,前面介绍了单线程,多线程比较的例子的多进程版本
版本一:使用最开始的函数执行四次
import datetime
import time
import logging
fmstr = "%(asctime)s%(process)8s %(processName)s %(threadName)s %(thread)s %(message)s"
logging.basicConfig(level=logging.INFO,format=fmstr)
def calc(i = 0):
sum = 0
for i in range(100000000):
sum+=1
logging.info(sum)
return i , sum
start = datetime.datetime.now()
calc()
calc()
calc()
calc()
delta = (datetime.datetime.now()-start).total_seconds()
print(delta) # 44.172526
版本二:多线程的假并行,一刻时刻只有一个线程在执行,相当于几个线程串行执行
import datetime
import time
import multiprocessing # 导入多进程
from multiprocessing import Event,Lock,Semaphore
import threading
import logging
fmstr = "%(asctime)s%(process)8s %(processName)s %(threadName)s %(thread)s %(message)s"
logging.basicConfig(level=logging.INFO,format=fmstr)
def calc(i = 0):
sum = 0
for i in range(100000000):
sum+=1
logging.info(sum)
return i , sum
waits = []
start = datetime.datetime.now()
for i in range(4):
t = threading.Thread(target=calc,name='calc-{}'.format(i))
waits.append(t)
t.start()
for wait in waits:
wait.join()
delta = (datetime.datetime.now()-start).total_seconds()
print(delta) # 45.035576
版本三: 多进程,多个进程同时运行,但这种方法得不到计算返回值
import datetime
import time
import multiprocessing # 导入多进程
from multiprocessing import Event,Lock,Semaphore
import threading
import logging
fmstr = "%(asctime)s%(process)8s %(processName)s %(threadName)s %(thread)s %(message)s"
logging.basicConfig(level=logging.INFO,format=fmstr)
def calc(i = 0):
sum = 0
for i in range(100000000):
sum+=1
logging.info(sum)
return i , sum
if __name__ == '__main__':
start = datetime.datetime.now()
ps = []
for i in range(4):
p = multiprocessing.Process(target=calc,args = (i,),name = 'calc{}'.format(i)) # 使用方法与多线程一样.只是换了中方法而已
ps.append(p)
p.start()
for p in ps:
p.join()
# logging.info('{}{}'.format(p.name,p.exitcode))
delta = (datetime.datetime.now()-start).total_seconds()
print(delta) # 22.425283
print("====end====")
对于上面这个程序,在同一主机上运行,查看进程管理,可以看到多个进行在使用CPU;
注意: 多进程代码一定要放在__name__ =="__main___"
下面执行
版本四:进程池, 函数全部运行结束,会有返回值
import datetime
import time
import multiprocessing # 导入多进程
from multiprocessing import Event,Lock,Semaphore
import threading
import logging
fmstr = "%(asctime)s%(process)8s %(processName)s %(threadName)s %(thread)s %(message)s"
logging.basicConfig(level=logging.INFO,format=fmstr)
def calc(i = 0):
sum = 0
for i in range(100000000):
sum+=1
logging.info(sum)
return i , sum
if __name__ == '__main__':
start = datetime.datetime.now()
pool = multiprocessing.Pool(4) # 开辟进程池的个数,默认为系统当前CPU核心数
for i in range(4):
r = pool.apply(calc,(i,))# 同步阻塞
# logging.info(r)
# print('++++++++++++++++')
# pool.terminate() 会强制关闭进程,不论进程是否执行完毕
pool.close() # 进程有join() 会一直等待进程结束,但是这里是进程池,不会主动释放池内进程,所以需要手动结束进程
pool.join() # 主进程需要等待其他进程
delta = (datetime.datetime.now()-start).total_seconds()
print(delta) # 43.232473
print("====end====")
print(threading.enumerate())
# 这里的多进程是个串行的方式进行运行,等一个运行结束才会继续运行下一个
解决办法:
import datetime
import time
import multiprocessing # 导入多进程
from multiprocessing import Event,Lock,Semaphore
import threading
import logging
fmstr = "%(asctime)s%(process)8s %(processName)s %(threadName)s %(thread)s %(message)s"
logging.basicConfig(level=logging.INFO,format=fmstr)
def calc(i = 0):
sum = 0
for i in range(100000000):
sum+=1
logging.info(sum)
return i , sum
def sub (pool:multiprocessing.Pool,target,i):
r = pool.apply(target,(i,))
logging.info(r)
if __name__ == '__main__':
start = datetime.datetime.now()
pool = multiprocessing.Pool(4)
for i in range(4):
t = threading.Thread(target=sub,args=(pool,calc,i),name="calc-{}".format(i))
t.start()
# print('++++++++++++++++')
# pool.terminate() 会强制关闭进程,不论进程是否执行完毕
pool.close()
pool.join()
delta = (datetime.datetime.now()-start).total_seconds()
print(delta)
print("====end====")
print(threading.enumerate())
版本五:非同步阻塞:
import datetime
import time
import multiprocessing # 导入多进程
from multiprocessing import Event,Lock,Semaphore
import threading
import logging
fmstr = "%(asctime)s%(process)8s %(processName)s %(threadName)s %(thread)s %(message)s"
logging.basicConfig(level=logging.INFO,format=fmstr)
def calc(i = 0):
sum = 0
for i in range(100000000):
sum+=1
logging.info(sum)
return i , sum
if __name__ == '__main__':
start = datetime.datetime.now()
pool = multiprocessing.Pool(4)
for i in range(4):
r = pool.apply_async(calc,(i,),callback=lambda x:logging.info("{}in main".format(i))) # 开始进入子进程计算,没有阻塞下面的代码,下面代码会继续执行
# logging.info(r) # 这里打印的只是一个中间的标识变量.相当于吃饭取号.
# 那么该如何得到函数计算的返回值呢?这里python提供了回调的方法
# callback=lambda x:logging.info("{}in main".format(i))
# 这里的回调是在主进程中完成的
# callback 方法,回调返回值
# print('***********')
# print('++++++++++++++++')
pool.close()
print('===============')
pool.join()
delta = (datetime.datetime.now()-start).total_seconds()
print(delta) # 20.626179
print("====end====")
print(threading.enumerate())
# 回调的意思简单可以理解为执行好了告诉进程,执行完毕.
多进行的方法:
名称 | 说明 |
---|---|
pid | 进程ID |
exitcode | 进程的退出状态码 |
terminate | 终止指定的进程 |
Python在进程间同步提供了和线程同步一样的类,使用的方法一样,使用的效果也类似。
不过,进程间代价要高于线程间,而且系统底层实现是不同的,只不过Python屏蔽了这些不同之处,让用户简单
使用多进程。
multiprocessing还提供共享内存、服务器进程来共享数据,还提供了用于进程间通讯的Queue队列、Pipe管道。
通信方式不同
进程池举例:(进程池,为了开辟的进程之间实现复用,不用多次重复开辟)
multiprocessing.Pool 是进程池类:
名称 | 含义 |
---|---|
apply(self, func, args=(), kwds={}) | 阻塞执行,导致主进程执行其他子进程就像一个个执行 |
apply_async(self, func, args=(), kwds={},callback=None, error_callback=None) | 与apply方法用法一致,非阻塞异步执行,得到结果后会执行回调 |
close() | 关闭池,池不能再接受新的任务,所有任务完成后退出进程 |
terminate() | 立即结束工作进程,不再处理未处理的任务 |
join() | 主进程阻塞等待子进程的退出, join方法要在close或terminate之后使用 |
多进程,多线程的选择
1,CPU密集型
CPython 中使用到了GIL ,多线程的时候锁相互竞争,且多核优势不能发挥,选用Python多进程效率更高
2,I/O密集型
在Python中适合是用多线程,可以减少多进程间IO 的序列化开销,且在IO等待的时候,切换到其他线程继续执
行,效率不错。
应用
请求/应答模型:WEB应用中常见的处理模型
master启动多个worker工作进程,一般和CPU数目相同。发挥多核优势。
worker工作进程中,往往需要操作网络IO和磁盘IO,启动多线程,提高并发处理能力。worker处理用户的请求,
往往需要等待数据,处理完请求还要通过网络IO返回响应。
这就是nginx工作模式。
僵尸进程
一个进程使用了fork创建了子进程,如果子进程终止进入僵死状态,而父进程并没有调用wait或者waitpid获取子进
程的状态信息,那么子进程仍留下一个数据结构保存在系统中,这种进程称为僵尸进程。
僵尸进程会占用一定的内存空间,还占用了进程号,所以一定要避免大量的僵尸进程产生。有很多方法可以避免僵
尸进程。
孤儿进程
父进程退出,而它的子进程仍在运行,那么这些子进程就会成为孤儿进程。孤儿进程会被init进程(进程号为1)收
养,并由init进程对它们完成状态收集工作。
init进程会循环调用wait这些孤儿进程,所以,孤儿进程没有什么危害。
守护进程
它是运行在后台的一种特殊进程。它独立于控制终端并周期性执行某种任务或等待处理某些事件。
守护进程的父进程是init进程,因为其父进程已经故意被终止掉了。
守护进程相对于普通的孤儿进程需要做一些特殊处理。
异步并行任务编程模块,提供一个高级的异步可执行的便利接口。
提供了2个池执行器
ThreadPoolExecutor 异步调用的线程池的Executor
ProcessPoolExecutor 异步调用的进程池的Executor
首先需要定义一个池的执行器对象,Executor类子类对象。
方法 | 含义 |
---|---|
ThreadPoolExecutor(max_workers=1) | 池中至多创建max_workers个线程的池来同时异步执行,返回Executor实例 |
submit(fn, *args, **kwargs) | 提交执行的函数及其参数,返回Future类的实例 |
shutdown(wait=True) | 清理池 |
Future类
方法 | 含义 |
---|---|
done() | 如果调用被成功的取消或者执行完成,返回True |
cancelled() | 如果调用被成功的取消,返回True |
running() | 如果正在运行且不能被取消,返回True |
cancel() | 尝试取消调用。如果已经执行且不能取消返回False,否则返回True |
result(timeout=None) 会阻塞 |
取返回的结果,timeout为None,一直等待返回;timeout设置到期,抛出 concurrent.futures.TimeoutError 异常 |
exception(timeout=None) | 取返回的异常,timeout为None,一直等待返回;timeout设置到期,抛出 concurrent.futures.TimeoutError 异常 |
示例:
from concurrent import futures
import threading
import time
import datetime
import threading
from concurrent.futures import ThreadPoolExecutor
import logging
FORMAT = '%(asctime)-15s\t [%(processName)s:%(threadName)s, %(process)d:%(thread)8d] %(message)s'
logging.basicConfig(level=logging.INFO, format=FORMAT)
def worker(n):
logging.info('begin to work-{}'.format(n))
time.sleep(5)
logging.info('finished {}'.format(n))
# 创建线程池,池容量为3
executer = futures.ThreadPoolExecutor(max_workers=3) # 线程池,最大为3
fs = []
for i in range(3):
future = executer.submit(worker, i)
fs.append(future)
for i in range(3, 6): # 虽然最大池任务为3 ,但是创建大于三个进程,就会使其他进程处于等待状态.等某个进程执行完了,下一个进程继续
future = executer.submit(worker, i) # 给池提交,异步库,不会阻塞
fs.append(future)
while True:
time.sleep(2)
logging.info(threading.enumerate())
flag = True
for f in fs: # 判断是否还有未完成的任务
logging.info(f.done()) # 用done 不会阻塞,但result 会阻塞
flag = flag and f.done()
# if not flag: # 注释了这个if,输出的日志看的清楚些
# break
print('-' * 30)
if flag:
executer.shutdown() # 默认wait = True ,等待进程工作结束才会继续执行 # 关闭线程池
logging.info(threading.enumerate())
break
# 线程池一旦创建了线程,就不需要频繁清除
上面虽然是异步执行,但仍然是一个串行执行,因为有GIL 存在
多进程池只需将上面线程池的executer = futures.ThreadPoolExecutor(max_workers=3)
改为executer = futures.ProcessPoolExecutor(max_workers=3)
即可,但代码要在if __name__ == '__main__':
下执行.
concurrent.futures.ProcessPoolExecutor继承自concurrent.futures._base.Executor
,而父类有__enter__
、
__exit__
方法,支持上下文管理。可以使用with语句。
__exit__
方法本质还是调用的shutdown(wait=True),就是一直阻塞到所有运行的任务完成
使用方法:
with ThreadPoolExecutor(max_workers=1) as executor:
future = executor.submit(pow, 323, 1235)
print(future.result())
将上面的代码改进
executer = futures.ProcessPoolExecutor(max_workers=3)
with executer:
.....
pass
if flag:
break
# 线程池一旦创建了线程,就不需要频繁清除
该库统一了线程池,进程池调用,简化了编程
是Python 简单的思想哲学的提现
唯一缺点,无法设置线程名称,但这不值得一提