標籤:st3 run方法 通訊機制 部分 wait %s int 建立線程 釋放
多線程
多線程是程式在同樣的上下文中同時運行多條線程的能力。這些線程共用同一個進程的資源,可以在併發模式(單核處理器)或並行模式(多核處理器)下執行多個任務
多線程有以下幾個優點:
- 持續響應:在單線程的程式中,執行一個長期啟動並執行任務可能會導致程式的凍結。多線程可以把這個長期啟動並執行任務放在一個線程中,在程式並發的運行任務時可以持續響應客戶的需求
- 更快的執行速度:在多核處理器的作業系統上,多線程可以通過真正的並行提高程式的運行速度
- 較低的資源消耗:利用線程模式,程式可以利用一個進程內的資源響應多個請求
- 更簡單的狀態共用與進程間的通訊機制:由於線程都共用同一資源和記憶體空間,因此線程之間的通比處理序間通訊簡單
- 並行化:多處理器系統可以實現多線程的每個線程獨立運行
但是多線程也有以下幾個缺點:
- 線程同步:由於多個線程是在同一資料上啟動並執行,所以需要引入一些機制預防競態條件
- 問題線程導致集體崩潰:雖然多個線程可以獨立運行,但一旦某個線程出現問題,也可能造成整個進程崩潰
- 死結:這是線程操作的常見問題。通常,線程執行任務時會鎖住正在使用的資源,當一個線程開始等待另一個線程資源釋放,而另一個線程同時也要等待第一個線程釋放資源時,就發生了死結
通常,多線程技術完全可以在多處理器上實現並行計算。但是Python的官方版本(CPython)有一個GIL限制,GIL會阻止多個線程同時運行Python的位元組碼,這就不是真正的並行了。假如你的系統有6個處理器,多線程可以把CPU跑到
600%,然而,你能看到的只有100%,甚至更慢一點,這都是GIL造成的
CPython的GIL是有必要的,因為CPython的記憶體管理不是安全執行緒的。因此,為了讓每個任務都按順序進行,它需要確保運行過程中記憶體不被幹擾。它可以更快的運行單線程程式,簡化C語言擴充庫的使用方法,因為它不需要考慮多線程問題。
但是,GIL是可以用一些辦法繞過的。例如,由於GIL只阻止多個線程同時運行Python的位元組碼,所以可以用C語言寫程式,然後用Python封裝。這樣,在程式運行過程中GIL就不會干擾多線程並發了
另一個GIL不影響效能的樣本就是網路伺服器了,伺服器大部分時間都在讀資料包,而當發生IO等待時,會嘗試釋放GIL。這種情況下,增加線程可以讀取更多的包,雖然這並不是真正的並行。這樣做可以增加伺服器的效能,但是不會影響速度。
用_thread模組建立線程
我們先用一個例子快速示範_thread模組的用法:_thread模組提供了start_new_thread方法。我們可以向裡面傳入以下參數:
- 目標函數:裡麵包含我們要啟動並執行代碼,一旦函數傳回值,線程就停止運行
- 參數:即執行目標函數所需的參數,一般以元組的形式傳入
import _threadimport timedef print_time(thread_name, delay): count = 0 while count < 5: time.sleep(delay) count += 1 print("%s:%s" % (thread_name, time.ctime(time.time())))try: _thread.start_new_thread(print_time, ("thread-A", 1)) _thread.start_new_thread(print_time, ("thread-B", 2))except: print("Error: unable to start thread")while 1: pass
運行結果:
thread-A:Sun Jul 8 07:39:27 2018thread-B:Sun Jul 8 07:39:28 2018thread-A:Sun Jul 8 07:39:28 2018thread-A:Sun Jul 8 07:39:29 2018thread-B:Sun Jul 8 07:39:30 2018thread-A:Sun Jul 8 07:39:30 2018thread-A:Sun Jul 8 07:39:31 2018thread-B:Sun Jul 8 07:39:32 2018thread-B:Sun Jul 8 07:39:34 2018thread-B:Sun Jul 8 07:39:36 2018
上面的例子很簡單,線程A和線程B是並發執行的。
_thread模組還提供了一些容易使用的線程原生介面:
- _thread.interrupt_main():這個方法可以向主線程發送中斷異常,就像通過鍵盤向程式輸入CTRL+C一樣,我們修改print_time方法,當count為2,休眠時間delay為2向主線程發送中斷異常
def print_time(thread_name, delay): count = 0 while count < 5: time.sleep(delay) count += 1 if count == 2 and delay == 2: _thread.interrupt_main() print("%s:%s" % (thread_name, time.ctime(time.time())))
運行結果:
thread-A:Sun Jul 8 09:12:57 2018thread-B:Sun Jul 8 09:12:58 2018thread-A:Sun Jul 8 09:12:58 2018thread-A:Sun Jul 8 09:12:59 2018thread-B:Sun Jul 8 09:13:00 2018Traceback (most recent call last): File "D:/pypath/hello/test3/test01.py", line 22, in <module> passKeyboardInterrupt
- exit:這個方法會從後台退出程式,它的優點是中斷線程時不會引起其他異常
def print_time(thread_name, delay): count = 0 while count < 5: time.sleep(delay) count += 1 if count == 2 and delay == 2: _thread.exit() print("%s:%s" % (thread_name, time.ctime(time.time())))
運行結果:
thread-A:Sun Jul 8 09:15:51 2018thread-B:Sun Jul 8 09:15:52 2018thread-A:Sun Jul 8 09:15:52 2018thread-A:Sun Jul 8 09:15:53 2018thread-A:Sun Jul 8 09:15:54 2018thread-A:Sun Jul 8 09:15:55 2018
allocate_lock方法可以為線程返回一個線程鎖,這個鎖可以保護某一代碼塊從開始運行到運行結束只有一個線程,線程鎖對象有三個方法:
- acquire:這個方法的主要作用是為當前的線程請求一把線程鎖。它接受一個可選的整型參數,如果參數是0,那麼線程鎖一旦被請求則立即擷取,不需要等待,如果參數不是0,則表示線程可以等待鎖
- release:這個方法會釋放線程鎖,讓下一個線程擷取
- locked:如果線程鎖被某個線程擷取,就返回True,否則為False
下面這段代碼用10個線程對一個全域變數增加值,因此,理想情況下,全域變數的值應該是10:
import _threadimport timeglobal_values = 0def run(thread_name): global global_values local_copy = global_values print("%s with value %s" % (thread_name, local_copy)) global_values = local_copy + 1for i in range(10): _thread.start_new_thread(run, ("thread-(%s)" % str(i),))time.sleep(3)print("global_values:%s" % global_values)
運行結果:
thread-(0) with value 0thread-(1) with value 0thread-(2) with value 0thread-(4) with value 0thread-(6) with value 0thread-(8) with value 0thread-(7) with value 0thread-(5) with value 0thread-(3) with value 0thread-(9) with value 1global_values:2
但是很遺憾,我們沒有得到我們希望的結果,相反,程式啟動並執行結果和我們希望的結果差距更遠。造成這樣的原因,都是因為多個線程操作同一變數或同一代碼塊導致有的線程不能讀到最新的值,甚至是把舊值的運算結果賦給全部局變數
現在,讓我們修改一下原先的代碼:
import _threadimport timeglobal_values = 0def run(thread_name, lock): global global_values lock.acquire() local_copy = global_values print("%s with value %s" % (thread_name, local_copy)) global_values = local_copy + 1 lock.release()lock = _thread.allocate_lock()for i in range(10): _thread.start_new_thread(run, ("thread-(%s)" % str(i), lock))time.sleep(3)print("global_values:%s" % global_values)
運行結果:
thread-(0) with value 0thread-(2) with value 1thread-(4) with value 2thread-(5) with value 3thread-(3) with value 4thread-(6) with value 5thread-(1) with value 6thread-(7) with value 7thread-(8) with value 8thread-(9) with value 9
現在可以看到,線程的執行順序依舊是亂序的,但全域變數的值是逐個遞增的
_thread還有其他一些方法:
- _thread.get_ident():這個方法會返回一個非0的整數,代表當前活動線程的id。這個整數會線上程結束或退出後收回,因此在整個程式的生命週期中它並不是唯一
- _thread.stack_size(size):size這個參數是可選項,可在代碼建立新線程時設定或返回線程棧的容量,這個容量可以是0,或者至少32KB,具體由作業系統決定
用threading模組建立線程
這是目前Python中處理線程普遍推薦的模組,這個模組提供了更完善和進階的介面,我們嘗試將前面的樣本轉化成threading模組的形式:
import threadingimport timeglobal_values = 0def run(thread_name, lock): global global_values lock.acquire() local_copy = global_values print("%s with value %s" % (thread_name, local_copy)) global_values = local_copy + 1 lock.release()lock = threading.Lock()for i in range(10): t = threading.Thread(target=run, args=("thread-(%s)" % str(i), lock)) t.start()time.sleep(3)print("global_values:%s" % global_values)
對於更複雜的情況,如果要更好地封裝線程的行為,我們可能需要建立自己的線程類,這裡需要注意幾點:
- 需要繼承thread.Thread類
- 需要改寫run方法,也可以使用__init__方法
- 如果改寫初始化方法__init__,需要在一開始調用父類的初始化方法Thread.__init__
- 當線程的run方法停止或拋出未處理的異常時,線程將停止,因此要提前設計好方法
- 可以用初始化方法的name參數名稱命名你的線程
import threadingimport timeclass MyThread(threading.Thread): def __init__(self, count): threading.Thread.__init__(self) self.total = count def run(self): for i in range(self.total): time.sleep(1) print("Thread:%s - %s" % (self.name, i))t = MyThread(2)t2 = MyThread(3)t.start()t2.start()print("finish")
運行結果:
finishThread:Thread-2 - 0Thread:Thread-1 - 0Thread:Thread-2 - 1Thread:Thread-1 - 1Thread:Thread-2 - 2
注意上面主線程先列印了finish,之後才列印其他線程裡面的print語句,這並不是什麼大問題,但下面的情況就有問題了:
f = open("content.txt", "w+")t = MyThread(2, f)t2 = MyThread(3, f)t.start()t2.start()f.close()
我們假設在MyThread中會將列印的語句寫入content.txt,但這段代碼是會出問題的,因為在開啟其他線程前,主線程可能會先關閉檔案處理器,如果想避免這種情況,應該使用join方法,join方法會使得被調用的線程執行完畢後,在能返回原先的線程繼續執行下去:
f = open("content.txt", "w+")t = MyThread(2, f)t2 = MyThread(3, f)t.start()t2.start()t.join()t2.join()f.close()print("finish")
join方法還支援一個選擇性參數:時限(浮點數或None),以秒為單位。但是join傳回值是None。因此,要檢查操作是否已逾時,需要在join方法返回後查看線程的啟用狀態,如果線程的狀態是啟用的,操作就逾時了
再來看一個樣本,它檢查一組網站的請求狀態代碼:
from urllib.request import urlopensites = [ "https://www.baidu.com/", "http://www.sina.com.cn/", "http://www.qq.com/"]def check_http_status(url): return urlopen(url).getcode()http_status = {}for url in sites: http_status[url] = check_http_status(url)for key, value in http_status.items(): print("%s %s" % (key, value))
運行結果:
# time python3 test01.py https://www.baidu.com/ 200http://www.sina.com.cn/ 200http://www.qq.com/ 200real0m1.669suser0m0.143ssys0m0.026s
現在,我們嘗試著把IO操作函數轉變為一個線程來最佳化代碼:
from urllib.request import urlopenimport threadingsites = [ "https://www.baidu.com/", "http://www.sina.com.cn/", "http://www.qq.com/"]class HttpStatusChecker(threading.Thread): def __init__(self, url): threading.Thread.__init__(self) self.url = url self.status = None def run(self): self.status = urlopen(self.url).getcode()threads = []http_status = {}for url in sites: t = HttpStatusChecker(url) t.start() threads.append(t)for t in threads: t.join()for t in threads: print("%s %s" % (t.url, t.status))
運行結果:
# time python3 test01.py https://www.baidu.com/ 200http://www.sina.com.cn/ 200http://www.qq.com/ 200real0m0.237suser0m0.110ssys0m0.019s
顯然,線程版的程式更快,運行速度幾乎是上一版的8倍,效能改善十分顯著
通過Event對象實現線程間通訊
雖然線程通常是作為獨立運行或並行的任務,但是有時也會出現線程間通訊的需求,threading模組提供了事件(event)對象實現線程間通訊,它包含一個內部標記,以及可以使用set()和clear()方法的調用線程
Event類的介面很簡單,它支援的方法如下:
- is_set:如果事件設定了內部標記,就返回True
- set:把內部標記設定為True。它可以喚醒等待被設定標記的所有線程,調用wait()方法的線程將不再被阻塞
- clear:重設內部標記。調用wait方法的線程,在調用set()方法之前都將被阻塞
- wait:在事件的內部標記被設定好之前,使用這個方法會一直阻塞線程調用,這個方法支援一個選擇性參數,作為等待時限(timeout)。如果等待時限非0,則線程會在時限內被一直阻塞
讓我們用線程事件對象來示範一個簡單的線程通訊樣本,它們可以輪流列印字串。兩個線程共用同一個事件對象。在while迴圈中,每次迴圈時,一個線程設定標記,另一個線程重設標記。
import threadingimport timeclass ThreadA(threading.Thread): def __init__(self, event): threading.Thread.__init__(self) self.event = event def run(self): count = 0 while count < 6: time.sleep(1) if self.event.is_set(): print("A") self.event.clear() count += 1class ThreadB(threading.Thread): def __init__(self, event): threading.Thread.__init__(self) self.event = event def run(self): count = 0 while count < 6: time.sleep(1) if not self.event.is_set(): print("B") self.event.set() count += 1event = threading.Event()ta = ThreadA(event)tb = ThreadB(event)ta.start()tb.start()
運行結果:
BABABABABAB
下面總結一下Python多線程的使用時機:
使用多線程:
- 頻繁的IO操作
- 並行任務可以通過並發解決
- GUI開發
不使用多線程:
多進程
由於GIL的存在,Python的多線程並沒有實現真正的並行。因此,一些問題使用threading模組並不能解決
不過Python為並行提供了一個替代方法:多進程。在多進程裡,線程被換成一個個子進程。每個進程都運作著各自的GIL(這樣Python就可以並行開啟多個進程,沒有數量限制)。需要明確的是,線程都是同一個進程的組成部分,它們共用同一塊記憶體、儲存空間和計算資源。而進程卻不會與它們的父進程共用記憶體,因此處理序間通訊比線程間通訊更為複雜
多進程相比多線程優缺點如下:
優點 |
缺點 |
可以使用多核作業系統 |
更多的記憶體消耗 |
進程使用獨立的記憶體空間,避免競態問題 |
進程間的資料共用變得更加困難 |
子進程容易中斷 |
處理序間通訊比線程困難 |
避開GIL限制 |
|
Python多線程與多進程(一)