最近工作上要使用到python多线程,由于之前没有写过,所以第一反应是查阅官方文档,找到了threading模块,但随即发现python实现多线程的局限:GIL
(PS:GIL存在与否取决于python采用的编译器,比如cpython有GIL,JPython就没有)
虽然代码并不是CPU密集型,但本强迫症还是决定换用multiprocessing。
multiprocessing模块实际上是多进程,既然多线程无法绕过GIL,那就干脆采用多进程,但由于进程之间数据不共享,所以多进程之间存在交换数据的问题,为此multiprocessing提供了Queue、Pipe和Manager模块三种方法来实现在进程之间交换数据。
除了创建/销毁/启动进程之外,multiprocessing还提供了进程池、一个方便的map方法以及进程之间通信的实现。
我的需求是在主进程里开启四个子进程,异步运行,这四个子进程分别向一个变量里存储数据,子进程的类实现如下:
class NameSearcher:
_results = {}
# 每个process异步地向managerDict添加值,SyncManger提供的dict是同步的
def setRs(self, key, managerDict):
hiveconn = HiveConnector()
#出于保密考虑业务相关代码替换成a、b、c、d
if key == 'a':
rs = hiveconn.fetchData(sql1)
if key == 'b':
rs = hiveconn.fetchData(sql2)
if key == 'c':
rs = hiveconn.fetchData(sql3)
if key == 'd':
rs = hiveconn.fetchData(sql4)
result = {}
for r in rs:
if r[1] is not None and r[0] is not None:
result[r[0]] = r[1]
if result:
managerDict[key] = result
print key, len(managerDict[key])
def getName(self, id, type):
return self._results[type][id].decode('utf-8')
def setResultSet(self, rs):
self._results.update(rs)
def printRs(self):
for r in self._results:
print len(r),'\t'
#为了解决PickleError用来包装NameSearcher的函数
def runProcess(ns_instance, key, managerDict):
ns_instance.setRs(key, managerDict)
以类的形式封装子进程代码,如果按python官方文档给的demo的方式调用类方法,会抛出
PicklingError: Can't pickle : attribute lookup _builtin__.instancemethod failed
原因是python 2.X不能pickle实例方法(3.X可以),而multiprocessing must pickle things to sling them among processes,参考stackoverflow pickle error
关于pickle,看文档的介绍我理解为python的序列化模块,查阅what can be pickled and unpickled 看哪些python object能被pickle
没办法改multiprocessing用的序列化模块,就只能另辟蹊径。为了解决这个PickleError,可以在外层套一个函数,就是上面代码中和NameSearcher同层缩进的runProcess()函数,由于函数可以被pickle,就这样骗过了编译器。
(PS:在上面贴的stackoverflow链接中还提供了两个方法:1.继承Object类重写__call__方法,我试了不传参的话有效,传参的话就还是会报错;2.用pathos.multiprocessing,亲测无效)
先贴一下在主函数里异步运行runProcess方法的代码:
def main():
logging.info("hiveSearcher begin")
# 通过Manager提供的进程间共享字典存储结果
d = Manager().dict()
l = Manager().list()
# NameSearcher对象
ns = NameSearcher()
keySet = ['a', 'b', 'c', 'd']
pool = Pool(4)
for key in keySet:
pool.apply_async(func=runProcess, args=(ns, key, d))
pool.close()
pool.join()
ns.setResultSet(d)
return ns
logging.info("hiveSearcher end")
因为我把取数据的操作封装在NameSearcher类里了,所以这里的main()函数我需要返回一个NameSearcher对象,当然这并不是必要代码,仅仅在此说明一下。
通过multiprocessing提供的进程池pool,调用apply_async()异步运行子进程,pool.join()表示主线程等待直到进程池中的所有进程都运行完毕。
为了在把四个子进程的结果放到一个字典里,我一开始想的是用类变量保存数据,因为四个子进程使用的对象都是ns,我就认为它们会使用同一个类变量,这种推想在多线程里没有问题,但由于multiprocessing是多进程,所以虽然ns看起来是同一个ns,但它传给四个子进程的是四份副本,所以类变量并不能共享。
(但同样本来同样写成类变量的hiveconn却会在一个进程结束被回收掉之后导致另一个进程也不能使用,似乎四个子进程共享了一个hiveconn,难道每个副本指向一个相同的引用?那四个进程改变的_results最终不能反映到ns._result上又作何解释?)
代码中通过Manager.dict()提供的一个同步的字典对象在进程间交换数据。
代码看起来没有任何错误,在dict持有的数据量的情况下(我测试是30w以下)也运行正常,但超过30W以后程序就会一直等待(等了一个多小时)运行不下去,用ps -ef | grep python命令查看进程也会看到子进程没有一个中止,非常诡异。
我只好认为是multiprocessing提供的进程间交换数据的方式不支持过大的数据量。
改为把数据量超过的b放到主进程里调用就解决了问题:
keySet = ['a', 'c', 'd']
pool = Pool(4)
for key in keySet:
pool.apply_async(func=runProcess, args=(ns, key, d))
pool.close()
key = 'b'
#setB的内容是用b的查询结果更新self._results
ns.setB(key)
pool.join()
ns.setResultSet(d)
return ns
这里应该还可以继续优化,因为不要求结果有序,所以可以把b的查询(结果有一千多万条)通过limit分成多个进程并行执行,在每个进程里加个锁更新self._results,懒得再弄了
————————————更新————————————
还是尝试写了通过limit分页并发查询,然后发现hive不支持limit offset,stackoverflow上的歪果仁提供了通过ROW_NUMBER函数生成rowid来分页的方法
由于查询sql中有group by的部分,而where先于group by,所以应该不会成功。
另:还发现了httplib的一个bug,httpclient.request()这句会报TypeError:a float is required,设置一下timeout即可fix