之前文章对python中进程池的原理、数据流以及应用从代码角度做了简单的剖析,现在让我们回头看看标准库中对进程池的实现都有哪些值得我们学习的地方。我们知道,进程池内部由多个线程互相协作,向客户端提供可靠的服务,那么这些线程之间是怎样做到数据共享与同步的呢?在客户端使用apply/map函数向进程池分配任务时,使用self._taskqueue来存放任务元素,_taskqueue定义为Queue.Queue(),这是一个python标准库中的线程安全的同步队列,它保证通知时刻只有一个线程向队列添加或从队列获取元素。这样,主线程向进程池中分配任务(taskqueue.put),进程池中_handle_tasks线程读取_taskqueue队列中的元素,两个线程同时操作taskqueue,互不影响。进程池中有N个worker进程在等待任务下发,那么进程池中的_handle_tasks线程读取出任务后,又如何保证一个任务不被多个worker进程获取到呢?我们来看下_handle_tasks线程将任务读取出来之后如何交给worker进程的:

for taskseq, set_length in iter(taskqueue.get, None):
i = -
for i, task in enumerate(taskseq):
if thread._state:
debug('task handler found thread._state != RUN')
break
try:
put(task)
except Exception as e:
job, ind = task[:]
try:
cache[job]._set(ind, (False, e))
except KeyError:
pass
else:
if set_length:
debug('doing set_length()')
set_length(i+)
continue
break
else:
debug('task handler got sentinel')
在从taskqueue中get到任务之后,对任务中的每个task,调用了put函数,这个put函数实际上是将task放入了管道,而主进程与worker进程的交互,正是通过管道来完成的。
再来看看worker进程的定义:
w = self.Process(target=worker,
args=(self._inqueue, self._outqueue,
self._initializer,
self._initargs, self._maxtasksperchild)
)
其中self._inqueue和self._outqueue为SimpleQueue()对象,实际是带锁的管道,上述_handle_task线程调用的put函数,即为SimpleQueue对象的方法。我们看到,这里worker进程定义均相同,所以进程池中的worker进程共享self._inqueue和self._outqueue对象,那么当一个task元素被put到共享的_inqueue管道中时,如何确保只有一个worker获取到呢,答案同样是加锁,在SimpleQueue()类的定义中,put以及get方法都带有锁,进行同步,唯一不同的是,这里的锁是用于进程间同步的。这样就保证了多个worker之间能够确保任务的同步。与分配任务类似,在worker进程运行完之后,会将结果put会_outqueue,_outqueue同样是SimpleQueue类对象,可以在多个进程之间进行互斥。

  在worker进程运行结束之后,会将执行结果通过管道传回,进程池中有_handle_result线程来负责接收result,取出之后,通过调用_set方法将结果写回ApplyResult/MapResult对象,客户端可以通过get方法取出结果,这里通过使用条件变量进行同步,当_set函数执行之后,通过条件变量唤醒阻塞在get函数的主进程。

  进程池终止工作通过调用Pool.terminate()来实现,这里的实现很巧妙,用了一个可调用对象,将终止Pool时的需要执行的回调函数先注册好,等到需要终止时,直接调用对象即可。

self._terminate = Finalize(
    self, self._terminate_pool,
    args=(self._taskqueue, self._inqueue, self._outqueue, self._pool,
self._worker_handler, self._task_handler,
self._result_handler, self._cache),
    exitpriority=15
)
在Finalize类的实现了__call__方法,在运行self._terminate()时,就会调用构造self._terminate时传入的self._terminate_pool对象。

  使用map/map_async函数向进程池中批量分配任务时,使用了生成器表达式:

self._taskqueue.put((((result._job, i, mapstar, (x,), {}) for i, x in enumerate(task_batches)), None))
生成器表达式很简单,只需把列表解析的的[]换成()即可,上述表达的列表解析表示为:
[(result._job, i, mapstar, (x,), {}) for i, x in enumerate(task_batches)]
这里使用生成器表达式的好处是,它相当于列表解析的扩展,是对内存有好的,因为它只是生成了一个生成器,当我们需要使用该生成器对应的逻辑目标数据时,它才会通过既定逻辑去生成该数据,所以不会大量占用内存。

  在Pool中,_worker_handler线程负责监控、创建新的工作进程,在监控工作进程退出时,同时将退出的进程从进程池中删除掉。这类似于,一边遍历一边删除列表。我们来看下下面代码的实现:

>>> l = [1, 2, 3, 3, 4, 4, 4, 5]
>>> for i in l:
if i in [3, 4, 5]:
l.remove(i) >>> l
[1, 2, 3, 4, 5]

我们看到l没有将所有的3和4都删除掉,这是因为remove改变了l的大小。再看下面的实现:

>>> l = [1, 2, 3, 3, 4, 4, 4, 5]
>>> for i in range(len(l)):
if l[i] in [3, 4]:
del l[i] Traceback (most recent call last):
File "<pyshell#37>", line 2, in <module>
if l[i] in [3, 4]:
IndexError: list index out of range
>>>

同样因为del l[i]时,l的大小改变,继续访问下去导致访问越界。而标准库中的进程池给出了遍历删除的一个正确示例:

for i in reversed(range(len(self._pool))):
worker = self._pool[i]
if worker.exitcode is not None:
worker.join()
cleaned = True
del self._pool[i]

使用reversed,从后向前删除list中的元素,这样会保证所有符合删除条件的元素被删除掉:

>>> l = [1, 2, 3, 3, 4, 4, 4, 5]
>>> for i in reversed(range(len(l))):
if l[i] in [3, 4, 5]:
del l[i] >>> l
[1, 2]

  可以看出,一个篇幅并不算大的Pool模块,就有很多值得学习的地方。对于python亦或者其他语言,技能的提升,多阅读标准库中代码,是一个很不错的选择。对于我们经常使用,而不知其中实现奥秘的源码,多阅读源码,了解其技术实现,就像侯捷那本《STL源码剖析》中讲到的,源码之前,了无秘密。更重要的是,将这些漂亮而又高效的编码方式,运用在自己的工作中,让自己的代码也可以像标准库中的代码一样优雅,这可以说是每一个开发人员的追求。

python进程池剖析(三)的更多相关文章

  1. python进程池剖析(一)

    python中两个常用来处理进程的模块分别是subprocess和multiprocessing,其中subprocess通常用于执行外部程序,比如一些第三方应用程序,而不是Python程序.如果需要 ...

  2. python进程池剖析(二)

    之前文章中介绍了python中multiprocessing模块中自带的进程池Pool,并对进程池中的数据结构和各个线程之间的合作关系进行了简单分析,这节来看下客户端如何对向进程池分配任务,并获取结果 ...

  3. Python进程池multiprocessing.Pool的用法

    一.multiprocessing模块 multiprocessing模块提供了一个Process类来代表一个进程对象,multiprocessing模块像线程一样管理进程,这个是multiproce ...

  4. python进程池:multiprocessing.pool

    本文转至http://www.cnblogs.com/kaituorensheng/p/4465768.html,在其基础上进行了一些小小改动. 在利用Python进行系统管理的时候,特别是同时操作多 ...

  5. python(进程池/线程池)

    进程池 import multiprocessing import time def do_calculation(data): print(multiprocessing.current_proce ...

  6. 万里长征第一步:Python进程池的一点点小坑

    # -*- coding: utf- -*- """ Created on Thu Mar :: @author: lilide """ # ...

  7. python进程池

    当需要创建的子进程数量不多时,可以直接利用multiprocessing中的Process动态成生多个进程,但如果是上百甚至上千个目标,手动的去创建进程的工作量巨大,此时就可以用到multiproce ...

  8. python 进程池的简单使用方法

    回到python,用一下python的进程池. 记得之前面试的时候,面试官问:你知道进程池的默认参数吗? 我没有回答上来,后来才知道,是有默认参数的.下面就看看它的默认参数 1. 不加参数 from ...

  9. python 进程池的使用和坑

    from multiprocessing import Pool,Process import time,os def Foo(a):#创建函数 time.sleep(2) print('in the ...

随机推荐

  1. Boost.Asio c++ 网络编程翻译(26)

    Boost.Asio-其他特性 这章我们讲了解一些Boost.Asio不那么为人所知的特性.标准的stream和streambuf对象有时候会更难用一些,但正如你所见.它们也有它们的益处.最后,你会看 ...

  2. Suse发生了错误Access denied for user &#39;&#39;@&#39;localhost&#39; to&amp;

    好久没实用MySQL了,上次由于装了Banq的论坛系统.在用MySQL Administrator进去的时候居然提示mysql error number 1045 access denied for ...

  3. 新秀翻译(两)——使用Java通用配置模板方法模式

    假设你发现你已经非常重码,你可能会考虑使用模板的方法来消除easy重复错误代码.下面是一个示例:以下两类,他完成了几乎相同的功能: 实例化并初始化一个Reader来读取CSV文件. 读取每一行并解析: ...

  4. mybatis型材xxxx.xml缺少后果返回类型

    下面是一个mybatis型材xxxx.xml失踪resultMap错误: 严重: Servlet.service() for servlet [SpringMVC] in context with p ...

  5. HDU 4916 Count on the path

    意甲冠军: 考虑到一棵树,m询价  不要求回答每一次询价u和v通过在两个节点形成的最低等级点路径 思路: 一開始以为是LCA-  只是T了好几次-  后来发现不用LCA也可做 考虑每一个询问u和v   ...

  6. EXCEL Pivot table manipulate

    Add filter For the Demo time,I would like to filter out the products which not in Red and Black colo ...

  7. hdu4864Task(馋)

    主题链接: 啊哈哈.点我 题目: Task Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Ot ...

  8. Claris and XOR

    Problem Description Claris loves bitwise operations very much, especially XOR, because it has many b ...

  9. bigdata_hadoop集群配置_内存分配

    haoop集群  做好内存管理跟重要,不然经常会给抛出个 OutMemory   ,内存溢出 以horntonworks给出推荐配置为样本,给出一种常见的Hadoop集群上各组件的内存分配方案.配置时 ...

  10. do...while(0)神奇

    1. do...while(0)消除goto语句. 通常,假设在一个函数中開始要分配一些资源.然后在中途运行过程中假设遇到错误则退出函数,当然,退出前先释放资源,我们的代码可能是这样: version ...