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

今天開始打算開一個新系列,就是python的多線程和多進程實現,這部分可能有些新手仍是比較模糊的,都知道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() #線程執行bash

下面咱們分析下上面代碼的結果:
你好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,則主線程也會執行打印,可是主線程不會結束,仍是須要待非守護子線程結束以後,主線程才結束。性能

上面的狀況,主進程都須要等待非守護子線程結束以後,主線程才結束。那咱們是否是注意到一點,我說的是「非守護子線程」,那什麼是非守護子線程?默認的子線程都是主線程的非守護子線程,可是有時候咱們有需求,當主進程結束,無論子線程有沒有結束,子線程都要跟隨主線程一塊兒退出,這時候咱們引入一個「守護線程」的概念。ui

若是某個子線程設置爲守護線程,主線程其實就不用管這個子線程了,當全部其餘非守護線程結束,主線程就會退出,而守護線程將和主線程一塊兒退出,守護主線程,這就是守護線程的意思spa

看看具體代碼,咱們這裏分2種狀況來討論守護線程,加深你們的理解, 還有一點,這個方法必定要設置在start方法前面線程

1.設置t1線程爲守護線程,看看執行結果code

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.)
複製代碼

我的看法,望指教

相關文章
相關標籤/搜索