python 徹底解讀多線程與多進程


title: 多線程與多進程
copyright: true
top: 0
date: 2019-03-03 16:16:41
tags: 多線程多進程
categories: Python高階筆記
permalink:
password:
keywords:
description: 對python的多線程多進程進一步刨析。

真是這樣的話,有些話,只有準確的時間準確的地點親口說出來。現在時間錯過了,再說也沒用了

在此之前請完整閱讀完

Python threading 多線程模塊

Python multiprocess 多進程模塊

GIL 全局解釋器鎖

GIL(全局解釋器鎖,GIL 只有cpython有):在同一個時刻,只能有一個線程在一個cpu上執行字節碼,沒法像c和Java一樣將多個線程映射到多個CPU上執行,但是GIL會根據執行的字節碼行數(爲了讓各個線程能夠平均利用CPU時間,python會計算當前已執行的微代碼數量,達到一定閾值後就強制釋放GIL)和時間片以及遇到IO操作的時候主動釋放鎖,讓其他字節碼執行。

作用:限制多線程同時執行,保證同一個時刻只有一個線程執行。

原因:線程並非獨立,在一個進程中多個線程共享變量的,多個線程執行會導致數據被污染造成數據混亂,這就是線程的不安全性,爲此引入了互斥鎖。

互斥鎖:即確保某段關鍵代碼的數據只能又一個線程從頭到尾完整執行,保證了這段代碼數據的安全性,但是這樣就會導致死鎖。

死鎖:多個子線程在等待對方解除佔用狀態,但是都不先解鎖,互相等待,這就是死鎖。

基於GIL的存在,在遇到大量的IO操作(文件讀寫,網絡等待)代碼時,使用多線程效率更高。

多線程

一個CPU再同一個時刻只能執行一個線程,但是當遇到IO操作或者運行一定的代碼量的時候就會釋放全局解釋器鎖,執行另外一個線程。

就好像你要燒水和拖地,這是兩個任務,如果是單線程來處理這兩個任務的話,先燒水,等水燒開,再拖地。這樣等待水燒開的時間就白白浪費了,倘若事交給多線程來做的話,就先燒水,燒水的過程中(相當於IO操作的時候)把時間資源讓出來給拖地,拖完地後水也燒好了,這個就是多線程的優勢,再同一個時間段做更多的事情,也就是再以後會降到的高併發。

它提供如下一些方法:

t1 = threading.Thread(target=你寫的函數名,args=(傳入變量(如果只有一個變量就必須在後加上逗號),),name=隨便取一個線程名):把一個線程實例化給t1,這個線程負責執行target=你寫的函數名
t1.start():負責執行啓動這個線程
t1.join():必須要等待你的子線程執行完成後再執行主線程
t1.setDeamon(True):當你的主線程執行完畢後,不管子線程有沒有執行完成都退出主程序,注意不能和t1.join()一起使用。
threading.current_thread().name:打印出線程名

這些方法一開始看可能會覺得有些多,不過不打緊,可以先把後面的代碼看完在回過頭看這些提供的方法就覺得很簡單了。

單線程版本

import time

def mop_floor():
    print('我要拖地了')
    time.sleep(1)
    print('地拖完了')

def heat_up_watrt():
    print('我要燒水了')
    time.sleep(6)
    print('水燒開了')

start_time = time.time()
heat_up_watrt()
mop_floor()
end_time = time.time()
print('總共耗時:{}'.format(end_time-start_time))

返回結果:

我要燒水了
水燒開了
我要拖地了
地拖完了
總共耗時:7.000766277313232

單線程一共耗時7秒

多線程版本

import threading
import time

def mop_floor():
    print('我要拖地了')
    time.sleep(1)
    print('地拖完了')

def heat_up_watrt():
    print('我要燒水了')
    time.sleep(6)
    print('水燒開了')

start_time = time.time()
t1 = threading.Thread(target=heat_up_watrt)
t2 = threading.Thread(target=mop_floor)
t1.start()
t2.start()
t1.join()
t2.join()
end_time = time.time()
print('總共耗時:{}'.format(end_time-start_time))

返回結果:

我要燒水了
我要拖地了
地拖完了
水燒開了
總共耗時:6.000690460205078

可以看到燒水等待的時候直接執行拖地任務,並且總共耗時爲6秒,關於這裏的start和jion都是固定的操作套路,記住這兩個代詞以後直接套用即可,需要注意的是多線程程序的執行順序是不確定的。當執行到sleep語句時,線程將被阻塞(Blocked),到sleep結束後,線程進入就緒(Runnable)狀態,等待調度的命令執行另一個子線程,線程調度將自行選擇一個線程執行。

類方法實現多線程

當遇到比較複雜的業務邏輯或者想要再子線程上面加一些方法的話,可以使用自己的類重寫代碼,還是使用剛剛的例子:

import threading
import time

class mop_floor(threading.Thread):
    def __init__(self):
        super().__init__()

    def run(self):
        print('我要拖地了')
        time.sleep(1)
        print('地拖完了')

class heat_up_watrt(threading.Thread):
    def __init__(self,name):
        # 這裏傳入參數name,就是傳入子線程名字
        super().__init__(name=name)
        # 記住這裏的格式不能錯

    def run(self):
        print('我要燒水了')
        print(self.name)
        print(threading.current_thread().name)
        # 這兩個都是打印出當前子線程的名字
        time.sleep(6)
        print('水燒開了')

start_time = time.time()
t1 = mop_floor()
t2 = heat_up_watrt('***我是燒水員***')
t1.start()
t2.start()
t1.join()
t2.join()
end_time = time.time()
print('總共耗時:{}'.format(end_time-start_time))

返回結果:

我要拖地了
我要燒水了
***我是燒水員***
***我是燒水員***
地拖完了
水燒開了
總共耗時:6.000684022903442

python的threading.Thread類有一個run方法,用於定義線程的功能函數,可以在自己的線程類中覆蓋該方法。而創建自己的線程實例後,通過Thread類的start方法,可以啓動該線程,交給python虛擬機進行調度,當該線程獲得執行的機會時,就會調用run方法執行線程。

當然你也可以在類方法中加上使用線程鎖

class test(threading.Thread):
	def __init__(self,lock):
		super().__init()__()
		self.lock = lock

	def run(self):
		self.lock.acquire()
		pass
		self.lock.release()

	if __name__ == '__main__':
		lock = threading.Lock()
		t = test(lock)

run方法與start方法的區別

其實不僅僅是start,使用run也可以讓子線程執行:

def mop_floor():
    print('我要拖地了')
    time.sleep(1)
    print('地拖完了')


def heat_up_watrt():
    print('我要燒水了')
    time.sleep(6)
    print('水燒開了')

start_time = time.time()
t1 = threading.Thread(target=heat_up_watrt)
t2 = threading.Thread(target=mop_floor)
t1.run()
t2.run()
# 注意這裏不能加上join()
end_time = time.time()
print('總共耗時:{}'.format(end_time - start_time))

返回結果:

我要燒水了
水燒開了
我要拖地了
地拖完了
總共耗時:7.000263929367065

兩個子線程都用run()方法啓動,但卻是先運行t1.run(),運行完之後才按順序運行t2.run(),兩個線程都工作在主線程,沒有啓動新線程,因此,run()方法僅僅是普通函數調用。

start() 方法是啓動一個子線程,線程名就是我們定義的name
run() 方法並不啓動一個新線程,就是在主線程中調用了一個普通函數而已。

因此,如果你想啓動多線程,就必須使用start()方法。

燒水和拖地只是兩個比喻,運用到實際業務中就好比爬蟲中一個線程去爬標題與網址另一個去爬標題內的內容,也好比一個線程去獲取網頁內容,另一個線程去把網頁內容寫入本地,網絡編程中也是,你在等待tcp鏈接的時候可以去做另外的事情等等。

主線程與子線程

回到剛剛燒水與拖地的例子中:

主線程:一個進程中至少有一個線程,並作爲程序的入口,這個線程就是主線程(從一開始的代碼執行到最後的打印出執行時間爲主線程)

子線程:一個進程至少有一個主線程,其它線程稱爲子線程(拖地與燒水兩個子線程)

上面的例子中可以發現一共有三個線程,一個主線程和兩個子線程,如何定義子線程的?其實在代碼中就發現了,使用t1 = threading.Thread(target=heat_up_watrt)即可生成一個子線程,然後使用t1.start()即可啓動這個子線程,這樣的話t1.jion()是不是就多餘呢?其實不然,使用t1.jion()的作用就是:等待子線程執行完畢後再執行主線程,如果不加上t1.jion()的話,子線程任然執行,但是子線程再等待的時候(io操作的時候),釋放出資源,這個時候主線程拿到資源運行主線程的任務,就會直接打印出共耗時:xxx,然後再等待子線程運行結束,最後退出主程序。

注意這裏的t1.jion()放到位置很重要:

t1.start()
t1.join()
t2.start()
t2.join()

如果這樣放的話,就是先執行線程1然後等待線程1執行完畢,然後執行線程2等待線程2執行完畢。

t1.start()
t2.start()
t1.join()
t2.join()

這樣的話就不一樣,會等到線程1與線程2執行完畢後再執行主線程。

上面說到主線程推出後要等待子線程執行完畢後纔會退出整個主程序,此時使用t1.setDaemon(True)的話,會當主線程執行完畢後,t1子線程不管有沒有執行完畢,都退出主程序。

線程間通信

在一個進程中,不同子線程負責不同的任務,t1子線程負責獲取到數據,t2子線程負責把數據保存的本地,那麼他們之間的通信使用Queue來完成。因爲再一個進程中,數據變量是共享的,即多個子線程可以對同一個全局變量進行操作修改,Queue是加了鎖的安全消息隊列。

在此之前回顧Queue消息隊列的使用Queue消息隊列
注意queue.join()阻塞等待隊列中任務全部處理完畢,需要配合queue.task_done使用

import threading
import time
import queue

q = queue.Queue(maxsize=5)

def t1(q):
    while 1:
        for i in range(10):
            q.put(i)




def t2(q):
    while not q.empty():
        print('隊列中的數據量:'+str(q.qsize()))
        # q.qsize()是獲取隊列中剩餘的數量
        print('取出值:'+str(q.get()))
        # q.get()是一個堵塞的,會等待直到獲取到數據
        print('-----')
        time.sleep(0.1)


t1 = threading.Thread(target=t1,args=(q,))
t2 = threading.Thread(target=t2,args=(q,))
t1.start()
t2.start()

返回結果:

隊列中的數據量:5
取出值:0
-----
隊列中的數據量:5
取出值:1
-----
隊列中的數據量:5
取出值:2
-----
隊列中的數據量:5
取出值:3
-----
隊列中的數據量:5
取出值:4
-----
隊列中的數據量:5
取出值:5

這個即使消息隊列的簡單使用,舉個例子就能清晰的明白線程間使用queue如何通信:

import threading
import time
import queue
'''
模擬包子店賣包子
廚房每一秒鐘製造一個包子
顧客每三秒吃掉一個包子
廚房一次性最多存放100個包子
'''
q = queue.Queue(maxsize=100)
# 廚房一次性最多存放100個包子

def produce(q):
# 這個函數專門產生包子
    for i in range(10000):
        q.put('第{}個包子'.format(str(i)))
        # 生產出包子,表明包子的id號
        time.sleep(1)
        # 要一秒才能造出一個包子


def consume(q):
    while not q.empty():
        # 只要包子店裏有包子
        print('包子店的包子剩餘量:'+str(q.qsize()))
        # q.qsize()是獲取隊列中剩餘的數量
        print('小桃紅吃了:'+str(q.get()))
        # q.get()是一個堵塞的,會等待直到獲取到數據
        print('------------')
        time.sleep(3)


t1 = threading.Thread(target=produce,args=(q,))
t2 = threading.Thread(target=consume,args=(q,))
t1.start()
t2.start()

返回結果:

包子店的包子剩餘量:1
小桃紅吃了:第0個包子
------------
包子店的包子剩餘量:2
小桃紅吃了:第1個包子
------------
包子店的包子剩餘量:4
小桃紅吃了:第2個包子
------------
包子店的包子剩餘量:6
小桃紅吃了:第3個包子
------------
包子店的包子剩餘量:8
小桃紅吃了:第4個包子
------------
包子店的包子剩餘量:10
小桃紅吃了:第5個包子
------------
......

線程同步

如果沒有控制多個線程對同一資源的訪問,對數據造成破壞,使得線程運行的結果不可預期。這種現象稱爲“線程不安全”。

同步就是協同步調,按預定的先後次序進行運行。

如:你說完,我再說。

"同"字從字面上容易理解爲一起動作

其實不是,"同"字應是指協同、協助、互相配合。

如進程、線程同步,可理解爲進程或線程A和B一塊配合,A執行到一定程度時要依靠B的某個結果,於是停下來,示意B運行;B依言執行,再將結果給A;A再繼續操作。

線程鎖實現同步控制

線程鎖使用threading.Lock()實例化,使用acquire()上鎖,使用release()釋放鎖,牢記acquire與release()必須要同時成對存在。它提供一些如下方法:

acquire():上鎖,這個時候只能運行上鎖後的代碼
release():解鎖,解鎖後把資源讓出來,給其他線程使用

舉個例子:

def run1():
    while 1:
        print('我是老大,我先運行')
def run2():
    while 1:
        print('我是老二,我第二運行')
def run3():
    while 1:
        print('我是老三,我最後運行')

t1 = threading.Thread(target=run1)
t2 = threading.Thread(target=run2)
t3 = threading.Thread(target=run3)
t1.start()
t2.start()
t3.start()

這樣運行的結果是無序沒法控制的,但是當你加上一把鎖後,就不一樣了。

def run1():
    while 1:
        if l1.acquire():
            # 如果第一把鎖上鎖了
            print('我是老大,我先運行')
            l2.release()
            # 釋放第二把鎖
def run2():
    while 1:
        if l2.acquire():
            # 如果第二把鎖上鎖了
            print('我是老二,我第二運行')
            l3.release()
            # 釋放第三把鎖

def run3():
    while 1:
        if l3.acquire():
            # 如果第三把鎖上鎖了
            print('我是老三,我最後運行')
            l1.release()
            # 釋放第一把鎖


t1 = threading.Thread(target=run1)
t2 = threading.Thread(target=run2)
t3 = threading.Thread(target=run3)

l1 = threading.Lock()
l2 = threading.Lock()
l3 = threading.Lock()
# 實例化三把鎖

l2.acquire()
l3.acquire()

t1.start()
t2.start()
t3.start()

返回結果:

我是老大,我先運行
我是老二,我第二運行
我是老三,我最後運行
我是老大,我先運行
我是老二,我第二運行
我是老三,我最後運行
我是老大,我先運行
我是老二,我第二運行
我是老三,我最後運行
.....

此時雖然按照自己的要求同步執行,但是運行速度會慢一點,因爲上鎖與釋放鎖需要時間會影響性能。

lock還有Rlock的方法,RLock允許在同一線程中被多次acquire(比如你一個函數上了鎖,這個函數調用另一個函數,另一個函數也上了鎖 )。而Lock卻不允許這種情況。否則會出現死循環,程序不知道解哪一把鎖。注意:如果使用RLock,那麼acquire和release必須成對出現,即調用了n次acquire,必須調用n次的release才能真正釋放所佔用的鎖

條件變量實現同步精準控制

條件變量,用於複雜的線程間同步。在一些對線程間通信要求比較精準的需求下,使用簡單的lock加鎖解鎖已經沒法實現需求,這個時候condition條件控制就派上用場了。

condition源代碼中本質上還是調用lock實現條件變量的控制, 他提供如下一些方法:

acquire():上鎖
release():解鎖
wait(timeout=None):堵塞線程,知道接受到一個notify或者超時才能繼續運行,需記住wait()必須在已經獲得lock的前提下才能調用
notify(n=1):打通線程(個人覺得這樣叫方便理解),堵塞的線程接收到notify後開始運行,需記住notify()必須在已經獲得lock的前提下才能調用
notifyAll():如果調用wait()堵塞的線程比較多,就打通所有的堵塞線程

通過實例代碼來弄清楚他們怎麼調用:

import threading
import random
import queue

q = queue.Queue(maxsize=100)

def produce(q):
    while 1:
        result = str(random.randint(1,100))
        q.put(result)
        print('我生成了一個隨機數字:'+result)
def consume(q):
    while 1:
        res = q.get()
        print('我獲取到了你生成的隨機數字:'+str(res))

t1 = threading.Thread(target=produce,args=(q,))
t2 = threading.Thread(target=consume,args=(q,))
t1.start()
t2.start()

返回結果:

我生成了一個隨機數字:29
我獲取到了你生成的隨機數字:92
我生成了一個隨機數字:38
我獲取到了你生成的隨機數字:9
我生成了一個隨機數字:21
我獲取到了你生成的隨機數字:37
我生成了一個隨機數字:28

由於線程的不安全性,每次生成和獲取的數字都並非同時是按順序索取要得到的,這個時候condition就派上用場了(其實如果設置消息隊列的q=queue.Queue(size=1)就能解決這個問題)。

import threading
import random

def produce():
    global q
    while 1:
        con.acquire()
        # 必須在有鎖的前提下才能使用條件變量
        q = str(random.randint(1,100))
        print('我生成了一個隨機數字:'+q)
        con.notify()
        # 發起一個信號,釋放一個被堵塞的線程
        con.wait()
        # 發起一個信號,堵塞當前線程,等待另一個notify出現的時候就執行下面的代碼
        con.release()
        # 必須要解鎖
def consume():
    global q
    while 1:
        con.acquire()
        # 必須在有鎖的前提下才能使用條件變量
        print('我獲取到你生成的隨機數字:'+q)
        con.notify()
        # 發起一個信號,釋放一個被堵塞的線程
        con.wait()
        # 堵塞當前線程
        con.release()

t1 = threading.Thread(target=produce)
t2 = threading.Thread(target=consume)
con = threading.Condition()
t1.start()
t2.start()

返回結果:

我生成了一個隨機數字:99
我獲取到你生成的隨機數字:99
我生成了一個隨機數字:53
我獲取到你生成的隨機數字:53
我生成了一個隨機數字:26
我獲取到你生成的隨機數字:26

同時condition的源碼類中是有enter和exit兩個魔法函數的,這也就意味能夠使用with上下管理,省去自己加鎖解鎖的過程,舉一個更加方便理解的例子:

import threading
import time
con = threading.Condition()
def run():
    while 1:
        with con:
            # 使用上下文管理器,省去了自動上鎖解鎖的過程
            print('-----------------')
            print('我完事了,該你了')
            con.notify()
            # 發起一個信號,釋放掉一個wait
            con.wait()


def result():
    while 1:
        with con:
            con.wait()
            # 我在等待一個noity出現,這樣我就能運行了
            time.sleep(0.3)
            print('三秒後....')
            print('我也完事了,你繼續')
            con.notify()

t1 = threading.Thread(target=run)
t2 = threading.Thread(target=result)
t2.start()
t1.start()
# 注意這裏必須t2先運行,想想爲什麼

返回結果:

-----------------
我完事了,該你了
三秒後....
我也完事了,你繼續
-----------------
我完事了,該你了
三秒後....
我也完事了,你繼續
......

condition源碼中其實是調用了兩把鎖,外層的鎖是調用lock,內層的鎖是通過wait()方法實現,每次調用wait的時候,都會分配一把鎖放在等待的隊列中,知道noitfy方法出現就釋放這把鎖。

信號量實現定量的線程同步

semaphore適用於控制進入數量的鎖,好比文件的讀寫操作,寫入的時候一般只用一個線程寫,如果多個線程同時執行寫入操作的時候,就會造成寫入數據混亂。 但是讀取的時候可以用多個線程來讀取,可以看到寫與寫是互斥的,讀與寫不是互斥的,讀與讀不是互斥的。

文件讀寫只是個例子,在一些日常業務中比如爬蟲讀取網址的線程數量控制等。

BoundedSemaphore。這種鎖允許一定數量的線程同時更改數據,它不是互斥鎖。比如地鐵安檢,排隊人很多,工作人員只允許一定數量的人進入安檢區,其它的人繼續排隊。

這個使用很簡單的:

import time
import threading

def run(n, se):
    se.acquire()
    print("run the thread: %s" % n)
    time.sleep(1)
    se.release()

# 設置允許5個線程同時運行
semaphore = threading.BoundedSemaphore(5)
for i in range(20):
    t = threading.Thread(target=run, args=(i,semaphore))
    t.start()

運行後,可以看到5個一批的線程被放行。他用來控制進入某段代碼的線程數量。

最後提起一點,Rolok是基於lock實現了,condition是基於Rlock和lock實現的,semahhore是基於condition實現的。

事件實現線程鎖同步

事件線程鎖的運行機制:
全局定義了一個Flag,如果Flag的值爲False,那麼當程序執行wait()方法時就會阻塞,如果Flag值爲True,線程不再阻塞。這種鎖,類似交通紅綠燈(默認是紅燈),它屬於在紅燈的時候一次性阻擋所有線程,在綠燈的時候,一次性放行所有排隊中的線程。

事件主要提供了四個方法set()、wait()、clear()和is_set()。

調用wait()方法將等待信號。
is_set():判斷當前是否狀態
調用set()方法會將Flag設置爲True。
調用clear()方法會將事件的Flag設置爲False。

舉個例子:

import threading
import time
import random

boys = ['此時一位撿瓶子的靚仔路過\n------------','此時一位沒錢的網友路過\n------------','此時一位推着屎球的屎殼郎路過\n------------']
event = threading.Event()
def lighter():
    event.set()
    while 1:
        ti = (random.randint(1, 10))
        time.sleep(ti)
        print('等待 {} 秒後'.format(str(ti)))
        event.clear()
        time.sleep(ti)
        event.set()


def go(boy):
    while 1:
        if event.is_set():
            # 如果事件被設置
            print('在遼闊的街頭')
            print(boy)
            time.sleep(random.randint(1, 5))
        else:
            print('在寂靜的田野')
            print(boy)
            event.wait()
            print('突然,一輛火車駛過')
            time.sleep(5)

t1 = threading.Thread(target=lighter)
t1.start()

for boy in boys:
    t2 = threading.Thread(target=go,args=(boy,))
    t2.start()

線程池編程

作用

線程池只有在py3.2才內置的,2版本只能自己維護一個線程池很麻煩。在Python3.2中的concurrent_futures,其可以實現線程池,進程池,不必再自己使用管道傳數據造成死鎖的問題。並且這個模塊具有線程池和進程池、管理並行編程任務、處理非確定性的執行流程、進程/線程同步等功能,但是平時用的最多的還是用來構建線程池和進程池。

在線程池中,主線程可以獲取任意一個線程的狀態以及返回結果,並且當一個線程完成後,主線程能立即得到結果。

功能

此模塊由以下部分組成:

  1. concurrent.futures.Executor: 這是一個虛擬基類,提供了異步執行的方法。
  2. submit(function, argument): 調度函數(可調用的對象)的執行,將 argument 作爲參數傳入。
  3. map(function, argument): 將 argument 作爲參數執行函數,以 異步 的方式。
  4. shutdown(Wait=True): 發出讓執行者釋放所有資源的信號。
  5. concurrent.futures.Future: 其中包括函數的異步執行。Future對象是submit任務(即帶有參數的functions)到executor的實例
  6. done():比如t1.done()判斷任務t1是否完成,沒完成則返回False
  7. cancel():比如t1.cancel(),取消線程執行,當線程正在執行和執行完畢後是沒法取消執行的,但是如果一個線程沒有啓動的話,是可以取消t1線程執行的

套用模板

線程池用起來非常方便,基本上是照着模板往裏面套即可

	# coding:utf-8
	from concurrent.futures import ThreadPoolExecutor
	# 導入線程池模塊
	import threading
	# 導入線程模塊,作用是獲取當前線程的名稱
	import os,time
	
	def task(n):
	    print('%s:%s is running' %(threading.currentThread().getName(),os.getpid()))
	    # 打印當前線程名和運行的id號碼
	    time.sleep(2)
	    return n**2
	    # 返回傳入參數的二次冪
	
	if __name__ == '__main__':
	    p=ThreadPoolExecutor()
	    #實例化線程池,設置線程池的數量,不填則默認爲cpu的個數*5
	    l=[]
	    # 用來保存返回的數據,做計算總計
	    for i in range(10):
	        obj=p.submit(task,i)
			# 這裏的obj其實是futures的對象,使用obj.result()獲取他的結果
	        # 傳入的參數是要執行的函數和該函數接受的參數
			# submit是非堵塞的
			# 這裏執行的方式是異步執行
			# -----------------------------------
			# # p.submit(task,i).result()即同步執行
			# -----------------------------------
			# 上面的方法使用range循環有個高級的寫法,即map內置函數
			# p = ThreadPoolExecutor()
			# obj=p.map(task,range(10))
			# p.shutdown()
			# 這裏的obj的值就是直接返回的所有計算結果,不屬於futures對象
			# -----------------------------------
	        l.append(obj)
	        # 把返回的結果保存在空的列表中,做總計算
	    p.shutdown()
	    # 所有計劃運行完畢,關閉結束線程池
	    
	    print('='*30)
	    print([obj.result() for obj in l])
	
	#上面方法也可寫成下面的方法
	    # with ThreadPoolExecutor() as p:   #類似打開文件,可省去.shutdown()
	    #     future_tasks = [p.submit(task, i) for i in range(10)]
	    # print('=' * 30)
	    # print([obj.result() for obj in future_tasks])

submit與map的區別

  1. submit返回的是一個futures的對象,使用.result()才能獲取他的運行結果
  2. submit接受的對象是函數加一個固定的參數
  3. map返回的是所有線程執行完畢後返回的結果
  4. map接受的對象是函數加一個傳入函數的集合列表
  5. 他們都能提前獲取先執行完的結果
  6. map比submit簡單好用
  7. map返回的結果是安裝列表傳入參數的順序返回結果,submit返回結果是哪個線程先執行完就返回哪個線程的結果

獲取部分執行完成的結果

想要獲取到部分執行完成的結果使用

from concurrent.futures import as_completed

as_completed其實是一個生成器,他只會返回以及完成的線程結果

比如:

all_tasks = [p.submit(task,obj) for obj in l]
for f in as_completed(all_tasks):
	data = f.result()

因爲生成器的緣故,他會先把執行完畢的結果賦值給data,其實通過map方法也可以實現

for data in p.map(task,range(10)):
	print data

這樣也是可以優先把執行完畢的結果獲取出來,map函數中其實也有yield函數。map和as_completed的區別在於,map執行是按照傳入列表的元素一個一個按順序返回的,並且返回的是直接的結果,as_completed返回的是一個futures的對象,使用.result()獲取他的結果。as_completed返回的不是按照順序返回的。

堵塞線程

多線程中使用jion堵塞線程,在線程池中也可以堵塞等待某一些子線程的線程池執行完畢後再執行主線程

from concurrent.futures import wait

wait用在你的線程池下面,比如:

all_tasks = [p.submit(task,obj) for obj in l]
wait(all_tasks)

這裏只有等待all_tasks裏面的線程執行完畢後才能繼續執行,裏面的可以加上參數等待線程池中只要有一個線程執行結束就執行後面的代碼

wait(all_tasks,return_when=FIRST_COMPLETED)

判斷是否執行完畢

task1 = p.submit(task,10)
print(task1.done())
# 如果執行完畢,返回True

取消執行線程

task1 = p.submit(task,10)
task1.cancel()

當線程正在執行和執行完畢後是沒法取消執行的,但是如果一個線程沒有啓動的話,是可以取消t1線程執行的。

線程池提供的方法非常簡單,易於使用,但是他背後的原理是很值得研究的。

線程池設計理念

線程池優秀的設計理念在於:他返回的結果並不是執行完畢後的結果,而是futures的對象,這個對象會在未來存儲線程執行完畢的結果,這一點也是異步編程的核心。python爲了提高與統一可維護性,多線程多進程和協程的異步編程都是採取同樣的方式。

多進程

因爲GIL的存在,沒法利用到多核CPU的優勢,這個時候多進程就出來了,它能利用多個CPU併發的優勢實現並行。

多進程:消耗CPU操作,CPU密集計算

多線程:大量的IO操作

進程間切換的開銷要大於線程間開銷,對操作系統來說開多線程比開多進程來說消耗的資源少一些,不同的進程是由主進程完全複製後,每個進程獨立隔離開,不像多線程對全局變量直接修改,因爲是完全複製獨立的,所以當主進程結束後,子進程任然執行。

另外多進程必須要注意的一點就是在運行的時候,必須要加上

if __name__ == '__main__':
	pass

進程池套用模板

與線程池用法幾乎一致,進程池是基於multiprocessing實現的

from concurrent.futures import ProcessPoolExecutor
import os,time,random
def task(n):
    print('%s is running' %os.getpid())
    time.sleep(2)
    return n**2


if __name__ == '__main__':
    p=ProcessPoolExecutor()  #不填則默認爲cpu的個數
    l=[]
    start=time.time()
    for i in range(10):
        obj=p.submit(task,i)   #submit()方法返回的是一個future實例,要得到結果需要用obj.result()
        l.append(obj)

    p.shutdown()  #類似用from multiprocessing import Pool實現進程池中的close及join一起的作用
    print('='*30)
    # print([obj for obj in l])
    print([obj.result() for obj in l])
    print(time.time()-start)

    #上面方法也可寫成下面的方法
    # start = time.time()
    # with ProcessPoolExecutor() as p:   #類似打開文件,可省去.shutdown()
    #     future_tasks = [p.submit(task, i) for i in range(10)]
    # print('=' * 30)
    # print([obj.result() for obj in future_tasks])
    # print(time.time() - start)

進程池相關的使用功能與線程池基於一致。

multiprocessing具體使用與數據通信

這些功能在我之前的筆記寫的比較完整與友好,這裏不重複造輪子寫概念了,
具體傳送Python multiprocess 多進程模塊

參考鏈接

具體鏈接之線程池進程池

參考鏈接 1

參考鏈接 2

參考鏈接 3

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