day9-01 線程

多任務能夠由多進程完成,也能夠由一個進程內的多線程完成。python

咱們前面提到了進程是由若干線程組成的,一個進程至少有一個線程。編程

因爲線程是操做系統直接支持的執行單元,所以,高級語言一般都內置多線程的支持,Python也不例外,而且,Python的線程是真正的Posix Thread,而不是模擬出來的線程。多線程

Python的標準庫提供了兩個模塊:_threadthreading_thread是低級模塊,threading是高級模塊,對_thread進行了封裝。絕大多數狀況下,咱們只須要使用threading這個高級模塊。併發

啓動一個線程就是把一個函數傳入並建立Thread實例,而後調用start()開始執行:ide

import time, threading
# 新線程執行的代碼:
def loop():
    print('thread %s is running...' % threading.current_thread().name)
    n = 0
    while n < 5:
        n = n + 1
        print('thread %s >>> %s' % (threading.current_thread().name, n))
        time.sleep(1)
    print('thread %s ended.' % threading.current_thread().name)

print('thread %s is running...' % threading.current_thread().name)
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join()
print('thread %s ended.' % threading.current_thread().name)

執行結果以下:函數

thread MainThread is running...
thread LoopThread is running...
thread LoopThread 
>>> 1thread LoopThread 
>>> 2thread LoopThread 
>>> 3thread LoopThread 
>>> 4thread LoopThread 
>>> 5thread LoopThread ended.
thread MainThread ended.

因爲任何進程默認就會啓動一個線程,咱們把該線程稱爲主線程,主線程又能夠啓動新的線程,Python的threading模塊有個current_thread()函數,它永遠返回當前線程的實例。主線程實例的名字叫MainThread,子線程的名字在建立時指定,咱們用LoopThread命名子線程。名字僅僅在打印時用來顯示,徹底沒有其餘意義,若是不起名字Python就自動給線程命名爲Thread-1Thread-2……oop

Lock

多線程和多進程最大的不一樣在於,多進程中,同一個變量,各自有一份拷貝存在於每一個進程中,互不影響,而多線程中,全部變量都由全部線程共享,因此,任何一個變量均可以被任何一個線程修改,所以,線程之間共享數據最大的危險在於多個線程同時改一個變量,把內容給改亂了。ui

來看看多個線程同時操做一個變量怎麼把內容給改亂了:操作系統

import time, threading
# 假定這是你的銀行存款:
balance = 0def change_it(n):
    # 先存後取,結果應該爲0:
    global balance
    balance = balance + n
    balance = balance - ndef run_thread(n):
    for i in range(100000):
        change_it(n)

t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)

咱們定義了一個共享變量balance,初始值爲0,而且啓動兩個線程,先存後取,理論上結果應該爲0,可是,因爲線程的調度是由操做系統決定的,當t一、t2交替執行時,只要循環次數足夠多,balance的結果就不必定是0了。線程

緣由是由於高級語言的一條語句在CPU執行時是若干條語句,即便一個簡單的計算:

balance = balance + n

也分兩步:

  1. 計算balance + n,存入臨時變量中;

  2. 將臨時變量的值賦給balance

也就是能夠當作:

x = balance + n
balance = x

因爲x是局部變量,兩個線程各自都有本身的x,當代碼正常執行時:

初始值 balance = 0
t1: x1 = balance + 5 # x1 = 0 + 5 = 5
t1: balance = x1     # balance = 5
t1: x1 = balance - 5 # x1 = 5 - 5 = 0
t1: balance = x1     # balance = 0
t2: x2 = balance + 8 # x2 = 0 + 8 = 8
t2: balance = x2     # balance = 8
t2: x2 = balance - 8 # x2 = 8 - 8 = 0
t2: balance = x2     # balance = 0

結果 balance = 0

可是t1和t2是交替運行的,若是操做系統如下面的順序執行t一、t2:

初始值 balance = 0

t1: x1 = balance + 5  # x1 = 0 + 5 = 5
t2: x2 = balance + 8  # x2 = 0 + 8 = 8
t2: balance = x2      # balance = 8
t1: balance = x1      # balance = 5
t1: x1 = balance - 5  # x1 = 5 - 5 = 0
t1: balance = x1      # balance = 0
t2: x2 = balance - 8  # x2 = 0 - 8 = -8
t2: balance = x2   # balance = -8
結果 balance = -8

究其緣由,是由於修改balance須要多條語句,而執行這幾條語句時,線程可能中斷,從而致使多個線程把同一個對象的內容改亂了。

兩個線程同時一存一取,就可能致使餘額不對,你確定不但願你的銀行存款莫名其妙地變成了負數,因此,咱們必須確保一個線程在修改balance的時候,別的線程必定不能改。

若是咱們要確保balance計算正確,就要給change_it()上一把鎖,當某個線程開始執行change_it()時,咱們說,該線程由於得到了鎖,所以其餘線程不能同時執行change_it(),只能等待,直到鎖被釋放後,得到該鎖之後才能改。因爲鎖只有一個,不管多少線程,同一時刻最多隻有一個線程持有該鎖,因此,不會形成修改的衝突。建立一個鎖就是經過threading.Lock()來實現:

balance = 0
lock = threading.Lock()
def run_thread(n):
    for i in range(100000):
        # 先要獲取鎖:
        lock.acquire()
        try:
            # 放心地改吧:
            change_it(n)
        finally:
            # 改完了必定要釋放鎖:
            lock.release()

當多個線程同時執行lock.acquire()時,只有一個線程能成功地獲取鎖,而後繼續執行代碼,其餘線程就繼續等待直到得到鎖爲止。

得到鎖的線程用完後必定要釋放鎖,不然那些苦苦等待鎖的線程將永遠等待下去,成爲死線程。因此咱們用try...finally來確保鎖必定會被釋放。

鎖的好處就是確保了某段關鍵代碼只能由一個線程從頭至尾完整地執行,壞處固然也不少,首先是阻止了多線程併發執行,包含鎖的某段代碼實際上只能以單線程模式執行,效率就大大地降低了。其次,因爲能夠存在多個鎖,不一樣的線程持有不一樣的鎖,並試圖獲取對方持有的鎖時,可能會形成死鎖,致使多個線程所有掛起,既不能執行,也沒法結束,只能靠操做系統強制終止。

多核CPU

若是你不幸擁有一個多核CPU,你確定在想,多核應該能夠同時執行多個線程。

若是寫一個死循環的話,會出現什麼狀況呢?

打開Mac OS X的Activity Monitor,或者Windows的Task Manager,均可以監控某個進程的CPU使用率。

咱們能夠監控到一個死循環線程會100%佔用一個CPU。

若是有兩個死循環線程,在多核CPU中,能夠監控到會佔用200%的CPU,也就是佔用兩個CPU核心。

要想把N核CPU的核心所有跑滿,就必須啓動N個死循環線程。

試試用Python寫個死循環:

import threading, multiprocessing
def loop():
    x = 0
    while True:
        x = x ^ 1for i in range(multiprocessing.cpu_count()):
    t = threading.Thread(target=loop)
    t.start()

啓動與CPU核心數量相同的N個線程,在4核CPU上能夠監控到CPU佔用率僅有102%,也就是僅使用了一核。

可是用C、C++或Java來改寫相同的死循環,直接能夠把所有核心跑滿,4核就跑到400%,8核就跑到800%,爲何Python不行呢?

由於Python的線程雖然是真正的線程,但解釋器執行代碼時,有一個GIL鎖:Global Interpreter Lock,任何Python線程執行前,必須先得到GIL鎖,而後,每執行100條字節碼,解釋器就自動釋放GIL鎖,讓別的線程有機會執行。這個GIL全局鎖實際上把全部線程的執行代碼都給上了鎖,因此,多線程在Python中只能交替執行,即便100個線程跑在100核CPU上,也只能用到1個核。

GIL是Python解釋器設計的歷史遺留問題,一般咱們用的解釋器是官方實現的CPython,要真正利用多核,除非重寫一個不帶GIL的解釋器。

因此,在Python中,可使用多線程,但不要期望能有效利用多核。若是必定要經過多線程利用多核,那隻能經過C擴展來實現,不過這樣就失去了Python簡單易用的特色。

不過,也不用過於擔憂,Python雖然不能利用多線程實現多核任務,但能夠經過多進程實現多核任務。多個Python進程有各自獨立的GIL鎖,互不影響。

小結

多線程編程,模型複雜,容易發生衝突,必須用鎖加以隔離,同時,又要當心死鎖的發生。

Python解釋器因爲設計時有GIL全局鎖,致使了多線程沒法利用多核。多線程的併發在Python中就是一個美麗的夢

相關文章
相關標籤/搜索