考虑下面的需求:
我们有一个日志目录,里面全是gzip压缩的日志文件。
每个日志文件的格式是固定的,我们要从中提取出所有访问过robots.txt文件的主机
1.1.1.1 ------------ [10/june/2012:00:18:50 - 0500] "GET /robots.txt ..." 200 71
2.1.1.3 ------------ [12/june/2013:00:18:50 - 0500] "GET /a.txt ..." 202 73
122.1.1.3 ------------ [12/june/2013:00:18:50 - 0500] "GET /robots.txt ..." 202 73
不使用并发,我们会写出如下的程序代码:
import gzip
import glob
import io
def find_robots(filename):
robots = set()
with gzip.open(filename) as f:
for line in io.TextIOWrapper(f, encoding='ascii'):
fields = line.split()
if fields[6] == '/robots.txt':
robots.add(fields[0])
return robots
def find_all_robots(logdir):
files = glob.glob(logdir+'/*.log.gz')
all_robots = set()
for robots in map(find_robots, files):
all_robots.update(robots)
return all_robots
上面的程序以map-reduce的风格来编写。
如果想改写上面的程序以利用多个CPU核心。只需要把map替换成一个类似的操作,并让它在concurrent.futures库中的进程池中执行即可。
下面是稍加修改的代码:
def find_all_robots(logdir):
files = glob.glob(logdir+'/*.log.gz')
all_robots = set()
with ProcessPoolExecutor as pool:
for robots in pool.map(find_robots, files):
all_robots.update(robots)
return all_robots
ProcessPoolExecutor的典型用法如下:
from concurrent.futures import ProcessPoolExecutor
with ProcessPoolExecutor() as pool:
do work in parallel using pool
在底层ProcessPoolExcutor使用N个独立的进程启动python解释器,N为CPU个数,也可以通过参数N传递ProcessPoolExecutor(N).直到with块中的最后一条语句执行完,ProcessPoolExecutor会退出,退出前会等待所有的任务执行完成。
提交到进程池中的任务必须是函数的形式,有2种方法可以提交任务。如果想并行处理一个列表推导式或者map操作,可以使用pool.map,也可以用submit手动提交一个任务。
def work(x):
result = '''
'''
return result
from concurrent.futures import ProcessPoolExecutor
with ProcessPoolExecutor() as pool:
future_result = pool.submit(work)
r = future_result.result()
手动提交任务会返回一个future对象,可以通过result方法获取结果,但是result方法会阻塞到结果返回。
为了不让其阻塞,还可以安装一个处理完成函数。
def when_done(r):
print('Got:', r.result())
with ProcessPoolExecutor() as pool:
future_result = pool.submit(work)
future_result.add_done_callback(when_done)
尽管进程池使用起来非常简单,但是还是要注意下面几点:
1.这种并行化处理技术只适合于可以将问题分解成各个独立部分的情况
2.任务只能定义成普通函数来提交,实例方法,闭包或者其他类型的可调用对象都是不支持并行处理的
3.函数的参数和返回值必须可兼容于pickle编码。任务的执行是在单独的解释器进程中执行的,这中间需要用到进程间通信。因此,不同的解释器间交换数据必须要进行序列化处理
4.提交的工作函数不应该维护持久的状态或者带有副作用。
5.在UNIX环境中,进程池是通过fork系统调用实现的
6.当进程池和线程池结合在一起时要格外小心,通常应该在创建线程池前启动进程池
如何规避GIL的限制。
在Python解释器的C语言实现中,有一部分代码并不是线程安全的,因此并不能完全地并发执行。事实上,解释器被一个称之为全局解释器锁(GIL)的东西保护着,任意时刻只允许有一个python线程执行。GIL带来的最明显影响就是多线程的Python程序无法充分利用多核心CPU的优势(即,一个采用多线程技术的计算密集型应用只能在一个CPU上运行)
要理解GIL,需要知道Python何时释放GIL。
每当阻塞等待I/O操作时解释器都会释放GIL。对于从不执行任何阻塞操作的的CPU密集型线程,Python解释器会在执行一定数量的字节码后释放GIL,以便其他线程得到执行机会。但是C语言扩展模块则不同,调用C函数时GIL会被锁定,直到其返回为止。
由于C代码是不受解释器控制的,这一期间不会执行任何Python字节码,因此解释器就没法释放GIL了。
TALK SO MUCH.要规避GIL限制,通常有2种策略:
1.如果完全使用Python来编程,使用multiprocessing模块来创建进程池,把它当作协处理器来使用。
2.把重点放在C语言扩展上,主要思想就是在计算密集型任务转移到C代码中,使其独立于Python,并在C代码中释放GIL。通过在C代码中插入特殊的宏来实现:
#include "Python.h"
PyObject *pyfunc(PyObject *self, PyObject *args)
{
...
Py_BEGIN_ALLOW_THREADS
// Threaded C code
...
Py_END_ALLOW_THREADS
...
}
如果使用cyptes库或者Cython来访问C代码,那么ctypes会自动释放GIL,不需要我们来干预。