ThreadPoolExecutor 的用法及实战(二)

243 阅读5分钟

前面介绍了ThreadPoolExecutor 的用法,现在我们从源码的角度来剖析 with ThreadPoolExecutor(5) as thread的使用

第一、with的使用

首先是 with的使用,说明ThreadPoolExecutor进行调用中一定含有__enter____exit__的方法实现

image.png

在ThreadPoolExecutor中我们并没有发现,我们发现他是继承了Executor

image.png

我们发现是在父类中进行了实现,所以我们可以使用with的方式对ThreadPoolExecutor进行调用

第二、分析ThreadPoolExecutor类

cocurrent.future模块中的future的意思是未来对象,可以把它理解为一个在未来完成的操作这是异步编程的基础 。在线程池submit()之后,返回的就是这个future对象,返回的时候任务并没有完成,但会在将来完成。也可以称之为task的返回容器,这个里面会存储task的结果和状态。那ThreadPoolExecutor内部是如何操作这个对象的呢?

init方法

image.png

init方法中主要重要的就是任务队列和线程集合,在其他方法中需要使用到。指定最大的同时存在的线程的数量,指定按指定的来,没指定,就是cpu的数目 * 5.

submit方法

image.png

submit中有两个重要的对象,_base.Future()_WorkItem()对象,_WorkItem()对象负责运行任务和对future对象进行设置,最后会将future对象返回,可以看到整个过程是立即返回的,没有阻塞。

这个方法的理解:我们将我们使用submit的方法是为了将func和参数封装到一个future对象中,并通过这个future对象和主线程之间交付结果,实际上最主要的函数就是_WorkItem对象,我们进入这个函数我们,能够发现,只是初始化workitem对象,准备好future对象funcfunc的参数,然后我们将整理好的workitem对象放入到work队列中,待执行,我们看是不是大于最大的线程数,取决定现在要不要创建线程直接执行,最后将我们的future对象返回。

_WorkItem对象

image.png

_WorkItem对象的职责就是执行任务和设置结果。这里面主要复杂的还是self.future.set_result(result)

线程执行函数--_worker

image.png

这是线程池创建线程时指定的函数入口,主要是从队列中依次取出task执行,我们看work队列非空的时候、从中获取workitem对象,并执行workitem对象的run方法,在执行的run函数中,我们是去尝试执行func, 若func正常执行的话、会将最终的结果放入到我们的future对象中;若func非正常执行的话、会将异常的报错信息放入到我们的future对象中;run函数执行完毕,我们将work_item对象进行清除,继续进行任务的读取;要是work的队列被取干净了,我们就会去执行弱引用指向对象,假设弱引用指向的对象销毁了,不存在了,或者弱引用指向对象的_shutdown是真,我们将None放回到work队列中,要不然,我们删除弱引用指向的对象。

adjust_thread_count方法

image.png

这个方法的含义很好理解,主要是创建指定的线程数。当我们的总线程数大于额定线程数量的时候,我们将其就不创建了,保持线程池中线程的个数。

其实真正开启线程的函数是这个
核心代码

if num_threads < self._max_workers:
    thread_name = '%s_%d' % (self._thread_name_prefix or self,
                             num_threads)
    t = threading.Thread(name=thread_name, target=_worker,
                         args=(weakref.ref(self, weakref_cb),
                               self._work_queue))
    t.daemon = True
    t.start()
    self._threads.add(t)
    _threads_queues[t] = self._work_queue
  • 第一、进行了当前线程数量和线程池中最多数量的判断,看是否要启动新线程
  • 第二、当前线程数量小于线程池的额定线程数量的时候,使用threading.Thread进行线程的生成
  • 第三、我们看Thread的参数:target 是 _worker, args 是 weakref.ref(self, weakref_cb),self._work_queue,weakref.ref的两个参数一个是对于当前的ThreadPoolExecutor对象的弱引用,第二个是回调函数,self._work_queue是得到我们的work队列
  • 第四、我们将线程设置成守护线程
  • 第五、我们使用start方法开启线程
  • 第六、将线程加入到self._threads线程列表中,并且上面我们定义了一个弱应用字典,将我们的work的队列,放入到我们的_threads_queues = weakref.WeakKeyDictionary(),键是线程,值是worker的队列

shutdown

image.png

总结

  • future的设计理念很棒,在线程池/进程池和携程中都存在future对象,是异步编程的核心。

  • ThreadPoolExecutor 让线程的使用更加方便,减小了线程创建/销毁的资源损耗,无需考虑线程间的复杂同步,方便主线程与子线程的交互。

  • 线程池的抽象程度很高,多线程和多进程的编码接口一致。

Future

  • concurrent.futures和asyncio中的Future类的作用相同,都表示可能己经完成或尚未完成的延迟计算
  • Future封装待完成的操作,可以放入队列,完成的状态可以查询,得到结果后可以获取结果
  • 使用exector.submit()方法提交执行的函数并获取一个Future,而不是直接创建,传入的参数是一个可调用的对象;获取的Future对象有一个done()方法,判断该Future是否己完成, add_one_callback()设置回调函数, result()来获取Future的结果。as_completed()传一个Future列表,在Future都完成之后返回一个迭代器(最重点)
  • 使用submit提交执行的函数到线程池中,并返回futer对象(非阻塞)
  • exector.submit()和futures.as_completed()这个组合比exector.map()更灵活,submit()可以处理不同的调用函数和参数,而map只能处理同一个可调用对象

这是ThreadPoolExecutor的全部代码,还是比较简单的,我们上面提到的线程池的具备的四个基本功能来分析:线程池管理器工作线程任务队列任务接口来分析

  • 线程池管理器: _threads_queues
  • 工作线程:_worker
  • 任务队列:_work_queue
  • 任务接口:submit

submit(func) 干了两件事:(1)把任务(func)放入queue中,(2)开启一个新线程不断从queue中取出任务,执行woker.run(),即执行func()
_adjust_thread_count()干了两件事:(1) 开启一个新线程执行_worker函数,这个函数的作用就是不断去queue中取出worker,执行woker.run(),即执行func() (2) 把新线程跟队列queue绑定,防止线程被join(0)强制中断

  • 关于shutdown的执行
    • 当executor执行shutdown()方法时executor._shutdown为True,同时会放入None到队列、当work_item.run()执行完毕时,又会进入到下一轮循环从queue中获取worker对象,但是由于shutdown()放入了None到queue,因此取出的对象是None,从而判断这里的if条件分支,发现executor._shutdown是True,又放入一个None到queue中,是来通知其他线程跳出while循环的,队列是用来通知其他线程中的某一个线程结束的,这样连锁反应使得所有线程执行完func中的逻辑后都会结束 可以看出,这个 _worker方法的作用就是在新新线程中不断获得queue中的worker对象,执行worker.run()方法,执行完毕后通过放入None到queue队列的方式来通知其他线程结束。
      再来看看_adjust_thread_count()方法中的_threads_queues[t] = self._work_queue这个操作是如何实现防止join(0)的操作强制停止正在执行的线程的

调用过程解析

26.png