python 学习之路 ---多进程精讲

多进程

由于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管道。

通信方式不同

  1. 多进程就是启动多个解释器进程,进程间通信必须序列化、反序列化
  2. 数据的线程安全性问题
    如果每个进程中没有实现多线程,GIL可以说没什么用了

进程池举例:(进程池,为了开辟的进程之间实现复用,不用多次重复开辟)

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工作模式。

Linux 的特殊进程


僵尸进程

一个进程使用了fork创建了子进程,如果子进程终止进入僵死状态,而父进程并没有调用wait或者waitpid获取子进
程的状态信息,那么子进程仍留下一个数据结构保存在系统中,这种进程称为僵尸进程。

僵尸进程会占用一定的内存空间,还占用了进程号,所以一定要避免大量的僵尸进程产生。有很多方法可以避免僵
尸进程。

孤儿进程

父进程退出,而它的子进程仍在运行,那么这些子进程就会成为孤儿进程。孤儿进程会被init进程(进程号为1)收
养,并由init进程对它们完成状态收集工作。

init进程会循环调用wait这些孤儿进程,所以,孤儿进程没有什么危害。

守护进程

它是运行在后台的一种特殊进程。它独立于控制终端并周期性执行某种任务或等待处理某些事件。

守护进程的父进程是init进程,因为其父进程已经故意被终止掉了。

守护进程相对于普通的孤儿进程需要做一些特殊处理。

concurrent包


concurrent.futures

异步并行任务编程模块,提供一个高级的异步可执行的便利接口。

提供了2个池执行器

ThreadPoolExecutor 异步调用的线程池的Executor

ProcessPoolExecutor 异步调用的进程池的Executor

ThreadPoolExecutor对象


首先需要定义一个池的执行器对象,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 存在

ProcessPoolExecutor对象

多进程池只需将上面线程池的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 简单的思想哲学的提现

唯一缺点,无法设置线程名称,但这不值得一提

你可能感兴趣的:(Python学习历程,python)