python:单线程,多线程,多进程,针对cpu密集型任务的计算速度对比

1,015 阅读4分钟

0/本文提纲

<1>有了多线程threading,为啥还要用multiprocessing
<2>多进程multiprocessing知识梳理
<3>代码实战:单线程,多线程,多进程对比cpu密集型任务的计算速度
<4>最后对池化技术进行解释

1/有了多线程threading,为啥还要用multiprocessing

GIL: 全局解释器锁
GIL在IO操作时会主动释放 (IO密集型任务)
GIL会根据执行的字节码行数以及时间片释放GIL(CPU密集型任务)

multiprocessing模块是python为了解决GIL缺陷而引入的一个模块,
原理是用多进程在多cpu(核心)上并行执行。

image.png

从上图中可以知道:
      对于IO密集型任务,使用python多线程,虽然因为GIL的存在导致任务是并发执行的(不是并行执行),
      但是由于IO的存在,所有多线程依然可以加速运行。
      比如thread1先执行,等thread1遇到了IO,这个时候thread1释放GIL,
      然后thread2获得GIL,然后开始执行,同时thread1正在IO,
      所以这样情况下是可以加速运行的。

      但是对于CPU密集型任务,由于IO很少甚至没有,
      如果使用多线程,则刚run 100 ticks的时候,线程会自动切换,
      这样反而变成了负担,减慢了执行速度。

所以cpu密集型任务,不适合使用多线程。

2/多进程multiprocessing知识梳理

 python提供的threading模块和multiprocessing模块很像,语法几乎完全一样,
 这是python官方有意而为之的,
 目的是让开发这可以方便无缝的迁移。

image.png

3/代码实战:单线程,多线程,多进程对比cpu密集型任务的计算速度

image.png

import time 
import math
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor

PRIMES = [56687541674151654] * 100  # 大小为100的列表,每个元素都很大

# 判断一个数字是否为素数
def func_is_prime(n):
   if n < 2:
      return False
   if n == 2:
      return True
   if n % 2 == 0:
      return False
   
   sqrt_n = int( math,floot(math.sqrt(n)) )
   for i in range(3,sqrt_n+1,2):
      if n % i == 0:
         return False
   
   return True

# 单线程
def single_thread():
   for i in PRIMES:
      func_is_prime(i)

# 多线程
def multi_thread():
   with ThreadPoolExecutor as pool:
      pool.map(func_is_prime,PRIMES)
  
# 多进程
def multi_process():
   with ProcessPoolExecutor as pool:
      pool.map(func_is_prime,PRIMES)
      
      
if __name__ == "__main__":
    start_time = time.time()
    single_thread()
    end_time = time.time()
    print('单线程所用时间为:',end_time - start-time)
   
    start_time = time.time()
    multi_thread()
    end_time = time.time()
    print('多线程所用时间为:',end_time - start-time)
    
    start_time = time.time()
    multi_process()
    end_time = time.time()
    print('多进程所用时间为:',end_time - start-time)
    

4/最后对池化技术进行解释

<1>基类Executor

Executor类是ThreadPoolExecutor和ProcessPoolExecutor的基类。
ThreadPoolExecutor和ProcessPoolExecutor分别对threading和multiprocessing进行了高级抽象,暴露出简单的统一接口。
它为我们提供了如下方法:
(1)submit(fn,*args,**kwargs)
    提交任务。以fn(*args **kwargs) 方式执行并返回 Future 对像。
    fn: 函数地址。
    *args:位置参数。
    **kwargs:关键字参数

(2)map(func,*iterables,timeout=None,chunksize=1)
    1)func: 函数地址。
    2)iterables: 一个可迭代对象,以迭代的方式将参数传递给函数。
    3)timeout: 这个参数没弄明白,如果是None等待所有进程结束。
    4)chunksize: 使用ProcessPoolExecutor时,
                这个方法会将iterables分割任务块,并作为独立的任务提交到执行池中。
                这些块的数量可以由chunksize指定设置。
                对很长的迭代器来说,重新设置chunksize值会比默认值1能显著地提高性能。 
                chunksize对ThreadPoolExecutor没有效果。

    5shutdown(wait=True):如果为True会等待线程池或进程池执行完成后释放正在使用的资源。
    如果wait=False,将立即返回,所有待执行的期程完成执行后会释放已分配的资源。 
    不管wait的值是什么,整个Python程序将等到所有待执行的期程完成执行后才退出。

<2>线程池对象

ThreadPoolExecutor是Executor的子类,下面介绍ThreadPoolExecutor的参数。

class concurrent.futures.ThreadPoolExecutor(
                                        max_workers=None, 
                                        thread_name_prefix='', 
                                        initializer=None, 
                                        initargs=()):

max_workers:线程池的数量。
thread_name_prefix:线程名前缀。默认线程名ThreadPoolExecutor-线程数。
initializer:一个函数或方法,在启用线程前会调用这个函数(给线程池添加额外任务)。
initargs  以元祖的方式给initializer中的函数传递参数。

这里需要说明的是除了max_workers这个参数外,
其它三个参数基本很少用。
max_workers很好理解就是线程池的数量,就是线程池里最多可以放多少个线程。

<3>进程池对象

ProcessPoolExecutor类也是Executor的子类,
下面是ProcessPoolExecutor参数介绍:
class concurrent.futures.ProcessPoolExecutor(
                                        max_workers=None, 
                                        mp_context=None, 
                                        initializer=None, 
                                        initargs=())

max_workers:进程数,及进程池子里最多的进程的数量,及我们打算开启多少个进程。
             如果max_workers为None或未给出(默认),它将默认为机器的处理器个数(及总cpu核心数)。
             如果max_workers小于等于0,则将引发 ValueError。 
             在 Windows上,max_workers必须小于等于 61,否则将引发ValueError。 
             如果max_workers为None,则所选择的默认最多为61,即使存在更多处理器。

mp_context:可以是一个多进程上下文或是 None。它将被用来启动工作进程。
            如果 mp_context 为 None 或未给出,将使用默认的多进程上下文。

initializer:一个函数或方法,在启用线程前会调用这个函数。
initargs :以元组的方式给initializer中的函数传递参数。
关于说initializer和initargs 与ThreadPoolExecutor 类似这里不多说了。

image.png

从上图可以知道,在对类ProcessPoolExecutor进行实例化对象的时候,
初始化一个数据,作为进程池中开启的进程的数量。
如果不给这个数字,则默认为服务器的cpu的总核心数
shutdown()函数,等价于close+join,关闭进程池,
另外堵塞主进程,及等待进程池中的所有任务都执行完毕了,再执行主进程。