一个email下载器:线程安全
在 《一个email下载器:多进程编程中遇到的问题》
文章中,遇到最大的问题就是在多进程解决方案中,子进程中的imap连接不能复用,只能单独创建,这影响了程序执行性能。
最近一直在研究这个问题,虽然还没有解决,但对Python多进程有了更深刻的理解。
1:什么是多进程
核心就是子进程会拷贝父进程的所有数据,内存是独立的。下面用可变对象变量说明这一点:
import multiprocessing as m def worker(lock): print (id(arr)) arr.append(10) print (arr) i=10 arr=[3] lock = m.Lock() jop=[] for ii in range(5): w = m.Process( target=worker, args=(,), ) jop.append(w) w.start() for ii in range(5): jop[ii].join()
运行可以看出arr这个可变类型变量在子进程中是独立的,互相不影响。
如果你想在子进程中共享一个list变量,可以采用Manager对象,代码如下:
def worker(lock): print (id(arr)) arr.append(10) print (arr) arr=m.Manager().list()
那多进程的lock对象呢?如果它也复制到各个子进程中,那么基本就没用了,因为子进程之间无法做到互斥。
在Python中,lock是一个synchronize对象,它在python子进程和线程中本身就是共享的,这一点请务必牢记,所以下面两段代码的作用是完全一样的:
for ii in range(5): w = multiprocessing.Process( target=worker, args=(lock,), ) for ii in range(5): w = multiprocessing.Process( target=worker, args=(,), )
就是说args参数是否传递lock变量,根本无所谓。
2:线程安全
emaildownloader工具中子进程不能复用imap对象在于其不是线程安全的,在一个主程序中不管你创建多少个连接对象(调用参数一致),引用的都是同一个对象(同一片内存区域):
c=imaplib_connect() c1=imaplib_connect() def worker(): print (id(c),id(c1)) w = multiprocessing.Process( target=worker, args=(,), ) print (id(c),id(c1))
可以看出不管在主进程还是子进程中c和c1引用的对象都是一样的,由于imap对象不是python标准的synchronize对象,所以应该选择了保护机制(自己还没了解),导致imap对象不能在子进程中复用。
所谓线程不安全,就是某块内存区域没有受保护(操作imap对象的时候会有修改),Python多进程、多线程拒绝了连接复用。
一个小问题如果想创建不同的imap对象,有什么办法?可以包装在函数中,其存储的变量都在栈内存中,是互相独立的,比如:
def getimap(): return imaplib_connect() c1=getimap() c2=getimap() print (id(c1),id(c2))
3:构建imap对象连接池
为了解决连接复用的问题,我看到网上一个解决方案,而且运行成功了,当然最后是我理解错误了,先看看代码:
def worker(): with lock: connection_id = idle_connections.pop() connection = connections[connection_id] connection.imap.list() with lock: idle_connections.append(connection_id) connections = [imaplib_connect for i in range(10)] manager = multiprocessing.Manager() idle_connections = manager.list(range(10)) lock = multiprocessing.Lock() from multiprocessing.pool import ThreadPool pool = ThreadPool(workers) for ii in range(10): job = pool.apply_async(worker, ii)
因为在python中运行成功了,所以我误以为通过lock获取一个独立的imap对象能够解决复用的问题,它的设计很巧妙,就是提前弄一对imap连接对象池,当我想把这个方案移植到emaildownloader工具时,发现并没有复用连接。
我仔细思考了下,其实不同的子进程还是会使用某一个进程池中的imap对象,即使不并行使用,按照上面理解的,多进程和多线程机制还是会进行保护的(即连接不能复用)。
最后我才发现ThreadPool其实是一个线程池,因为是multiprocessing类库中的对象,我以为都是多进程。
在python官方文档multiprocessing模块中并没有发现ThreadPool的说明,通过查看源代码才找到,multiprocessing.pool.ThreadPool继承之multiprocessing.Pool,功能和接口一样,可内部是基于线程的。
其实在Python中有很多这样的引用,比如multiprocessing.Pool()实际上调用的是multiprocessing.pool.Pool(),在手册中根本没有Pool类;再比如multiprocessing.Lock实际上是一个工厂函数,它返回的是一个multiprocessing.synchronize.Lock类的对象。
在 《一个email下载器:多线程思路》
文章中,好像多线程能够复用imap了解,那为什么还要在ThreadPool多线程池中lock imap连接池中的某个imap连接呢?不是多此一举吗?这个我会在多线程解决方案中再说,本文主要描述多进程不能复用imap连接的问题。
最后,虽然问题还没有解决,但却理解的更透了,以前编写Web代码的时候,很少使用异步、多线程/多进程、非阻塞的模式,都是顺序执行的,通过学习收获确实很多。
近期写的一些文章可能大家不敢兴趣,说的也支离破碎,如果你对Python有兴趣,可以看看emaildownloader工具的源码,相信会有体会。