多任務可以由多進程完成,也可以由一個進程內的多線程完成。
我們前面提到了進程是由若干線程組成的,一個進程至少有一個線程。
由于線程是操作系統直接支持的執行單元,因此,高級語言通常都內置多線程的支持,Python也不例外,并且,Python的線程是真正的Posix Thread,而不是模擬出來的線程。
Python的標準庫提供了兩個模塊:`_thread`和`threading`,`_thread`是低級模塊,`threading`是高級模塊,對`_thread`進行了封裝。絕大多數情況下,我們只需要使用`threading`這個高級模塊。
啟動一個線程就是把一個函數傳入并創建`Thread`實例,然后調用`start()`開始執行:
~~~
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 >>> 1
thread LoopThread >>> 2
thread LoopThread >>> 3
thread LoopThread >>> 4
thread LoopThread >>> 5
thread LoopThread ended.
thread MainThread ended.
~~~
由于任何進程默認就會啟動一個線程,我們把該線程稱為主線程,主線程又可以啟動新的線程,Python的`threading`模塊有個`current_thread()`函數,它永遠返回當前線程的實例。主線程實例的名字叫`MainThread`,子線程的名字在創建時指定,我們用`LoopThread`命名子線程。名字僅僅在打印時用來顯示,完全沒有其他意義,如果不起名字Python就自動給線程命名為`Thread-1`,`Thread-2`……
### Lock
多線程和多進程最大的不同在于,多進程中,同一個變量,各自有一份拷貝存在于每個進程中,互不影響,而多線程中,所有變量都由所有線程共享,所以,任何一個變量都可以被任何一個線程修改,因此,線程之間共享數據最大的危險在于多個線程同時改一個變量,把內容給改亂了。
來看看多個線程同時操作一個變量怎么把內容給改亂了:
~~~
import time, threading
# 假定這是你的銀行存款:
balance = 0
def change_it(n):
# 先存后取,結果應該為0:
global balance
balance = balance + n
balance = balance - n
def 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`,但是,由于線程的調度是由操作系統決定的,當t1、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是交替運行的,如果操作系統以下面的順序執行t1、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 - 5 # x2 = 0 - 5 = -5
t2: balance = x2 # balance = -5
結果 balance = -5
~~~
究其原因,是因為修改`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 ^ 1
for 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中就是一個美麗的夢。
### 參考源碼
[multi_threading.py](https://github.com/michaelliao/learn-python3/blob/master/samples/multitask/multi_threading.py)
[do_lock.py](https://github.com/michaelliao/learn-python3/blob/master/samples/multitask/do_lock.py)
- 關于
- Python簡介
- 安裝Python
- Python解釋器
- 第一個Python程序
- 使用文本編輯器
- Python代碼運行助手
- 輸入和輸出
- Python基礎
- 數據類型和變量
- 字符串和編碼
- 使用list和tuple
- 條件判斷
- 循環
- 使用dict和set
- 函數
- 調用函數
- 定義函數
- 函數的參數
- 遞歸函數
- 高級特性
- 切片
- 迭代
- 列表生成式
- 生成器
- 迭代器
- 函數式編程
- 高階函數
- 返回函數
- 匿名函數
- 裝飾器
- 偏函數
- 模塊
- 使用模塊
- 安裝第三方模塊
- 面向對象編程
- 類和實例
- 訪問限制
- 繼承和多態
- 獲取對象信息
- 實例屬性和類屬性
- 面向對象高級編程
- 使用slots
- 使用@property
- 多重繼承
- 定制類
- 使用枚舉類
- 使用元類
- 錯誤、調試和測試
- 錯誤處理
- 調試
- 單元測試
- 文檔測試
- IO編程
- 文件讀寫
- StringIO和BytesIO
- 操作文件和目錄
- 序列化
- 進程和線程
- 多進程
- 多線程
- ThreadLocal
- 進程 vs. 線程
- 分布式進程
- 正則表達式
- 常用內建模塊
- datetime
- collections
- base64
- struct
- hashlib
- itertools
- XML
- HTMLParser
- urllib
- 常用第三方模塊
- PIL
- virtualenv
- 圖形界面
- 網絡編程
- TCP/IP簡介
- TCP編程
- UDP編程
- 電子郵件
- SMTP發送郵件
- POP3收取郵件
- 訪問數據庫
- 使用SQLite
- 使用MySQL
- 使用SQLAlchemy
- Web開發
- HTTP協議簡介
- HTML簡介
- WSGI接口
- 使用Web框架
- 使用模板
- 異步IO
- 協程
- asyncio
- aiohttp
- 實戰
- Day 1 - 搭建開發環境
- Day 2 - 編寫Web App骨架
- Day 3 - 編寫ORM
- Day 4 - 編寫Model
- Day 5 - 編寫Web框架
- Day 6 - 編寫配置文件
- Day 7 - 編寫MVC
- Day 8 - 構建前端
- Day 9 - 編寫API
- Day 10 - 用戶注冊和登錄
- Day 11 - 編寫日志創建頁
- Day 12 - 編寫日志列表頁
- Day 13 - 提升開發效率
- Day 14 - 完成Web App
- Day 15 - 部署Web App
- Day 16 - 編寫移動App
- FAQ
- 期末總結