基本概念 / Basic Concept
快速跳轉
- 程序 / Process
- 線程 / Thread
- 協程 / Coroutine
- 全局解釋器鎖 / Global Interpreter Lock
- 守護線程 / Daemon Thread
- 信号量 / Semaphore
- 有界信号量 / BoundedSemaphore
- 同步原語 / Synchronization Primitive
- 鎖 / Lock
- 互斥鎖和可重入鎖 / Mutex Lock and Reentrant Lock
- 死鎖 / Deadlock
- 線程安全 / Thread Safety
0 簡介與動機 / Why Multi-Thread/Multi-Process/Coroutine
在多線程(multithreaded, MT)程式設計出現之前,計算機程式的執行是由單個步驟序列組成的,該序列在主機的CPU中按照同步順序執行。即無論任務多少,是否包含子任務,都要按照順序方式進行。
然而,假定子任務之間互相獨立,沒有因果關系,若能使這些獨立的任務同時運作,則這種并行處理方式可以顯著提高整個任務的性能,這便是多線程程式設計。
而對于Python而言,雖然受限于GIL(全局解釋器鎖)的控制,在處理計算密集型程式時,多線程可能并不能提升性能,但對于I/O密集型的程式來說,Python的多線程模式就能很好的起到性能提升的作用。
1 相關名詞 / Relevant Noun
1.0 程序 / Process
是計算機中的程式關于某資料集合上的一次運作活動,是系統進行資源配置設定和排程的基本機關,是作業系統結構的基礎。在早期面向程序設計的計算機結構中,程序是程式的基本執行實體;在當代面向線程設計的計算機結構中,程序是線程的容器。程式是指令、資料及其組織形式的描述,程序是程式的實體,是一個執行中的程式,也被稱為重量級程序。
1.1 線程 / Thread
線程,有時被稱為輕量級程序(Lightweight Process,LWP),是程式執行流的最小單元。一個标準的線程由線程ID,目前指令指針(PC),寄存器集合和堆棧組成。另外,線程是程序中的一個實體,是被系統獨立排程和分派的基本機關,線程自己不擁有系統資源,隻擁有一點兒在運作中必不可少的資源,但它可與同屬一個程序的其它線程共享程序所擁有的全部資源。一個線程可以建立和撤消另一個線程,同一程序中的多個線程之間可以并發執行。由于線程之間的互相制約,緻使線程在運作中呈現出間斷性。線程也有就緒、阻塞和運作三種基本狀态。就緒狀态是指線程具備運作的所有條件,邏輯上可以運作,在等待處理機;運作狀态是指線程占有處理機正在運作;阻塞狀态是指線程在等待一個事件(如某個信号量),邏輯上不可執行。每一個程式都至少有一個線程,若程式隻有一個線程,那就是程式本身。
1.2 協程 / Coroutine
協程是在一個線程執行過程中可以在一個子程式的預定或者随機位置中斷,然後轉而執行别的子程式,在适當的時候再傳回來接着執行。它本身是一種特殊的子程式或者稱作函數。
一個程式可以包含多個協程,可以對比與一個程序包含多個線程。我們知道多個線程相對獨立,有自己的上下文,切換受系統控制;而協程也相對獨立,有自己的上下文,但是其切換由自己控制,由目前協程切換到其他協程由目前協程來控制。
1.3 全局解釋器鎖 / Global Interpreter Lock
全局解釋器鎖GIL是計算機程式設計語言解釋器用于同步線程的工具,使得解釋器任何時刻僅有一個線程在執行。常見例子有CPython(JPython不使用GIL)與Ruby MRI。
正式由于全局解釋器鎖的存在,使得Python解釋器在任意時刻隻能以單線程的形式運作,即Python中的多線程實際上是對多線程中的每個線程執行一定記憶體數量的程式後,切換到另一個線程繼續執行,直到再次切回繼續執行。是以Python應對CPU-bound Computation時難以展現優勢,而處理類似爬蟲等I/O-intensive Computation時則有較大優勢。
1.4 守護線程 / Daemon Thread
所謂守護線程,是指在程式運作的時候在背景提供一種通用服務的線程,比如垃圾回收線程就是一個很稱職的守護者,并且這種線程并不屬于程式中不可或缺的部分。是以,當所有的非守護線程結束時,程式也就終止了,同時會殺死程序中的所有守護線程。反過來說,隻要任何非守護線程還在運作,程式就不會終止。即守護線程的存在不影響程式退出。
使用者線程和守護線程兩者幾乎沒有差別,唯一的不同之處就在于虛拟機的離開,如果使用者線程已經全部退出運作了,隻剩下守護線程存在了,虛拟機也就退出了。因為沒有了被守護者,守護線程也就沒有工作可做了,也就沒有繼續運作程式的必要了。
1.5 信号量 / Semaphore
Semaphore是最古老的同步原語之一,由荷蘭計算機科學家 Edsger W. Dijkstra 發明。(他最早使用名為P()和V()的函數對應acquire()和release())。threading子產品中,Semaphore在内部管理着一個計數器。調用acquire()會使這個計數器-1,release()則是+1。計數器的值永遠不會小于 0,當計數器到0時,再調用acquire()就會阻塞,直到其他線程來調用release()。Semaphore 也支援上下文管理協定。
1.6 有界信号量 / BoundedSemaphore
threading子產品中的一個工廠函數,傳回一個新的有界信号量對象。一個有界信号量會確定它目前的值不超過它的初始值。如果超過,則引發ValueError。在大部分情況下,信号量用于守護有限容量的資源。如果信号量被釋放太多次,它是一種有bug的迹象。如果沒有給出,value預設為1。
1.7 同步原語 / Synchronization Primitive
當一個程序調用一個send原語時,在消息開始發送後,發送程序便處于阻塞狀态,直至消息完全發送完畢,send原語的後繼語句才能繼續執行。當一個程序調用一個receive原語時,并不立即傳回控制,而是等到把消息實際接收下來,并把它放入指定的接收區,才傳回控制,繼續執行該原語的後繼指令。在這段時間它一直處于阻塞狀态。上述的send和receive被稱為同步通信原語或阻塞通信原語。事件作為一種同步原語,是計算機科學中的一種同步機制,用來訓示等待中的程序特定條件已經變為真。
1.8 鎖 / Lock
對于鎖來說,其實鎖的本質是一個線程之間約定的控制權,即鎖的作用實質上并不能将某一資源進行鎖定,使其他線程無法修改。鎖的本質在于,多個線程之間對某一個鎖進行約定,約定這把鎖對應的公共資源,當需要對這把鎖對應的資源進行修改時,必須擁有這把鎖的權限才能進行。是以,當需要修改某資源時,就需要嘗試擷取對應的鎖,而當這把鎖的權限被其他線程擷取時,其餘需要擷取的線程就會進入阻塞等待狀态。且當鎖釋放時,權限的擷取是随機的,不論進入阻塞的時間先後。
1.9 互斥鎖和可重入鎖 / Mutex Lock and Reentrant Lock
對于互斥鎖和可重用鎖,互斥鎖隻能被擷取一次,若多次擷取則會産生阻塞,需等待原鎖釋放後才能再次入鎖。而可重入鎖則可被本線程多次acquire入鎖,但是要求入鎖次數與釋放次數相同,才能完全解鎖,且鎖的釋放需要在同一個線程中進行
Note: 對于可重入鎖來說,可多次入鎖的特性僅在本線程有效,也就是說,即使是可重入鎖,被一個線程擷取鎖定時,其他線程無法再次進入,隻有本線程可以。
1.10 死鎖 / Deadlock
死鎖出現在一個資源被多次調用,而調用方均未能釋放資源,便會造成死鎖現象。死鎖大緻可分為兩種形式出現,疊代死鎖和互相調用死鎖。一般死鎖是有互斥鎖造成的,使用可重入鎖則可以避免部分死鎖問題。
1.11 線程安全 / Thread Safety
線程安全就是多線程通路時,采用了加鎖機制,當一個線程通路該類的某個資料時,進行保護,其他線程不能進行通路直到該線程讀取完,其他線程才可使用。不會出現資料不一緻或者資料污染。 線程不安全就是不提供資料通路保護,有可能出現多個線程先後更改資料造成所得到的資料是髒資料。
參考連結
《Python 核心程式設計 第三版》
https://stackoverflow.com/questions/24744739/multi-threading-in-python-is-it-really-performance-effiicient-most-of-the-time