一个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工具的源码,相信会有体会。