Python:線程、進程與協程(7)——線程池

     前面轉載了一篇分析進程池源碼的博文,是一篇分析進程池很全面的文章,點擊此處可以閱讀。在Python中還有一個線程池的概念,它也有併發處理能力,在一定程度上能提高系統運行效率;不正之處歡迎批評指正。

     線程的生命週期可以分爲5個狀態:創建、就緒、運行、阻塞和終止。自線程創建到終止,線程便不斷在運行、創建和銷燬這3個狀態。一個線程的運行時間可由此可以分爲3部分:線程的啓動時間、線程體的運行時間和線程的銷燬時間。在多線程處理的情景中,如果線程不能被重用,就意味着每次創建都需要經過啓動、銷燬和運行3個過程。這必然會增加系統相應的時間,降低了效率。看看之前介紹線程的博文的例子中(點擊此處可以閱讀),有多少個任務,就創建多少個線程,但是由於Python特有的GIL限制,它並不是真正意義上的多線程,反而會因爲頻繁的切換任務等開銷而降低了性能(點擊此處可以瞭解Python的GIL)。這種情況下可以使用線程池提高運行效率。


        線程池的基本原理如下圖,它是通過將事先創建多個能夠執行任務的線程放入池中,所需要執行的任務通常要被安排在隊列任務中。一般情況下,需要處理的任務比線程數目要多,線程執行完當前任務後,會從隊列中取下一個任務,知道所有的任務完成。

wKioL1hCrKWhht4HAAFhIvzPzXo843.png-wh_50


  由於線程預先被創建並放入線程池中,同時處理完當前任務之後並不銷燬而是被安排處理下一個任務,因此能夠避免多次創建線程,從而節省線程創建和銷燬的開銷,能帶來更好的性能和系統穩定性。所以,說白了,Python的線程池也沒有利用到多核或者多CPU的優勢,只是跟普通的多線程相比,它不用去多次創建線程,節省了線程創建和銷燬的時間,從而提高了性能。

    Python中 線程池技術適合處理突發性大量請求或者需要大量線程來完成任務、但每個任務實際處理時間較短的場景,它能有效的避免由於系統創建線程過多而導致性能負荷過大、響應過慢等問題。下面介紹幾種利用線程池的方法。

(一)自定義線程池模式

我們可以利用Queue模塊和threading模塊來實現線程池。Queue用來創建任務隊列,threading用來創建一個線程池子。

看下面例子

import Queue,threading

class Worker(threading.Thread):
    """
    定義一個能夠處理任務的線程類,屬於自定義線程類,自定義線程類就需要定義run()函數
    """

    def __init__(self,workqueue,resultqueue,**kwargs):
        threading.Thread.__init__(self,**kwargs)
        self.workqueue = workqueue#存放任務的隊列,任務一般都是函數
        self.resultqueue = resultqueue#存放結果的隊列

    def run(self):
        while True:
            try:
                #從任務隊列中取出一個任務,block設置爲False表示如果隊列空了,就會拋出異常
                callable,args,kwargs = self.workqueue.get(block=False)
                res = callable(*args,**kwargs)
                self.resultqueue.put(res)#將任務的結果存放到結果隊列中
            except Queue.Empty:#拋出空隊列異常
                break

class WorkerManger(object):
    """
    定義一個線程池的類
    """
    def __init__(self,num=10):#默認這個池子裏有10個線程
        self.workqueue = Queue.Queue()#任務隊列,
        self.resultqueue = Queue.Queue()#存放任務結果的隊列
        self.workers = []#所有的線程都存放在這個列表中
        self._recruitthreads(num)#創建一系列線程的函數
    def _recruitthreads(self,num):
        """
        創建線程
        """
        for i in xrange(num):
            worker = Worker(self.workqueue,self.resultqueue)
            self.workers.append(worker)

    def start(self):
        """
        啓動線程池中每個線程
        """
        for work in self.workers:
            work.start()

    def wait_for_complete(self):
        """
        等待至任務隊列中所有任務完成
        """
        while len(self.workers):
            worker = self.workers.pop()
            worker.join()
            if worker.isAlive() and not self.workqueue.empty():
                self.workers.append(worker)

    def add_job(self,callable,*args,**kwargs):
        """
        往任務隊列中添加任務
        """
        self.workqueue.put((callable,args,kwargs))


    def get_result(self,*args,**kwargs):
        """
        獲取結果隊列
        """
        return self.resultqueue.get(*args,**kwargs)
        
    def add_result(self,result):
        self.resultqueue.put(result)

上面定義了一個線程池,它的初始化函數__init__()定義了一些存放相關數據的屬性,這在Python的一些內部模塊的類的定義中很常見,所有有時候多看看源碼其實挺好的,學習大神的編程習慣和編程思想。

另外還要提到一點,Queue模塊中的隊列,不僅可以存放數據(指字符串,數值,列表,字典等等),還可以存放函數的(也就是任務),上面的代碼中,callable是一個函數,當用put()將一個函數添加到隊列時,put()接受的參數有函數對象以及該函數的相關參數,而且要是一個整體,所以就有了上面代碼中的self.workqueue.put((callable,args,kwargs))。同理,當從這種存放函數的隊列中取出數據,它返回的就是一個函數對象包括它的相關參數,有興趣的可以打印出上面代碼中run()裏的callable,args,kwargs。如果你對Queue模塊不瞭解,可參考我之前的博文,點擊此處即可閱讀

下面就簡單的舉個小例子吧。

import urllib2,datetime
def open_url(url):
    try:
        res = urllib2.urlopen(url).getcode()
    except urllib2.HTTPError, e:
        res = e.code
    #print res
    res = str(res)
    with open('/home/liulonghua/無標題文檔','wr') as f:
        f.write(res)
    return res
if __name__ == "__main__":
    urls = [
        'http://www.python.org',
        'http://www.python.org/about/',
        'http://www.onlamp.com/pub/a/python/2003/04/17/metaclasses.html',
        'http://www.python.org/doc/',
        'http://www.python.org/download/',
        'http://www.python.org/getit/',
        'http://www.python.org/community/',
        'https://wiki.python.org/moin/',
        'http://planet.python.org/',
        'https://wiki.python.org/moin/LocalUserGroups',
        'http://www.python.org/psf/',
        'http://docs.python.org/devguide/',
        'http://www.python.org/community/awards/'
    ]
    t1 = datetime.datetime.now()
    w = WorkerManger(2)
    for url in urls:
        w.add_job(open_url,url)
    w.start()
    w.wait_for_complete()
    t2 = datetime.datetime.now()
    print t2 - t1

     最後結果如下:

wKiom1hC0TDhmzEvAAApKwBAgLg044.png-wh_50


如果把上面代碼改成用多線程而不是用線程池,會是怎樣的呢?

代碼如下:

if __name__ == "__main__":
    urls = [
        'http://www.python.org',
        'http://www.python.org/about/',
        'http://www.onlamp.com/pub/a/python/2003/04/17/metaclasses.html',
        'http://www.python.org/doc/',
        'http://www.python.org/download/',
        'http://www.python.org/getit/',
        'http://www.python.org/community/',
        'https://wiki.python.org/moin/',
        'http://planet.python.org/',
        'https://wiki.python.org/moin/LocalUserGroups',
        'http://www.python.org/psf/',
        'http://docs.python.org/devguide/',
        'http://www.python.org/community/awards/'
    ]
    t1 = datetime.datetime.now()
    for url in urls:
        t = threading.Thread(target=open_url,args=(url,))
        t.start()
        t.join()
    t2 = datetime.datetime.now()
    print t2-t1

運行結果如下:

wKiom1hC0ZmRlXASAAAoAdUU_os511.png-wh_50

運行效率的差異還是很大的,有興趣的可以動手試試。


(二)使用現成的線程池模塊

下載安裝也很簡單,用pip工具

sudo pip install threadpool

注意:這裏要提到一點,我就陷入這個坑,還好沒有花多長時間就解決了。由於我的電腦裏有python2.7.12,python3.5,還有一個PyPy5.4.1,上面的指令竟然將threadpool包安裝到了PyPy目錄下了,所以在python2.7.12裏,我import threadpool,它一直報錯,如果你的系統裏有多個Python版本,又沒有用virtualenvs虛擬環境工具,很容易造成這種混亂,雖然我安裝了virtualenvs,但在自己的電腦上很少用,這裏的解決方法是:

sudo python -m pip install threadpool

以區分PyPy,同理如果是在PyPy環境下安裝第三方包的話,用sudo pypy -m pip install packagename,這個在之前的博文中也有介紹,感興趣的可以點此

該模塊主要的類和方法:

1.threadpool.ThreadPool:線程池類,主要是用來分派任務請求和收集運行結果。主要方法有:

(1)__init__(self,number_workers,q_size,resq_size=0,poll_timeout=5):

    建立線程池,並啓動對應的num_workers的線程;q_size表示任務請求隊列的大小,resq_size表示存放運行結果隊列的大小。

(2)createWorkers(self,num_workers,poll_timeout=5):

    將num_workers數量對應的線程加入線程池

(3)dismissWorkers(self,num_workers,do_join=False):

    告訴num_workers數量的工作線程在執行完當前任務後退出

(4)joinAllDismissWorkers(self):

    在設置爲退出的線程上執行Thread.join

(5)putRequest(self,request,block=True,timeout=None):

    加入一個任務請求到工作隊列

(6)pool(self,block=False)

    處理任務隊列中新請求。也就是循環的調用各個線程結果中的回調和錯誤回調。不過,當請求隊列爲空時會拋出 NoResultPending 異常,以表示所有的結果都處理完了。這個特點對於依賴線程執行結果繼續加入請求隊列的方式不太適合。

(7)wait(self)

    等待執行結果,直到所有任務完成。當所有執行結果返回後,線程池內部的線程並沒有銷燬,而是在等待新任務。因此,wait()之後依然可以在此調用pool.putRequest()往其中添加任務。

2. threadpool.WorkerThread:處理任務的工作線程,主要有run()方法和dismiss()方法。

3.threadpool.WorkRequest:任務請求類,包含有具體執行方法的工作請求類

__init__(self,callable,args=None,kwds=None,requestID=None,callback=None,exc_callback=None)

創建一個工作請求。

4.makeRequests(callable_,args_list,callback=None,exc_callback=_handle_thread_exception):

主要函數,用來創建具有相同的執行函數但參數不同的一系列工作請求。


有了上面自定義線程池模式的基礎,這個模塊不難理解,有興趣的可以去看看該模塊的源碼。它的使用步驟一般如下:

(1)引入threadpool模塊

(2)定義線程函數

(3)創建線程 池threadpool.ThreadPool()

(4)創建需要線程池處理的任務即threadpool.makeRequests()

(5)將創建的多個任務put到線程池中,threadpool.putRequest

(6)等到所有任務處理完畢theadpool.pool()

將上面的例子用線程池模塊進行修改,代碼如下:

import threadpool
if __name__ == "__main__":
    urls = [
        'http://www.python.org',
        'http://www.python.org/about/',
        'http://www.onlamp.com/pub/a/python/2003/04/17/metaclasses.html',
        'http://www.python.org/doc/',
        'http://www.python.org/download/',
        'http://www.python.org/getit/',
        'http://www.python.org/community/',
        'https://wiki.python.org/moin/',
        'http://planet.python.org/',
        'https://wiki.python.org/moin/LocalUserGroups',
        'http://www.python.org/psf/',
        'http://docs.python.org/devguide/',
        'http://www.python.org/community/awards/'
    ]
    t1 = datetime.datetime.now()
    pool = threadpool.ThreadPool(2)

    requests = threadpool.makeRequests(open_url,urls)
    [pool.putRequest(req) for req in requests]
    pool.wait()
    t2 = datetime.datetime.now()
    print t2-t1

執行結果如下:

wKiom1hC5pyDhoP-AAAuGNHlbfk494.png-wh_50

該模塊的其它方法,感興趣的可以自己動手體會下。

(3)multiprocessing.dummy 執行多線程任務

multiprocessing.dummy 模塊與 multiprocessing 模塊的區別: dummy 模塊是多線程,而 multiprocessing 是多進程, api 都是通用的。

Python3裏的multiprocessing裏也有現成的線程池,如下

from multiprocessing.pool import ThreadPool


有時候看到有人這麼用dummy,from multiprocessing.dummy import Pool as ThreadPool ,把它當作了一個線程池。它的屬性和方法可以參考進程池。將上面的例子可以用這種方法改下代碼如下:

from multiprocessing.dummy import Pool as ThreadPool 
if __name__ == "__main__":
    urls = [
        'http://www.python.org',
        'http://www.python.org/about/',
        'http://www.onlamp.com/pub/a/python/2003/04/17/metaclasses.html',
        'http://www.python.org/doc/',
        'http://www.python.org/download/',
        'http://www.python.org/getit/',
        'http://www.python.org/community/',
        'https://wiki.python.org/moin/',
        'http://planet.python.org/',
        'https://wiki.python.org/moin/LocalUserGroups',
        'http://www.python.org/psf/',
        'http://docs.python.org/devguide/',
        'http://www.python.org/community/awards/'
    ]
    t1 = datetime.datetime.now()
    pool =ThreadPool(2)
    pool.map(open_url,urls)
    pool.close()
    pool.join()
    t2 = datetime.datetime.now()
    print t2-t1


運行結果如下:

wKioL1hC6-fjdzllAAAniQpjKuA876.png-wh_50


我覺得上面三種方法的主體思路還是差不多的,還是比較好理解的,希望對你有幫助,不正之處歡迎批評指正!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章