多線程python
多線程是程序在一樣的上下文中同時運行多條線程的能力。這些線程共享同一個進程的資源,能夠在併發模式(單核處理器)或並行模式(多核處理器)下執行多個任務安全
多線程有如下幾個優勢:bash
可是多線程也有如下幾個缺點:服務器
一般,多線程技術徹底能夠在多處理器上實現並行計算。可是Python的官方版本(CPython)有一個GIL限制,GIL會阻止多個線程同時運行Python的字節碼,這就不是真正的並行了。假如你的系統有6個處理器,多線程能夠把CPU跑到網絡
600%,然而,你能看到的只有100%,甚至更慢一點,這都是GIL形成的多線程
CPython的GIL是有必要的,由於CPython的內存管理不是線程安全的。所以,爲了讓每一個任務都按順序進行,它須要確保運行過程當中內存不被幹擾。它能夠更快的運行單線程程序,簡化C語言擴展庫的使用方法,由於它不須要考慮多線程問題。併發
可是,GIL是能夠用一些辦法繞過的。例如,因爲GIL只阻止多個線程同時運行Python的字節碼,因此能夠用C語言寫程序,而後用Python封裝。這樣,在程序運行過程當中GIL就不會干擾多線程併發了app
另外一個GIL不影響性能的示例就是網絡服務器了,服務器大部分時間都在讀數據包,而當發生IO等待時,會嘗試釋放GIL。這種狀況下,增長線程能夠讀取更多的包,雖然這並非真正的並行。這樣作能夠增長服務器的性能,可是不會影響速度。ide
用_thread模塊建立線程函數
咱們先用一個例子快速演示_thread模塊的用法:_thread模塊提供了start_new_thread方法。咱們能夠向裏面傳入如下參數:
import _thread import time def 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 2018 thread-B:Sun Jul 8 07:39:28 2018 thread-A:Sun Jul 8 07:39:28 2018 thread-A:Sun Jul 8 07:39:29 2018 thread-B:Sun Jul 8 07:39:30 2018 thread-A:Sun Jul 8 07:39:30 2018 thread-A:Sun Jul 8 07:39:31 2018 thread-B:Sun Jul 8 07:39:32 2018 thread-B:Sun Jul 8 07:39:34 2018 thread-B:Sun Jul 8 07:39:36 2018
上面的例子很簡單,線程A和線程B是併發執行的。
_thread模塊還提供了一些容易使用的線程原生接口:
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 2018 thread-B:Sun Jul 8 09:12:58 2018 thread-A:Sun Jul 8 09:12:58 2018 thread-A:Sun Jul 8 09:12:59 2018 thread-B:Sun Jul 8 09:13:00 2018 Traceback (most recent call last): File "D:/pypath/hello/test3/test01.py", line 22, in <module> pass KeyboardInterrupt
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 2018 thread-B:Sun Jul 8 09:15:52 2018 thread-A:Sun Jul 8 09:15:52 2018 thread-A:Sun Jul 8 09:15:53 2018 thread-A:Sun Jul 8 09:15:54 2018 thread-A:Sun Jul 8 09:15:55 2018
allocate_lock方法能夠爲線程返回一個線程鎖,這個鎖能夠保護某一代碼塊從開始運行到運行結束只有一個線程,線程鎖對象有三個方法:
下面這段代碼用10個線程對一個全局變量增長值,所以,理想狀況下,全局變量的值應該是10:
import _thread import time global_values = 0 def run(thread_name): global global_values local_copy = global_values print("%s with value %s" % (thread_name, local_copy)) global_values = local_copy + 1 for 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 0 thread-(1) with value 0 thread-(2) with value 0 thread-(4) with value 0 thread-(6) with value 0 thread-(8) with value 0 thread-(7) with value 0 thread-(5) with value 0 thread-(3) with value 0 thread-(9) with value 1 global_values:2
可是很遺憾,咱們沒有獲得咱們但願的結果,相反,程序運行的結果和咱們但願的結果差距更遠。形成這樣的緣由,都是由於多個線程操做同一變量或同一代碼塊致使有的線程不能讀到最新的值,甚至是把舊值的運算結果賦給所有局變量
如今,讓咱們修改一下原先的代碼:
import _thread import time global_values = 0 def 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 0 thread-(2) with value 1 thread-(4) with value 2 thread-(5) with value 3 thread-(3) with value 4 thread-(6) with value 5 thread-(1) with value 6 thread-(7) with value 7 thread-(8) with value 8 thread-(9) with value 9
如今能夠看到,線程的執行順序依舊是亂序的,但全局變量的值是逐個遞增的
_thread還有其餘一些方法:
用threading模塊建立線程
這是目前Python中處理線程廣泛推薦的模塊,這個模塊提供了更完善和高級的接口,咱們嘗試將前面的示例轉化成threading模塊的形式:
import threading import time global_values = 0 def 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)
對於更復雜的狀況,若是要更好地封裝線程的行爲,咱們可能須要建立本身的線程類,這裏須要注意幾點:
import threading import time class 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")
運行結果:
finish Thread:Thread-2 - 0 Thread:Thread-1 - 0 Thread:Thread-2 - 1 Thread:Thread-1 - 1 Thread: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 urlopen sites = [ "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/ 200 http://www.sina.com.cn/ 200 http://www.qq.com/ 200 real 0m1.669s user 0m0.143s sys 0m0.026s
如今,咱們嘗試着把IO操做函數轉變爲一個線程來優化代碼:
from urllib.request import urlopen import threading sites = [ "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/ 200 http://www.sina.com.cn/ 200 http://www.qq.com/ 200 real 0m0.237s user 0m0.110s sys 0m0.019s
顯然,線程版的程序更快,運行速度幾乎是上一版的8倍,性能改善十分顯著
經過Event對象實現線程間通訊
雖然線程一般是做爲獨立運行或並行的任務,可是有時也會出現線程間通訊的需求,threading模塊提供了事件(event)對象實現線程間通訊,它包含一個內部標記,以及可使用set()和clear()方法的調用線程
Event類的接口很簡單,它支持的方法以下:
讓咱們用線程事件對象來演示一個簡單的線程通訊示例,它們能夠輪流打印字符串。兩個線程共享同一個事件對象。在while循環中,每次循環時,一個線程設置標記,另外一個線程重置標記。
import threading import time class 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 += 1 class 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 += 1 event = threading.Event() ta = ThreadA(event) tb = ThreadB(event) ta.start() tb.start()
運行結果:
B A B A B A B A B A B
下面總結一下Python多線程的使用時機:
使用多線程:
不使用多線程:
多進程
因爲GIL的存在,Python的多線程並無實現真正的並行。所以,一些問題使用threading模塊並不能解決
不過Python爲並行提供了一個替代方法:多進程。在多進程裏,線程被換成一個個子進程。每一個進程都運做着各自的GIL(這樣Python就能夠並行開啓多個進程,沒有數量限制)。須要明確的是,線程都是同一個進程的組成部分,它們共享同一塊內存、存儲空間和計算資源。而進程卻不會與它們的父進程共享內存,所以進程間通訊比線程間通訊更爲複雜
多進程相比多線程優缺點以下:
優勢 | 缺點 |
可使用多核操做系統 | 更多的內存消耗 |
進程使用獨立的內存空間,避免競態問題 | 進程間的數據共享變得更加困難 |
子進程容易中斷 | 進程間通訊比線程困難 |
避開GIL限制 |