前面介绍了ThreadPoolExecutor
的用法,现在我们从源码的角度来剖析 with ThreadPoolExecutor(5) as thread
的使用
第一、with的使用
首先是 with的使用,说明ThreadPoolExecutor进行调用
中一定含有__enter__
和__exit__
的方法实现
在ThreadPoolExecutor中我们并没有发现,我们发现他是继承了Executor中
我们发现是在父类中进行了实现,所以我们可以使用with的方式对ThreadPoolExecutor进行调用
第二、分析ThreadPoolExecutor类
cocurrent.future
模块中的future
的意思是未来对象,可以把它理解为一个在未来完成的操作这是异步编程的基础 。在线程池submit()
之后,返回的就是这个future
对象,返回的时候任务并没有完成,但会在将来完成。也可以称之为task的返回容器,这个里面会存储task的结果和状态。那ThreadPoolExecutor
内部是如何操作这个对象的呢?
init方法
init
方法中主要重要的就是任务队列和线程集合,在其他方法中需要使用到。指定最大的同时存在的线程的数量,指定按指定的来,没指定,就是cpu的数目 * 5.
submit方法
submit
中有两个重要的对象,_base.Future()
和_WorkItem()
对象,_WorkItem()
对象负责运行任务和对future
对象进行设置,最后会将future
对象返回,可以看到整个过程是立即返回的,没有阻塞。
这个方法的理解:我们将我们使用
submit
的方法是为了将func和参数封装到一个future对象中,并通过这个future对象和主线程之间交付结果,实际上最主要的函数就是_WorkItem
对象,我们进入这个函数我们,能够发现,只是初始化workitem对象,准备好future对象
,func
,func的参数
,然后我们将整理好的workitem对象放入到work队列中,待执行,我们看是不是大于最大的线程数,取决定现在要不要创建线程直接执行,最后将我们的future对象返回。
_WorkItem对象
_WorkItem
对象的职责就是执行任务和设置结果。这里面主要复杂的还是self.future.set_result(result)
。
线程执行函数--_worker
这是线程池创建线程时指定的函数入口,主要是从队列中依次取出task执行,我们看work队列非空的时候、从中获取workitem对象,并执行workitem对象的run方法,在执行的run函数中,我们是去尝试执行func, 若func正常执行的话、会将最终的结果放入到我们的future对象中;若func非正常执行的话、会将异常的报错信息放入到我们的future对象中;run函数执行完毕,我们将work_item对象进行清除,继续进行任务的读取;要是work的队列被取干净了,我们就会去执行弱引用指向对象
,假设弱引用指向的对象销毁了,不存在了,或者弱引用指向对象的_shutdown
是真,我们将None放回到work队列中,要不然,我们删除弱引用指向的对象。
adjust_thread_count方法
这个方法的含义很好理解,主要是创建指定的线程数。当我们的总线程数大于额定线程数量的时候,我们将其就不创建了,保持线程池中线程的个数。
其实真正开启线程的函数是这个
核心代码
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
总结
-
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)的操作强制停止正在执行的线程的
调用过程解析