執行緒和程式
計算機,用於計算的機器。計算機的核心是CPU,在現在多核心的電腦很常見了。為了充分利用cpu核心做計算任務,程式實現了多執行緒模型。通過多執行緒實現多工的並行執行。
現在的作業系統多是多工作業系統。每個應用程式都有一個自己的程式。作業系統會為這些程式分配一些執行資源,例如記憶體空間等。在程式中,又可以建立一些執行緒,他們共享這些記憶體空間,並由作業系統呼叫,以便平行計算。
執行緒狀態
建立執行緒之後,執行緒並不是始終保持一個狀態。其狀態大概如下:
New
建立。Runnable
就緒。等待排程Running
執行。Blocked
阻塞。阻塞可能在Wait
Locked
Sleeping
Dead
消亡
這些狀態之間是可以相互轉換的,一圖勝千顏色:
(圖片引用 內心求法部落格)
執行緒中執行到阻塞,可能有3種情況:
- 同步:執行緒中獲取同步鎖,但是資源已經被其他執行緒鎖定時,進入Locked狀態,直到該資源可獲取(獲取的順序由Lock佇列控制)
- 睡眠:執行緒執行sleep()或join()方法後,執行緒進入Sleeping狀態。區別在於sleep等待固定的時間,而join是等待子執行緒執行完。當然join也可以指定一個“超時時間”。從語義上來說,如果兩個執行緒a,b, 在a中呼叫b.join(),相當於合併(join)成一個執行緒。最常見的情況是在主執行緒中join所有的子執行緒。
- 等待:執行緒中執行wait()方法後,執行緒進入Waiting狀態,等待其他執行緒的通知(notify)。
執行緒型別
執行緒有著不同的狀態,也有不同的型別。大致可分為:
- 主執行緒
- 子執行緒
- 守護執行緒(後臺執行緒)
- 前臺執行緒
Python執行緒與GIL
相比程式,執行緒更加輕量,可以實現併發。可是在python的世界裡,對於執行緒,就不得不說一句GIL(全域性直譯器鎖)。GIL的存在讓python的多執行緒多少有點雞肋了。Cpython的執行緒是作業系統原生的執行緒在直譯器解釋執行任何Python程式碼時,都需要先獲得這把鎖才行,在遇到 I/O 操作時會釋放這把鎖。因為python的程式做為一個整體,直譯器程式內只有一個執行緒在執行,其它的執行緒都處於等待狀態等著GIL的釋放。
關於GIL可以有更多的趣事,一時半會都說不完。總之python想用多執行緒併發,效果可能還不如單執行緒(執行緒切換耗時間)。想要利用多核,可以考慮使用多程式。
執行緒的建立
雖然python執行緒比較雞肋,可是也併發一無是處。多瞭解還是有理由對併發模型的理解。
Python提供兩個模組進行多執行緒的操作,分別是thread
和threading
,前者是比較低階的模組,用於更底層的操作,一般應有級別的開發不常用。後者則封裝了更多高階的介面,類似java的多執行緒風格,提供run
方法和start
呼叫。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import time import threading class MyThread(threading.Thread): def run(self): for i in range(5): print 'thread {}, @number: {}'.format(self.name, i) time.sleep(1) def main(): print "Start main threading" # 建立三個執行緒 threads = [MyThread() for i in range(3)] # 啟動三個執行緒 for t in threads: t.start() print "End Main threading" if __name__ == '__main__': main() |
輸入如下:(不同的環境不一樣)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Start main threading thread Thread-1, @number: 0 thread Thread-2, @number: 0 thread Thread-3, @number: 0 End Main threading thread Thread-1, @number: 1 thread Thread-3, @number: 1 thread Thread-2, @number: 1 thread Thread-3, @number: 2 thread Thread-1, @number: 2 thread Thread-2, @number: 2 thread Thread-2, @number: 3 thread Thread-1, @number: 3 thread Thread-3, @number: 3 |
每個執行緒都依次列印 0 – 3 三個數字,可是從輸出的結果觀察,執行緒並不是順序的執行,而是三個執行緒之間相互交替執行。此外,我們的主執行緒執行結束,將會列印 End Main threading
。從輸出結果可以知道,主執行緒結束後,新建的執行緒還在執行。
執行緒合併(join方法)
上述的例子中,主執行緒結束了,子執行緒還在執行。如果需要主執行緒等待子執行緒執行完畢再退出,可是使用執行緒的join
方法。join方法官網文件大概是
join(timeout)
方法將會等待直到執行緒結束。這將阻塞正在呼叫的執行緒,直到被呼叫join()方法的執行緒結束。
主執行緒或者某個函式如果建立了子執行緒,只要呼叫了子執行緒的join方法,那麼主執行緒就會被子執行緒所阻塞,直到子執行緒執行完畢再輪到主執行緒執行。其結果就是所有子執行緒執行完畢,才列印 End Main threading
。只需要修改上面的main
函式
1 2 3 4 5 6 7 8 9 10 11 12 13 |
def main(): print "Start main threading" threads = [MyThread() for i in range(3)] for t in threads: t.start() # 一次讓新建立的執行緒執行 join for t in threads: t.join() print "End Main threading" |
輸入如下:
1 2 3 4 5 6 7 8 9 10 |
Start main threading thread Thread-1, @number: 0 thread Thread-2, @number: 0 thread Thread-3, @number: 0 thread Thread-2, @number: 1 .... thread Thread-3, @number: 4 End Main threading Process finished with exit code 0 |
所有子執行緒結束了才會執行也行print "End Main threading"
。有人會這麼想,如果在 t.start()
之後join會怎麼樣?結果也能阻塞主執行緒,但是每個執行緒都是依次執行,變得有順序了。其實join很好理解,就字面上的意思就是子執行緒 “加入”(join)主執行緒嘛。在CPU執行時間片段上“等於”主執行緒的一部分。在start之後join,也就是每個子執行緒由被後來新建的子執行緒給阻塞了,因此執行緒之間變得有順序了。
借用moxie的總結:
1 join方法的作用是阻塞主程式(擋住,無法執行join以後的語句),專注執行多執行緒。
2 多執行緒多join的情況下,依次執行各執行緒的join方法,前頭一個結束了才能執行後面一個。
3 無引數,則等待到該執行緒結束,才開始執行下一個執行緒的join。
4 設定引數後,則等待該執行緒這麼長時間就不管它了(而該執行緒並沒有結束)。不管的意思就是可以執行後面的主程式了
執行緒同步與互斥鎖
執行緒之所以比程式輕量,其中一個原因就是他們共享記憶體。也就是各個執行緒可以平等的訪問記憶體的資料,如果在短時間“同時並行”讀取修改記憶體的資料,很可能造成資料不同步。例如下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
count = 0 class MyThread(threading.Thread): def run(self): global count time.sleep(1) for i in range(100): count += 1 print 'thread {} add 1, count is {}'.format(self.name, count) def main(): print "Start main threading" for i in range(10): MyThread().start() print "End Main threading" |
輸出結果如下,十個執行緒,每個執行緒增加100,運算結果應該是1000:
1 2 3 4 5 6 7 8 9 10 11 12 |
Start main threading End Main threading thread Thread-6 add 1, count is 161thread Thread-1 add 1, count is 433 thread Thread-7 add 1, count is 482 thread Thread-2 add 1, count is 100 thread Thread-9 add 1, count is 125 thread Thread-8 add 1, count is 335 thread Thread-5 add 1, count is 533thread Thread-3 add 1, count is 533 thread Thread-10 add 1, count is 261 thread Thread-4 add 1, count is 308 |
為了避免執行緒不同步造成是資料不同步,可以對資源進行加鎖。也就是訪問資源的執行緒需要獲得鎖,才能訪問。threading模組正好提供了一個Lock功能,修改程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# 建立鎖 mutex = threading.Lock() class MyThread(threading.Thread): def run(self): global count time.sleep(1) # 獲取鎖,修改資源 if mutex.acquire(): for i in range(100): count += 1 print 'thread {} add 1, count is {}'.format(self.name, count) # 釋放鎖 mutex.release() |
死鎖
有鎖就可以方便處理執行緒同步問題,可是多執行緒的複雜度和難以除錯的根源也來自於執行緒的鎖。利用不當,甚至會帶來更多問題。比如死鎖就是需要避免的問題。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
mutex_a = threading.Lock() mutex_b = threading.Lock() class MyThread(threading.Thread): def task_a(self): if mutex_a.acquire(): print "thread {} get mutex a ".format(self.name) time.sleep(1) if mutex_b.acquire(): print "thread {} get mutex b ".format(self.name) mutex_b.release() mutex_a.release() def task_b(self): if mutex_b.acquire(): print "thread {} get mutex a ".format(self.name) time.sleep(1) if mutex_a.acquire(): print "thread {} get mutex b ".format(self.name) mutex_a.release() mutex_b.release() def run(self): self.task_a() self.task_b() def main(): print "Start main threading" threads = [MyThread() for i in range(2)] for t in threads: t.start() print "End Main threading" |
執行緒需要執行兩個任務,兩個任務都需要獲取鎖,然而兩個任務先得到鎖後,就需要等另外鎖釋放。
可重入鎖
為了支援在同一執行緒中多次請求同一資源,python提供了可重入鎖
(RLock)。RLock內部維護著一個Lock和一個counter變數,counter記錄了acquire的次數,從而使得資源可以被多次require。直到一個執行緒所有的acquire都被release,其他的執行緒才能獲得資源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
mutex = threading.RLock() class MyThread(threading.Thread): def run(self): if mutex.acquire(1): print "thread {} get mutex".format(self.name) time.sleep(1) mutex.acquire() mutex.release() mutex.release() def main(): print "Start main threading" threads = [MyThread() for i in range(2)] for t in threads: t.start() print "End Main threading" |
條件變數
實用鎖可以達到執行緒同步,前面的互斥鎖就是這種機制。更復雜的環境,需要針對鎖進行一些條件判斷。Python提供了Condition物件。它除了具有acquire和release方法之外,還提供了wait和notify方法。執行緒首先acquire一個條件變數鎖。如果條件不足,則該執行緒wait,如果滿足就執行執行緒,甚至可以notify其他執行緒。其他處於wait狀態的執行緒接到通知後會重新判斷條件。
條件變數可以看成不同的執行緒先後acquire獲得鎖,如果不滿足條件,可以理解為被扔到一個(Lock或RLock)的waiting池。直達其他執行緒notify之後再重新判斷條件。該模式常用於生成消費者模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
queue = [] con = threading.Condition() class Producer(threading.Thread): def run(self): while True: if con.acquire(): if len(queue) > 100: con.wait() else: elem = random.randrange(100) queue.append(elem) print "Producer a elem {}, Now size is {}".format(elem, len(queue)) time.sleep(random.random()) con.notify() con.release() class Consumer(threading.Thread): def run(self): while True: if con.acquire(): if len(queue) < 0: con.wait() else: elem = queue.pop() print "Consumer a elem {}. Now size is {}".format(elem, len(queue)) time.sleep(random.random()) con.notify() con.release() def main(): for i in range(3): Producer().start() for i in range(2): Consumer().start() |
上述就是一個簡單的生產者消費模型,先看生產者,生產者條件變數鎖之後就檢查條件,如果不符合條件則wait,wait的時候會釋放鎖。如果條件符合,則往佇列新增元素,然後會notify其他執行緒。注意生產者呼叫了condition的notify()方法後,消費者被喚醒,但喚醒不意味著它可以開始執行,notify()並不釋放lock,呼叫notify()後,lock依然被生產者所持有。生產者通過con.release()顯式釋放lock。消費者再次開始執行,獲得條件鎖然後判斷條件執行。
佇列
生產消費者模型主要是對佇列程式操作,貼心的Python為我們實現了一個佇列結構,佇列內部實現了鎖的相關設定。可以用佇列重寫生產消費者模型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import Queue queue = Queue.Queue(10) class Producer(threading.Thread): def run(self): while True: elem = random.randrange(100) queue.put(elem) print "Producer a elem {}, Now size is {}".format(elem, queue.qsize()) time.sleep(random.random()) class Consumer(threading.Thread): def run(self): while True: elem = queue.get() queue.task_done() print "Consumer a elem {}. Now size is {}".format(elem, queue.qsize()) time.sleep(random.random()) def main(): for i in range(3): Producer().start() for i in range(2): Consumer().start() |
queue內部實現了相關的鎖,如果queue的為空,則get元素的時候會被阻塞,知道佇列裡面被其他執行緒寫入資料。同理,當寫入資料的時候,如果元素個數大於佇列的長度,也會被阻塞。也就是在 put 或 get的時候都會獲得Lock。
執行緒通訊
執行緒可以讀取共享的記憶體,通過記憶體做一些資料處理。這就是執行緒通訊的一種,python還提供了更加高階的執行緒通訊介面。Event
物件可以用來進行執行緒通訊,呼叫event物件的wait
方法,執行緒則會阻塞等待,直到別的執行緒set
之後,才會被喚醒。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class MyThread(threading.Thread): def __init__(self, event): super(MyThread, self).__init__() self.event = event def run(self): print "thread {} is ready ".format(self.name) self.event.wait() print "thread {} run".format(self.name) signal = threading.Event() def main(): start = time.time() for i in range(3): t = MyThread(signal) t.start() time.sleep(3) print "after {}s".format(time.time() - start) signal.set() |
上面的例子建立了3個執行緒,呼叫執行緒之後,執行緒將會被阻塞,sleep 3秒後,才會被喚醒執行,大概輸出如下:
1 2 3 4 5 6 7 |
thread Thread-1 is ready thread Thread-2 is ready thread Thread-3 is ready after 3.00441598892s thread Thread-2 run thread Thread-3 run thread Thread-1 run |
後臺執行緒
預設情況下,主執行緒退出之後,即使子執行緒沒有join。那麼主執行緒結束後,子執行緒也依然會繼續執行。如果希望主執行緒退出後,其子執行緒也退出而不再執行,則需要設定子執行緒為後臺執行緒。python提供了seDeamon方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class MyThread(threading.Thread): def run(self): wait_time = random.randrange(1, 10) print "thread {} will wait {}s".format(self.name, wait_time) time.sleep(wait_time) print "thread {} finished".format(self.name) def main(): print "Start main threading" for i in range(5): t = MyThread() t.setDaemon(True) t.start() print "End Main threading" |
輸出結果如下:
1 2 3 4 5 6 |
Start main threading thread Thread-1 will wait 3s thread Thread-2 will wait 6s thread Thread-3 will wait 4s thread Thread-4 will wait 6s thread Thread-5 will wait 2sEnd Main threading |
每個執行緒都應該等待sleep幾秒,可是主執行緒很快就執行完了,子執行緒因為設定了後臺執行緒,所以也跟著主執行緒退出了。
關於Python多執行緒的介紹暫且就這些,多執行緒用於併發任務。對於併發模型,Python還有比執行緒更好的方法。同樣設計任務的時候,也需要考慮是計算密集型還是IO密集型。針對不同的場景,設計不同的程式系統。
文中的程式碼 learn-threading
參考資料:
1 http://www.cnblogs.com/holbrook/tag/%E5%A4%9A%E7%BA%BF%E7%A8%8B/
2 http://zhuoqiang.me/python-thread-gil-and-ctypes.html
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式