Python基礎知識點梳理6,推薦收藏
- 2019 年 10 月 5 日
- 筆記
今天整理的文章是Python的執行緒,執行緒在高級程式語言中是一個重點也是難點,今天我們一起看看Python的執行緒操作。
執行緒
多任務可以由多進程完成,也可以由一個進程內的多執行緒完成。
我們前面提到了進程是由若干執行緒組成的,一個進程至少有一個執行緒。
多執行緒類似於同時執行多個不同程式,多執行緒運行有如下優點:
- 可以把運行時間長的任務放到後台去處理。
- 用戶介面可以更加吸引人,比如用戶點擊了一個按鈕去觸發某些事件的處理,可以彈出一個進度條來顯示處理的進度。
- 程式的運行速度可能加快。
- 在一些需要等待的任務實現上,如用戶輸人、文件讀寫和網路收發數據等,執行緒就比較有用了。在這種情況下我們可以釋放一些珍貴的資源,如記憶體佔用等。
Python的標準庫提供了兩個模組: thread 和threading
,thread
是低級模組,threading
是高級模組,對thread
進行了封裝。絕大多數情況下,我們只需要使用threading這個高級模組。
啟動一個執行緒就是把一個函數傳入並創建Thread實例,然後調用start()開始執行:
# -*- coding:utf-8 -*- 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 得到: 46, 且每次運行結果都會不一樣。
我們定義了一個共享變數balance,初始值為0,並且啟動兩個執行緒,先存後取,理論上結果應該為0,但是,由於執行緒的調度是由作業系統決定的,當t1、t2交替執行時,只要循環次數足夠多,balance的結果就不一定是0了。
由於彼此間的交替運算,所以結果會發生變化,如果是在銀行操作,一存一取就可能導致餘額不對,所以必須確保一個執行緒在修改balance的時候,別的執行緒一定不能改。
如果我們要確保balance計算正確,就要給change_it()上一把鎖,當某個執行緒開始執行change_it()時,我們說,該執行緒因為獲得了鎖,因此其他執行緒不能同時執行change_it(),只能等待,直到鎖被釋放後,獲得該鎖以後才能改。由於鎖只有一個,無論多少執行緒,同一時刻最多只有一個執行緒持有該鎖,所以,不會造成修改的衝突。創建一個鎖就是通過threading.Lock()來實現:
修改後的程式碼:
# -*- coding:utf-8 -*- import time, threading # 假定這是你的銀行存款: balance = 0 def change_it(n): # 先存後取,結果應該為0: global balance balance = balance + n balance = balance - n lock = threading.Lock() def run_thread(n): for i in range(100000): # 先要獲取鎖: lock.acquire() try: # 放心地改吧: change_it(n) finally: # 改完了一定要釋放鎖: lock.release() 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 結果,無論怎麼執行都是0,這正是我們期望的結果。
當多個執行緒同時執行lock.acquire()時,只有一個執行緒能成功地獲取鎖,然後繼續執行程式碼,其他執行緒就繼續等待直到獲得鎖為止。
獲得鎖的執行緒用完後一定要釋放鎖,否則那些苦苦等待鎖的執行緒將永遠等待下去,成為死執行緒。所以我們用try…finally來確保鎖一定會被釋放。
鎖的好處就是確保了某段關鍵程式碼只能由一個執行緒從頭到尾完整地執行,壞處當然也很多,首先是阻止了多執行緒並發執行,包含鎖的某段程式碼實際上只能以單執行緒模式執行,效率就大大地下降了。其次,由於可以存在多個鎖,不同的執行緒持有不同的鎖,並試圖獲取對方持有的鎖時,可能會造成死鎖,導致多個執行緒全部掛起,既不能執行,也無法結束,只能靠作業系統強制終止。
全局解釋器
如果你不幸擁有一個多核CPU,你肯定在想,多核應該可以同時執行多個執行緒。
在Python的原始解釋器CPython中存在著GIL(Global Interpreter Lock,全局解釋器鎖)因此在解釋執行Python程式碼時,會產生互斥鎖來限制執行緒對共享資源的訪問,直到解釋器遇到I/O操作或者操作次數達到一定數據時支委會釋放GIL。由於全局器鎖的存在,在進行多執行緒操作的時候,不能調用多個CPU內核,只能利用一個內核,所以在進行CPU密集型操作的時候,不推薦使用多執行緒,更加傾向於多進程,那麼多執行緒適合什麼樣的應用場景呢?對於IO密集型操作,多執行緒可以明顯提高效率,例如Python爬蟲的開發,絕大多數時間爬蟲是在等待socket返回數據,網路IO操作延時比CPU大得多。
ThreadLocal
在多執行緒環境下,每個執行緒都有自己的數據。一個執行緒使用自己的局部變數比使用全局變數好,因為局部變數只有執行緒自己能看見,不會影響其他執行緒,而全局變數的修改必須加鎖。
但是局部變數也有問題,就是在函數調用的時候,傳遞起來很麻煩:
def process_student(name): std = Student(name) # std是局部變數,但是每個函數都要用它,因此必須傳進去: do_task_1(std) do_task_2(std) def do_task_1(std): do_subtask_1(std) do_subtask_2(std) def do_task_2(std): do_subtask_2(std) do_subtask_2(std)
每個函數一層一層調用都這麼傳參數那還得了?用全局變數?也不行,因為每個執行緒處理不同的Student對象,不能共享。
如果用一個全局dict存放所有的Student對象,然後以thread自身作為key獲得執行緒對應的Student對象如何?
global_dict = {} def std_thread(name): std = Student(name) # 把std放到全局變數global_dict中: global_dict[threading.current_thread()] = std do_task_1() do_task_2() def do_task_1(): # 不傳入std,而是根據當前執行緒查找: std = global_dict[threading.current_thread()] ... def do_task_2(): # 任何函數都可以查找出當前執行緒的std變數: std = global_dict[threading.current_thread()] ...
這種方式理論上是可行的,它最大的優點是消除了std對象在每層函數中的傳遞問題,但是,每個函數獲取std的程式碼有點丑。
有沒有更簡單的方式?
ThreadLocal應運而生,不用查找dict,ThreadLocal幫你自動做這件事:
import threading # 創建全局ThreadLocal對象: local_school = threading.local() def process_student(): print('Hello, %s (in %s)' % (local_school.student, threading.current_thread().name)) def process_thread(name): # 綁定ThreadLocal的student: local_school.student = name process_student() t1 = threading.Thread(target= process_thread, args=('Alice',), name='Thread-A') t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B') t1.start() t2.start() t1.join() t2.join() 得到: Hello, Alice (in Thread-A) Hello, Bob (in Thread-B)
全局變數local_school
就是一個ThreadLocal
對象,每個Thread
對它都可以讀寫student
屬性,但互不影響。你可以把local_school
看成全局變數,但每個屬性如local_school.student
都是執行緒的局部變數,可以任意讀寫而互不干擾,也不用管理鎖的問題,ThreadLocal內部會處理。
可以理解為全局變數local_school
是一個dict,不但可以用local_school.student
,還可以綁定其他變數,如local_school.teacher
等等。
ThreadLocal
最常用的地方就是為每個執行緒綁定一個資料庫連接,HTTP請求,用戶身份資訊等,這樣一個執行緒的所有調用到的處理函數都可以非常方便地訪問這些資源。