公衆号:pythonislover
今天開始打算開一個新系列,就是python的多線程和多程序實作,這部分可能有些新手還是比較模糊的,都知道python中的多線程是假的,但是又不知道怎麼回事,首先我們看一個例子來看看python多線程的實作。
import threading
import time
def say(name):
print('你好%s at %s' %(name,time.ctime()))
time.sleep(2)
print("結束%s at %s" %(name,time.ctime()))
def listen(name):
print('你好%s at %s' % (name,time.ctime()))
time.sleep(4)
print("結束%s at %s" % (name,time.ctime()))
if __name__ == '__main__':
t1 = threading.Thread(target=say,args=('tony',)) #Thread是一個類,執行個體化産生t1對象,這裡就是建立了一個線程對象t1
t1.start() #線程執行
t2 = threading.Thread(target=listen, args=('simon',)) #這裡就是建立了一個線程對象t2
t2.start()
print("程式結束=====================")
結果:
你好tony at Thu Apr 25 16:46:22 2019
你好simon at Thu Apr 25 16:46:22 2019
程式結束=====================
結束tony at Thu Apr 25 16:46:24 2019
結束simon at Thu Apr 25 16:46:26 2019
Process finished with exit code 0
python的多線程是通過threading子產品的Thread類來實作的。
建立線程對象
t1 = threading.Thread(target=say,args=('tony',)) #Thread是一個類,執行個體化産生t1對象,這裡就是建立了一個線程對象t1
啟動線程
t1.start() #線程執行
下面我們分析下上面代碼的結果:
你好tony at Thu Apr 25 16:46:22 2019 --t1線程執行
你好simon at Thu Apr 25 16:46:22 2019 --t2線程執行
程式結束===================== --主線程執行
結束tony at Thu Apr 25 16:46:24 2019 --sleep之後,t1線程執行
結束simon at Thu Apr 25 16:46:26 2019 --sleep之後,t2線程執行
Process finished with exit code 0 --主線程結束
我們可以看到主線程的print并不是等t1,t2線程都執行完畢之後才列印的,這是因為主線程和t1,t2 線程是同時跑的。但是主程序要等非守護子線程結束之後,主線程才會退出。
上面其實就是python多線程的最簡單用法,但是可能有人會和我有一樣的需求,一般開發中,我們需要主線程的print列印是在最後面的,表明所有流程都結束了,也就是主線程結束了。這裡就引入了一個join的概念。
import threading
import time
def say(name):
print('你好%s at %s' %(name,time.ctime()))
time.sleep(2)
print("結束%s at %s" %(name,time.ctime()))
def listen(name):
print('你好%s at %s' % (name,time.ctime()))
time.sleep(4)
print("結束%s at %s" % (name,time.ctime()))
if __name__ == '__main__':
t1 = threading.Thread(target=say,args=('tony',)) #Thread是一個類,執行個體化産生t1對象,這裡就是建立了一個線程對象t1
t1.start() #線程執行
t2 = threading.Thread(target=listen, args=('simon',)) #這裡就是建立了一個線程對象t2
t2.start()
t1.join() #join等t1子線程結束,主線程列印并且結束
t2.join() #join等t2子線程結束,主線程列印并且結束
print("程式結束=====================")
結果:
你好tony at Thu Apr 25 16:57:32 2019
你好simon at Thu Apr 25 16:57:32 2019
結束tony at Thu Apr 25 16:57:34 2019
結束simon at Thu Apr 25 16:57:36 2019
程式結束=====================
上面代碼中加入join方法後實作了,我們上面所想要的結果,主線程print最後執行,并且主線程退出,注意主線程執行了列印操作和主線程結束不是一個概念,如果子線程不加join,則主線程也會執行列印,但是主線程不會結束,還是需要待非守護子線程結束之後,主線程才結束。
上面的情況,主程序都需要等待非守護子線程結束之後,主線程才結束。那我們是不是注意到一點,我說的是“非守護子線程”,那什麼是非守護子線程?預設的子線程都是主線程的非守護子線程,但是有時候我們有需求,當主程序結束,不管子線程有沒有結束,子線程都要跟随主線程一起退出,這時候我們引入一個“守護線程”的概念。
如果某個子線程設定為守護線程,主線程其實就不用管這個子線程了,當所有其他非守護線程結束,主線程就會退出,而守護線程将和主線程一起退出,守護主線程,這就是守護線程的意思
看看具體代碼,我們這裡分2種情況來讨論守護線程,加深大家的了解,
還有一點,這個方法一定要設定在start方法前面
1.設定t1線程為守護線程,看看執行結果
import threading
import time
def say(name):
print('你好%s at %s' %(name,time.ctime()))
time.sleep(2)
print("結束%s at %s" %(name,time.ctime()))
def listen(name):
print('你好%s at %s' % (name,time.ctime()))
time.sleep(4)
print("結束%s at %s" % (name,time.ctime()))
if __name__ == '__main__':
t1 = threading.Thread(target=say,args=('tony',)) #Thread是一個類,執行個體化産生t1對象,這裡就是建立了一個線程對象t1
t1.setDaemon(True)
t1.start() #線程執行
t2 = threading.Thread(target=listen, args=('simon',)) #這裡就是建立了一個線程對象t2
t2.start()
print("程式結束=====================")
結果:
你好tony at Thu Apr 25 17:11:41 2019
你好simon at Thu Apr 25 17:11:41 2019
程式結束=====================
結束tony at Thu Apr 25 17:11:43 2019
結束simon at Thu Apr 25 17:11:45 2019
注意執行順序,
這裡如果設定t1為Daemon,那麼主線程就不管t1的運作狀态,隻管等待t2結束, t2結束主線程就結束了
因為t2的時間4秒,t1的時間2秒,主線程在等待t2線程結束的過程中,t1線程自己結束了,是以結果是:
你好tony at Thu Apr 25 14:11:54 2019
你好simon at Thu Apr 25 14:11:54 2019程式結束===============
結束tony at Thu Apr 25 14:11:56 2019 (也會列印,因為主線程在等待t2線程結束的過程中, t1線程自己結束了)
結束simon at Thu Apr 25 14:11:58 2019
2.設定t2為守護線程
import threading
import time
def say(name):
print('你好%s at %s' %(name,time.ctime()))
time.sleep(2)
print("結束%s at %s" %(name,time.ctime()))
def listen(name):
print('你好%s at %s' % (name,time.ctime()))
time.sleep(4)
print("結束%s at %s" % (name,time.ctime()))
if __name__ == '__main__':
t1 = threading.Thread(target=say,args=('tony',)) #Thread是一個類,執行個體化産生t1對象,這裡就是建立了一個線程對象t1
t1.start() #線程執行
t2 = threading.Thread(target=listen, args=('simon',)) #這裡就是建立了一個線程對象t2
t2.setDaemon(True)
t2.start()
print("程式結束=====================")
結果:
你好tony at Thu Apr 25 17:15:36 2019
你好simon at Thu Apr 25 17:15:36 2019
程式結束=====================
結束tony at Thu Apr 25 17:15:38 2019
注意執行順序:
這裡如果設定t2為Daemon,那麼主線程就不管t2的運作狀态,隻管等待t1結束, t1結束主線程就結束了
因為t2的時間4秒,t1的時間2秒, 主線程在等待t1線程結束的過程中, t2線程自己結束不了,是以結果是:
你好tony at Thu Apr 25 14:14:23 2019
你好simon at Thu Apr 25 14:14:23 2019
程式結束 == == == == == == == == == == =
結束tony at Thu Apr 25 14:14:25 2019
結束simon at Thu Apr 25 14:11:58 2019 不會列印,因為主線程在等待t1線程結束的過程中, t2線程自己結束不了,t2的時間4秒,t1的時間2秒
不知道大家有沒有弄清楚上面python多線程的實作方式以及join,守護線程的用法。
主要方法:
join():在子線程完成運作之前,這個子線程的父線程将一直被阻塞。
setDaemon(True):
将線程聲明為守護線程,必須在start() 方法調用之前設定, 如果不設定為守護線程程式會被無限挂起。這個方法基本和join是相反的。
當我們在程式運作中,執行一個主線程,如果主線程又建立一個子線程,主線程和子線程 就分兵兩路,分别運作,那麼當主線程完成
想退出時,會檢驗子線程是否完成。如 果子線程未完成,則主線程會等待子線程完成後再退出。但是有時候我們需要的是 隻要主線程
完成了,不管子線程是否完成,都要和主線程一起退出,這時就可以 用setDaemon方法啦
其他方法:
run(): 線程被cpu排程後自動執行線程對象的run方法
start():啟動線程活動。
isAlive(): 傳回線程是否活動的。
getName(): 傳回線程名。
setName(): 設定線程名。
threading子產品提供的一些方法:
threading.currentThread(): 傳回目前的線程變量。
threading.enumerate(): 傳回一個包含正在運作的線程的list。正在運作指線程啟動後、結束前,不包括啟動前和終止後的線程。
threading.activeCount():傳回正在運作的線程數量,與len(threading.enumerate())有相同的結果。
上面的例子中我們注意到兩如果個任務如果順序執行要6s結束,如果是多線程執行4S結束,性能是有所提升的,但是我們要知道這裡的性能提升實際上是由于cpu并發實作性能提升,也就是cpu線程切換(多道技術)帶來的,而并不是真正的多cpu并行執行。
上面提到了并行和并發,那這兩者有什麼差別呢?
并發:是指一個系統具有處理多個任務的能力(cpu切換,多道技術)
并行:是指一個系統具有同時處理多個任務的能力(cpu同時處理多個任務)
并行是并發的一種情況,子集
那為什麼python在多線程中為什麼不能實作真正的并行操作呢?就是在多cpu中執行不同的線程(我們知道JAVA中多個線程可以在不同的cpu中,實作并行運作)這就要提到python中大名鼎鼎GIL,那什麼是GIL?
GIL:全局解釋器鎖 無論你啟多少個線程,你有多少個cpu, Python在執行的時候隻會的在同一時刻隻允許一個線程(線程之間有競争)拿到GIL在一個cpu上運作,當線程遇到IO等待或到達者輪詢時間的時候,cpu會做切換,把cpu的時間片讓給其他線程執行,cpu切換需要消耗時間和資源,是以計算密集型的功能(比如加減乘除)不适合多線程,因為cpu線程切換太多,IO密集型比較适合多線程。
任務:
IO密集型(各個線程都會都各種的等待,如果有等待,線程切換是比較适合的),也可以采用可以用多程序+協程
計算密集型(線程在計算時沒有等待,這時候去切換,就是無用的切換),python不太适合開發這類功能
我們前面舉得例子裡面模拟了sleep操作,其實就是相當于遇到IO,這種場景用多線程是可以增加性能的,但是如果我們用多線程來計算資料的計算,性能反而會降低。
下面是GIL的一段原生解釋:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once.
This lock is necessary mainly because CPython’s memory management is not thread-safe.
(However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
個人見解,望指教