Python核心編程筆記————多線程編程(一)

簡介

. 多線程編程的目的是提高整個任務的性能。其對具有如下特點的編程任務是非常理想的:
  需要多個併發活動;
  每個活動的處理順序是不確定的(隨機的)。
  使用多線程來規劃合適的編程任務可以降低程序的複雜性,使其更加清晰、高效、簡介。

線程和進程

進程

. 進程是一個執行中的程序,每個進程都擁有自己的地址空間、內存、數據棧以及其他用於跟蹤執行的輔助數據。操作系統管理其上的所有進程,爲這些進程分配時間。進程也可以通過“派生”(fork或spawn)新的進程來執行其他任務。新的進程也就擁有了自己的內存和數據棧等,進程之間的通信只能使用**進程間通信(IPC)**的方式共享信息。

線程

. 線程與進程類似,不過它們是在同一個進程下執行的,並共享相同的上下文(ps:上下文到底是啥意思啊?我自己的理解是線程所屬的進程當時的狀態,比如進程擁有某個資源,那此刻大家都有使用權,進程把這個資源還了,那此刻大家就都沒有使用權)。
  線程包括開始、執行順序和結束三部分。它有一個指令指針,用於記錄當前的上下文。當其他線程運行時,它可以被搶佔(中斷)和臨時掛起(睡眠)——這種做法叫做讓步
  一個進程中的各線程與主線程共享同一片數據空間,因此相比較於獨立的進程而言,線程間的通信更加容易。線程一般是以併發的方式執行的,由於這種併發和數據共享機制,使得多任務的協作成爲可能。其實在單核CPU系統中不可能做到真正的併發,所以線程的執行實際上是這樣規劃的:每個線程運行一小會兒,然後讓步給其他的線程並排隊等候。在整個進程的執行過程中,每個線程執行自己特定的任務,在必要時與其他線程進行通信。
  當然,這種共享不是沒有風險的。如果多個線程訪問同一片數據,由於數據的訪問順序不一樣,可能導致結果不一致。這種情況通常稱爲競態條件。幸運的是,大多數線程庫都有一些同步原語,以允許線程管理器控制執行和訪問。
  另一個需要注意的問題是,線程無法給予公平的執行時間。這是因爲一些函數會在完成前保持阻塞狀態,如果沒有專門爲多線程情況進行修改,會導致 CPU 的時間分配向這些貪婪的函數傾斜。

線程和Python

全局解釋器鎖

. Python 代碼的執行是由 Python 虛擬機進行控制的。Python 在設計時是這樣考慮的,在主循環中同時只能有一個控制線程在執行,就像單核 CPU 系統中的多進程一樣。內存中可以有許多程序,但是在任意給定時刻只能有一個程序在運行。同理,儘管 Python 解釋器中可以運行多個線程,但是在任意給定時刻只有一個線程會被解釋器執行。
  對 Python 虛擬機的訪問是由全局解釋器鎖(GIL)控制的。這個鎖保證了同時只有一個線程在運行。在多線程的環境中,Python虛擬機執行方式如下所示:
  1.設置GIL;
  2.切換到一個線程去運行;
  3.執行下面的操作之一:
    a.指定數量的字節碼指令;
    b.線程主動讓出控制權(可以調用 time.sleep(0)來完成);
  4.把線程設置回睡眠狀態(切換出線程);
  5.解鎖 GIL;
  6.重複上述步驟。
  對於Python代碼來說,任意麪向I/O的Python例程(調用了內置的操作系統 C 代碼的那種),GIL會在I/O調用前被釋放,以允許其他線程在 I/O 執行的時候運行。因此I/O 密集型的 Python 程序要比計算密集型的代碼能夠更好地利用多線程環境。

退出線程

. 當一個線程完成函數的執行時,就會退出,也可以通過調用thread.exit()之類的退出函數,或者sys.exit()之類的退出Python進程的標準方法,亦或是拋出SystemExit異常來使線程退出。

在Python中使用線程

. Python支持多線程編程,但是程序是否正常運行還是要取決於運行的操作系統,另外,編寫代碼時還需要知道所使用的解釋器是否支持線程,可以嘗試從交互式解釋器中導入thread模塊,沒有報錯說明可用,如果報錯,可能需要重新編譯Python解釋器才能夠使用線程(ps:自帶的IDLE居然報錯了)。一般可以在調用configure 腳本的時候使用–with-thread 選項。查閱你所使用的發行版本的 README 文件,來獲取如何在你的系統中編譯線程支持的 Python 的指定指令。

Python的threading模塊

. Python 提供了多個模塊來支持多線程編程,包括 thread、threading 和 Queue 模塊等。程序可以使用thread和threading模塊來創建和管理線程。thread模塊提供了基本的線程和鎖定支持;而threading模塊提供更高級別、功能更全面的線程管理。使用Queue模塊,可以創建一個隊列數據結構,用於在多線程之間進行共享。
  值得注意的是書中給了一個建議:儘量避免使用thread模塊(在Python中被重命名爲_thread),因爲threading模塊中有更先進、更好的線程支持,並且thread模塊中的一些屬性和threading模塊中的會有衝突;另一個原因是thread模塊擁有的同步原語(事件)很少,而threading中有很多;還有一個原因是thread對進程何時退出沒有控制。當主線程結束時,所有其他線程也都強制結束,不會發出警告或是做適當的處理,至少threading模塊能確保重要的子線程在進程退出前結束

thread模塊

. 除了派生線程外,thread模塊還提供了基本的同步數據結構,成爲鎖對象(lock object,也叫原語鎖、簡單鎖、互斥鎖、互斥和二進制信號量),下表列出了一些常用的線程函數和鎖對象的方法:

函數/方法 描述
thread模塊的函數
start_new_thread(function,args,kwargs = None) 派生一個新線程,使用給定的args和可選的kwargs來執行function
allocate_lock() 分配LockType對象
exit() 給線程退出指令
LockType鎖對象的方法
acquire(wait = None) 嘗試獲取鎖對象
locked() 如果獲取了鎖對象則返回True,否則返回Flase
release() 釋放鎖

. 以下是使用thread模塊的一個簡單多線程示例:

import _thread
from time import sleep,ctime

def loop0():
    print('start loop0 at:',ctime())
    sleep(4)
    print('end loop0 at:',ctime())

def loop1():
    print('start loop1 at:', ctime())
    sleep(2)
    print('end loop1 at:', ctime())

def main():
    print('start at:',ctime())
    _thread.start_new_thread(loop0,())		#loop0即使沒有參數,也要傳遞空元祖
    _thread.start_new_thread(loop1,())
    sleep(6)
    print('all done at:',ctime())

if __name__ == '__main__':
    main()

//運行結果如下:
start at: Thu Jan 24 11:50:55 2019
start loop0 at: Thu Jan 24 11:50:55 2019
start loop1 at: Thu Jan 24 11:50:55 2019
end loop1 at: Thu Jan 24 11:50:57 2019
end loop0 at: Thu Jan 24 11:50:59 2019
all done at: Thu Jan 24 11:51:01 2019

. 併發情況下兩個函數執行總共花的時間只是4秒而不是6秒,主線程的sleep是爲了防止兩個線程在主線程退出的時候被強行終止,但是這種方式不適合用在代碼中,因爲無法確定主線程的時間是否比其他線程慢,這太不可靠了,因此的作用就顯現出來了。下面是使用鎖來完成的代碼:

import _thread
from time import sleep,ctime

loops = [4,2]

def loop(nloop,nsec,lock):
    print('start loop',nloop,'at:',ctime())
    sleep(nsec)
    print('end loop', nloop, 'at:', ctime())
    lock.release()

def main():
    print('start at:',ctime())
    locks = []
    nloops = range(len(loops))
    for i in nloops:
        lock = _thread.allocate_lock()
        lock.acquire()
        locks.append(lock)
    for i in nloops:
        _thread.start_new_thread(loop,(i,loops[i],locks[i]))
    for i in nloops:
        while locks[i].locked():
            pass
    print('all done at:',ctime())

if __name__ == '__main__':
    main()

//執行結果如下
start at: Thu Jan 24 14:06:53 2019
start loop 1 at: Thu Jan 24 14:06:53 2019
start loop 0 at: Thu Jan 24 14:06:53 2019
end loop 1 at: Thu Jan 24 14:06:55 2019
end loop 0 at: Thu Jan 24 14:06:57 2019
all done at: Thu Jan 24 14:06:57 2019

. 在main函數中有一個檢查每個鎖對象狀態的循環,一旦所有鎖對象都釋放,主線程就能儘快的執行後面的語句。

threading模塊

. 現在介紹更高級別的threading模塊,下表是模塊中可用對象的列表:

對象 描述
Thread 表示一個執行線程的對象
Lock 鎖原語對象(和thread中的一樣)
RLock 可重入 鎖對象,使單一線程可以(再次)獲得已持有的鎖(遞歸鎖)
Condition 條件變量對象,使得一個線程等待另一個線程滿足特定的條件,比如改變狀態或某個數據值
Event 條件變量的通用版本,任意數量的線程等待某個事件的發生,在該事件發生後所有線程將被激活
Semaphore 爲線程間共享的有限資源提供了一個“計數器”,如果沒有可用資源時會被阻塞
BoundedSemaphore 與 Semaphore 相似,不過它不允許超過初始值
Timer 與 Thread 相似,不過它要在運行前等待一段時間
Barrier 創建一個“障礙”,必須達到指定數量的線程後纔可以繼續

. threading模塊支持守護線程,這也是避免使用tread模塊的原因,守護線程的工作方式是:其一般是一個等待客戶端請求服務的服務器,如果沒有客戶端請求,守護線程就是空閒的。如果將一個線程設置爲守護線程,就表示這個線程是不重要的,進程退出的時候不需要等待這個線程執行完成。
  要將一個線程設置爲守護線程,需要在啓動線程之前執行如下的賦值語句:thread.daemon = True。

Thread類

. Thread類是模塊中主要的執行對象,下表給出了它的屬性和方法:

屬性 描述
Thread對象數據屬性
name 線程名
ident 線程的標識符
daemon 表示這個線程是否是守護線程
Thread對象方法
_init_(group=None, tatget=None, name=None, args=(),kwargs ={}, verbose=None, daemon=None) 實例化一個線程對象,需要有一個可調用的 target,以及其參數 args或 kwargs。還可以傳遞 name 或 group 參數,不過後者還未實現。此外,verbose 標誌也是可接受的。而 daemon 的值將會設定thread.daemon 屬性/標誌
start() 開始執行該線程
run() 定義線程功能的方法(通常在子類中被應用開發者重寫)
join (timeout=None) 直至啓動的線程終止之前一直掛起;除非給出了 timeout(秒),否則會一直阻塞
getName() 已棄用 返回線程名
setName (name) 已棄用 設定線程名
isAlivel /is_alive () 布爾標誌,表示這個線程是否還存活
isDaemon() 如果是守護線程,則返回 True;否則,返回 False
setDaemon(daemonic) 已棄用 把線程的守護標誌設定爲布爾值 daemonic(必須在線程 start()之前調用)

. 使用Thread類創建線程的方式有很多,以下介紹三種較爲相似的:

創建Thread實例,傳給它一個函數

. 將Thread實例化,然後將函數及其參數傳遞進去,當線程開始執行時,函數也開始執行:

import threading
from time import sleep,ctime

loops = [4,2]

def loop(nloop,nsec):
    print('start loop',nloop,'at:',ctime())
    sleep(nsec)
    print('end loop', nloop, 'at:', ctime())

def main():
    print('start at:',ctime())
    threads = []
    nloops = range(len(loops))
    for i in nloops:
        t = threading.Thread(target=loop,args=(i,loops[i]))
        threads.append(t)

    for i in nloops:
        threads[i].start()
    for i in nloops:
       threads[i].join()
    print('all done at:',ctime())

if __name__ == '__main__':
    main()

. 和之前的版本不同的地方在於:線程並不是一被創建就開始執行,而是調用start()方法之後纔會開始執行;另外一點就是相比管理一組鎖,這裏只使用了join方法,join方法將等待線程結束或是等待至超時時間。
  另一個使用join的重點是其實它根本不用被調用,一旦線程啓動,它們就會一直執行,知道給定的函數完成後退出(關於這點,可以比較一下使用_thread模塊的那個版本不用無限循環等待鎖釋放,和這個版本不寫join部分的代碼,執行結果的不同)。

創建 Thread 的實例,傳給它一個可調用的類實例

. 不是傳入函數而是傳入一個可調用的類的實例,用於線程執行——這種方法更加接近面向對象的多線程編程。這種調用有更好的靈活性:

import threading
from time import sleep,ctime

loops = [4,2]

class ThreadFunc(object):

    def __init__(self,func,args,name=''):
        self.func = func
        self.args = args
        self.name = name

    def __call__(self):
        self.func(*self.args)

def loop(nloop,nsec):
    print('start loop',nloop,'at:',ctime())
    sleep(nsec)
    print('end loop', nloop, 'at:', ctime())

def main():
    print('start at:',ctime())
    threads = []
    nloops = range(len(loops))
    for i in nloops:
        t = threading.Thread(target=ThreadFunc(loop,(i,loops[i]),loop.__name__))
        threads.append(t)

    for i in nloops:
        threads[i].start()
    for i in nloops:
       threads[i].join()
    print('all done at:',ctime())

if __name__ == '__main__':
    main()

. 這次主要是添加了TreadFunc類,值得注意的是,這個類使得代碼變得更加通用靈活,比如讓這個類保存函數的參數、函數自身和函數名。構造函數__init__()用於設定上面這些值。
  當創建新的線程的時候,Thread類的代碼將調用ThreadFunc對象,此時會調用__call__()這個特殊方法。

派生 Thread 的子類,並創建子類的實例

. 當創建線程時使用子類相對容易閱讀,並且使我們在定製線程對象時擁有更多的靈活性,也能夠簡化線程創建的調用過程:

import threading
from time import sleep,ctime

loops = [4,2]

class MyThread(threading.Thread):
    def __init__(self,func,args,name=''):
        threading.Thread.__init__(self)
        self.func = func
        self.args = args
        self.name = name
    def run(self):
        self.func(*self.args)

def loop(nloop,nsec):
    print('start loop',nloop,'at:',ctime())
    sleep(nsec)
    print('end loop', nloop, 'at:', ctime())

def main():
    print('start at:',ctime())
    threads = []
    nloops = range(len(loops))
    for i in nloops:
        t = MyThread(loop,(i,loops[i]),loop.__name__)
        threads.append(t)

    for i in nloops:
        threads[i].start()
    for i in nloops:
       threads[i].join()
    print('all done at:',ctime())

if __name__ == '__main__':
    main()

. 與上個版本最主要的變化是:1.MyThread子類的構造函數必須先調用基類的構造函數;2.之前的特殊方法__call__()在這裏必須爲run(),這個方法是重寫的,標準run()方法調用了傳遞給對象的構造函數的可調對象作爲目標參數,如果有這樣的參數的話,順序和關鍵字參數分別從args和kargs取得。

Threading模塊的其它函數

函數 描述
active_count() 當前活動的 Thread 對象個數
current_thread 返回當前的 Thread 對象
enumerate() 返回當前活動的 Thread 對象列表
settrace (func) 爲所有線程設置一個 trace 函數
setprofile (func) 爲所有線程設置一個 profile 函數
stack_size (size=0) 返回新創建線程的棧大小;或爲後續創建的線程設定棧的大小爲 size
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章