天天看點

Python學習—python中的線程

線程是作業系統能夠進行運算排程的最小機關。它被包含在程序之中,是程序中的實際運作機關。一條線程指的是程序中一個單一順序的控制流,一個程序中可以并發多個線程,每條線程并行執行不同的任務。一個程序至少有一個線程,一個程序必定有一個主線程。

建立線程的兩個子產品:

(1)thread(在python3中改名為_thread)

(2)threding

_thread提供了低級别的、原始的線程以及一個簡單的鎖。threading基于java的線程模型設計。thread和threading子產品都可以用來建立和管理線程,而thread子產品提供了基本的線程和鎖支援。threading提供的是更進階的完全的線程管理。低級别的thread子產品是推薦給高手用,一般應用程式推薦使用更進階的threading子產品:

1.它更先進,有完善的線程管理支援,此外,在thread子產品的一些屬性會和threading子產品的這些屬性沖突。

2.thread子產品有很少的(實際上是一個)同步原語,而threading卻有很多。

3.thread子產品沒有很好的控制,特别當你的程序退出時,比如:當主線程執行完退出時,其他的線程都會無警告,無儲存的死亡,而threading會允許預設,重要的子線程完成後再退出,它可以特别指定daemon類型的線程。

_thread子產品建立程序

每次運作程式可以看到不同的結果:

這些結果不同,是因為線程并發執行,三個線程來回切換在cpu工作,且當主線程結束後,不管其它線程是否完成工作都被迫結束。

通過threading子產品建立線程

每次運作程式的結果:

可以看到,不同的多個線程是互相交叉着在cpu執行的,和_thread不同的是它建立了一個線程類對象,也不會因為主線程的結束而結束所有的線程。

使用join方法

在a線程中調用了b線程的join法時,表示隻有當b線程執行完畢時,a線程才能繼續執行。多個線程使用了join方法,剩下的其它線程隻有在這些線程執行完後才能繼續執行。

這裡調用的join方法是沒有傳參的,join方法其實也可以傳遞一個參數給它的。

join方法中如果傳入參數,則表示這樣的意思:如果a線程中掉用b線程的join(10),則表示a線程會等待b線程執行10毫秒,10毫秒過後,a、b線程并行執行。

需要注意的是,jdk規定,join(0)的意思不是a線程等待b線程0秒,而是a線程等待b線程無限時間,直到b線程執行完畢,即join(0)等價于join()。

當通過繼承thread類來建立線程時,需要傳入參數,可以在構造方法增加相應的屬性,以此來傳入所需要的參數。

thread類有一個run方法,當建立一個線程後,使用start方法時,實際上就是在調用類裡面的run方法,是以可以在繼承thread類的時候,重寫run方法來完成自己的任務。

可以看到,通過繼承線程類,然後重寫run方法,執行個體化這個類,這樣也可以新建立線程,在某些情況下,這樣還更加友善。

線程的daemon屬性:當主線程執行結束, 讓沒有執行完成的線程強制結束的一個屬性:daemon

setdaemon方法是改變線程類的一個屬性:daemon,也可以在建立線程的時候指定這個屬性的值,他的值預設為none

運作結果:

當設定daemon屬性為true,就和_thread子產品的線程一樣主線程結束,其它線程也被迫結束

什麼是全局解釋器鎖(gil)

python代碼的執行由python 虛拟機(也叫解釋器主循環,cpython版本)來控制,python 在設計之初就考慮到要在解釋器的主循環中,同時隻有一個線程在執行,即在任意時刻,隻有一個線程在解釋器中運作。對python 虛拟機的通路由全局解釋器鎖(gil)來控制,正是這個鎖能保證同一時刻隻有一個線程在運作。

即全局解釋器鎖,使得在同一時間内,python解釋器隻能運作一個線程的代碼,這大大影響了python多線程的性能。

需要明确的一點是gil并不是python的特性

gil是在實作python解析器(cpython)時所引入的一個概念。就好比c++是一套語言(文法)标準,但是可以用不同的編譯器來編譯成可執行代碼。有名的編譯器例如gcc,intel c++,visual c++等。python也一樣,同樣一段代碼可以通過cpython,pypy,psyco等不同的python執行環境來執行。像其中的jpython就沒有gil。然而因為cpython是大部分環境下預設的python執行環境。是以在很多人的概念裡cpython就是python,也就想當然的把gil歸結為python語言的缺陷。

python gil 會影響多線程等性能的原因:

因為在多線程的情況下,隻有當線程獲得了一個全局鎖的時候,那麼該線程的代碼才能運作,而全局鎖隻有一個,是以使用python多線程,在同一時刻也隻有一個線程在運作,是以在即使在多核的情況下也隻能發揮出單核的性能。

經過gil這一道關卡處理,會增加執行的開銷。這意味着,如果你想提高代碼的運作速度,使用threading包并不是一個很好的方法。

在多線程環境中,python 虛拟機按以下方式執行:

設定gil

切換到一個線程去運作

運作:

a. 指定數量的位元組碼指令,或者

b. 線程主動讓出控制(可以調用time.sleep(0))

把線程設定為睡眠狀态

解鎖gil

再次重複以上所有步驟

既然python在同一時刻下隻能運作一個線程的代碼,那線程之間是如何排程的呢?

對于有io操作的線程,當一個線程在做io操作的時候,因為io操作不需要cpu,是以,這個時候,python會釋放python全局鎖,這樣其他需要運作的線程就會使用該鎖。

對于cpu密集型的線程,比如一個線程可能一直需要使用cpu做計算,那麼python中會有一個執行指令的計數器,當一個線程執行了一定數量的指令時,該線程就會停止執行并讓出目前的鎖,這樣其他的線程就可以執行代碼了。

由上面可知,至少有兩種情況python會做線程切換,一是一但有io操作時,會有線程切換,二是當一個線程連續執行了一定數量的指令時,會出現線程切換。當然此處的線程切換不一定就一定會切換到其他線程執行,因為如果目前線程優先級比較高的話,可能在讓出鎖以後,又繼續獲得鎖,并優先執行。

這裡就可以将操作分兩種:

i/o密集型

cpu密集型(計算密集型)

對于前者我們盡可能的采用多線程方式,後者盡可能采用多程序方式

為什麼會需要線程鎖?

多個線程對同一個資料進行修改時, 會出現不可預料的情況。

例如:

因為沒有對變量money做通路限制,在某一個線程對其進行操作時,另一個線程仍可以對它進行通路、操作,緻使最終結果出錯,且不可預料,不是期待值。

當我們使用線程鎖的時候:

運作結果正确,始終為0

使用多線程來查ip的地理位置

結果:

1). 理論上多線程執行任務, 會産生一些資料, 為其他程式執行作鋪墊;

2). 多線程是不能傳回任務執行結果的, 是以需要一個容器來存儲多線程産生的資料

3). 這個容器如何選擇? list(棧, 隊列), tuple(x), set(x), dict(x), 此處選擇隊列來實作

隊列與多線程

在軟體開發的過程中,經常碰到這樣的場景:

某些子產品負責生産資料,這些資料由其他子產品來負責處理(此處的子產品可能是:函數、線程、程序等)。産生資料的子產品稱為生産者,而處理資料的子產品稱為消費者。在生産者與消費者之間的緩沖區稱之為倉庫。生産者負責往倉庫運輸商品,而消費者負責從倉庫裡取出商品,這就構成了生産者消費者模式。

為了容易了解,我們舉一個寄信的例子。假設你要寄一封信,大緻過程如下:

1、你把信寫好——相當于生産者生産資料

2、你把信放入郵箱——相當于生産者把資料放入緩沖區

3、郵差把信從郵箱取出,做相應處理——相當于消費者把資料取出緩沖區,處理資料

生産者消費者模式的優點

1.解耦

假設生産者和消費者分别是兩個線程。如果讓生産者直接調用消費者的某個方法,那麼生産者對于消費者就會産生依賴(也就是耦合)。如果未來消費者的代碼發生變化,可能會影響到生産者的代碼。而如果兩者都依賴于某個緩沖區,兩者之間不直接依賴,耦合也就相應降低了。

舉個例子:我們去郵局投遞信件,如果不使用郵箱(也就是緩沖區),你必須得把信直接交給郵差。有同學會說,直接給郵差不是挺簡單的嘛?其實不簡單,你必須 得認識誰是郵差,才能把信給他。這就産生了你和郵差之間的依賴(相當于生産者和消費者的強耦合)。萬一哪天郵差 換人了,你還要重新認識一下(相當于消費者變化導緻修改生産者代碼)。而郵箱相對來說比較固定,你依賴它的成本就比較低(相當于和緩沖區之間的弱耦合)。

2.并發

由于生産者與消費者是兩個獨立的并發體,他們之間是用緩沖區通信的,生産者隻需要往緩沖區裡丢資料,就可以繼續生産下一個資料,而消費者隻需要從緩沖區拿資料即可,這樣就不會因為彼此的處理速度而發生阻塞。

繼續上面的例子:如果我們不使用郵箱,就得在郵局等郵差,直到他回來,把信件交給他,這期間我們啥事兒都不能幹(也就是生産者阻塞)。或者郵差得挨家挨戶問,誰要寄信(相當于消費者輪詢)。

3.支援忙閑不均

當生産者制造資料快的時候,消費者來不及處理,未處理的資料可以暫時存在緩沖區中,慢慢處理掉。而不至于因為消費者的性能造成資料丢失或影響生産者生産。

我們再拿寄信的例子:假設郵差一次隻能帶走1000封信,萬一碰上情人節(或是聖誕節)送賀卡,需要寄出去的信超過了1000封,這時候郵箱這個緩沖區就派上用場了。郵差把來不及帶走的信暫存在郵箱中,等下次過來時再拿走。

執行個體:

1.檔案ipfile.txt中有大量的ip位址,要求将ip位址取出來再與端口号組合,放入隊列中

2.從隊列中取出位址,依次通路并傳回通路結果

運作結果就不截圖了。

傳統多線程方案會使用“即時建立, 即時銷毀”的政策。盡管與建立程序相比,建立線程的時間已經大大的縮短,但是如果送出給線程的任務是執行時間較短,而且執行次數極其頻繁,那麼伺服器将處于不停的建立線程,銷毀線程的狀态。

一個線程的運作時間可以分為3部分:線程的啟動時間、線程體的運作時間和線程的銷毀時間。在多線程處理的情景中,如果線程不能被重用,就意味着每次建立都需要經過啟動、銷毀和運作3個過程。這必然會增加系統相應的時間,降低了效率。

使用線程池:

由于線程預先被建立并放入線程池中,同時處理完目前任務之後并不銷毀而是被安排處理下一個任務,是以能夠避免多次建立線程,進而節省線程建立和銷毀的開銷,能帶來更好的性能和系統穩定性。

concurrent.futures.threadpoolexecutor,在送出任務的時候,有兩種方式,一種是submit()函數,另一種是map()函數,兩者的主要差別在于:

(1)map可以保證輸出的順序, submit輸出的順序是亂的

(2)如果你要送出的任務的函數是一樣的,就可以簡化成map。但是假如送出的任務函數是不一樣的,或者執行的過程之可能出現異常(使用map執行過程中發現問題會直接抛出錯誤)就要用到submit()

(3)submit和map的參數是不同的,submit每次都需要送出一個目标函數和對應的參數,map隻需要送出一次目标函數,目标函數的參數放在一個疊代器(清單,字典)裡就可以。