python多线程

来源:互联网 发布:未来人工智能机器人 编辑:程序博客网 时间:2024/06/14 16:19

threading

最近工作上要使用到python多线程,由于之前没有写过,所以第一反应是查阅官方文档,找到了threading模块,但随即发现python实现多线程的局限:GIL 
(PS:GIL存在与否取决于python采用的编译器,比如cpython有GIL,JPython就没有) 
虽然代码并不是CPU密集型,但本强迫症还是决定换用multiprocessing。

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

Pickle Error

以类的形式封装子进程代码,如果按python官方文档给的demo的方式调用类方法,会抛出

PicklingError: Can't pickle <type 'instancemethod'>: attribute lookup _builtin__.instancemethod failed
  • 1
  • 2

原因是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")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

因为我把取数据的操作封装在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._resultsns.setB(key)pool.join()ns.setResultSet(d)return ns
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

这里应该还可以继续优化,因为不要求结果有序,所以可以把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

原创粉丝点击