一篇文章搞懂Python多線程簡單實現和GIL

公衆號: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.)

個人見解,望指教

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章