淺談Python多線程

淺談Python多線程

做者簡介:html

姓名:黃志成(小黃)

博客: 博客python

線程

一.什麼是線程?

操做系統原理相關的書,基本都會提到一句很經典的話: "進程是資源分配的最小單位,線程則是CPU調度的最小單位"。web

線程是操做系統可以進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運做單位。一條線程指的是進程中一個單一順序的控制流,一個進程中能夠併發多個線程,每條線程並行執行不一樣的任務

好處 :安全

1.易於調度。

2.提升併發性。經過線程可方便有效地實現併發性。進程可建立多個線程來執行同一程序的不一樣部分。

3.開銷少。建立線程比建立進程要快,所需開銷不多。

4.利於充分發揮多處理器的功能。經過建立多線程進程,每一個線程在一個處理器上運行,從而實現應用程序的併發性,使每一個處理器都獲得充分運行。

在解釋python多線程的時候. 先和你們分享一下 python 的GIL 機制。多線程

二.GIL(Global Interpreter Lock)全局解釋器鎖

Python代碼的執行由Python 虛擬機(也叫解釋器主循環,CPython版本)來控制,Python 在設計之初就考慮到要在解釋器的主循環中,同時只有一個線程在執行,即在任意時刻,只有一個線程在解釋器中運行。對Python 虛擬機的訪問由全局解釋器鎖(GIL)來控制,正是這個鎖能保證同一時刻只有一個線程在運行。併發

在多線程環境中,Python 虛擬機按如下方式執行:app

  1. 設置GIL
  2. 切換到一個線程去運行
  3. 運行:
    a. 指定數量的字節碼指令,或者
    b. 線程主動讓出控制(能夠調用time.sleep(0))
  4. 把線程設置爲睡眠狀態
  5. 解鎖GIL
  6. 再次重複以上全部步驟

首先須要明確的一點是GIL並非Python的特性,它是在實現Python解析器(CPython)時所引入的一個概念。Python一樣一段代碼能夠經過CPython,PyPy,Psyco等不一樣的Python執行環境來執行。像其中的JPython就沒有GIL。然而由於CPython是大部分環境下默認的Python執行環境。因此在不少人的概念裏CPython就是Python,也就想固然的把GIL歸結爲Python語言的缺陷。因此這裏要先明確一點:GIL並非Python的特性,Python徹底能夠不依賴於GIL函數

還有,就是在作I/O操做時,GIL老是會被釋放。對全部面向I/O 的(會調用內建的操做系統C 代碼的)程序來講,GIL 會在這個I/O 調用以前被釋放,以容許其它的線程在這個線程等待I/O 的時候運行。若是是純計算的程序,沒有 I/O 操做,解釋器會每隔 100 次操做就釋放這把鎖,讓別的線程有機會執行(這個次數能夠經過 sys.setcheckinterval 來調整)若是某線程並未使用不少I/O 操做,它會在本身的時間片內一直佔用處理器(和GIL)。也就是說,I/O 密集型的Python 程序比計算密集型的程序更能充分利用多線程環境的好處。高併發

三.線程的生命週期

image

各個狀態說明:ui

  • New新建 :新建立的線程通過初始化後,進入Runnable狀態。
  • Runnable就緒:等待線程調度。調度後進入運行狀態。
  • Running運行:線程正常運行
  • Blocked阻塞:暫停運行,解除阻塞後進入Runnable狀態從新等待調度。
  • Dead消亡:線程方法執行完畢返回或者異常終止。

可能有3種狀況從Running進入Blocked:

  • 同步:線程中獲取同步鎖,可是資源已經被其餘線程鎖定時,進入Locked狀態,直到該資源可獲取(獲取的順序由Lock隊列控制)
  • 睡眠:線程運行sleep()或join()方法後,線程進入Sleeping狀態。區別在於sleep等待固定的時間,而join是等待子線程執行完。sleep()確保先運行其餘線程中的方法。固然join也能夠指定一個「超時時間」。從語義上來講,若是兩個線程a,b, 在a中調用b.join(),至關於合併(join)成一個線程。將會使主調線程(即a)堵塞(暫停運行, 不佔用CPU資源), 直到被調用線程運行結束或超時, 參數timeout是一個數值類型,表示超時時間,若是未提供該參數,那麼主調線程將一直堵塞到被調線程結束。最多見的狀況是在主線程中join全部的子線程。
  • 等待:線程中執行wait()方法後,線程進入Waiting狀態,等待其餘線程的通知(notify)。wait方法釋放內部所佔用的瑣,同時線程被掛起,直至接收到通知被喚醒或超時(若是提供了timeout參數的話)。當線程被喚醒並從新佔有瑣的時候,程序纔會繼續執行下去。

threading.Lock()不容許同一線程屢次acquire(), 而RLock容許, 即屢次出現acquire和release

四.Python threading模塊

上面介紹了這麼多理論.下面咱們用python提供的threading模塊來實現一個多線程的程序

threading 提供了兩種調用方式:

  • 直接調用
import threading

def func(n): # 定義每一個線程要運行的函數
    while n > 0:
        print("當前線程數:", threading.activeCount())
        n -= 1
        
for x in range(5):
    t = threading.Thread(target=func, args=(2,))  # 生成一個線程實例,生成實例後 並不會啓動,須要使用start命令
    t.start() #啓動線程
  • 繼承式調用
class MyThread(threading.Thread): # 繼承threading的Thread類
    def __init__(self, num):
        threading.Thread.__init__(self) # 必須執行父類的構造方法
        self.num = num # 傳入參數 num

    def run(self):  # 定義每一個線程要運行的函數
        while self.num > 0:
            print("當前線程數:", threading.activeCount())
            self.num -= 1

for x in range(5):
    t = MyThread(2) # 生成實例,傳入參數
    t.start() #啓動線程

兩種方式均可以調用咱們的多線程方法。

五.子線程阻塞

運行下面的代碼,看看結果.

import threading
def func(n):
    while n > 0:
        print("當前線程數:", threading.activeCount())
        n -= 1
for x in range(5):
    t = threading.Thread(target=func, args=(2,))
    t.start()

print("主線程:", threading.current_thread().name)

運行結果:

當前線程數: 2
當前線程數: 2
當前線程數: 2
當前線程數: 2
當前線程數: 2
當前線程數: 3
當前線程數: 3
當前線程數: 3
主線程: MainThread
當前線程數: 3
當前線程數: 3

那咱們如何阻塞子線程讓他們運行完,在繼續後面的操做呢.這個時候join()方法就派上用途了. 咱們改寫代碼:

import threading

def func(n):
    while n > 0:
        print("當前線程數:", threading.activeCount())
        n -= 1

threads = [] #運行的線程列表
for x in range(5):
    t = threading.Thread(target=func, args=(2,))
    threads.append(t) # 將子線程追加到列表
    t.start()

for t in threads:
    t.join()

print("主線程:", threading.current_thread().name)

join的原理就是依次檢驗線程池中的線程是否結束,沒有結束就阻塞直到線程結束,若是結束則跳轉執行下一個線程的join函數。

先看看這個:

  1. 阻塞主進程,專一於執行多線程中的程序。
  2. 多線程多join的狀況下,依次執行各線程的join方法,前頭一個結束了才能執行後面一個。
  3. 無參數,則等待到該線程結束,纔開始執行下一個線程的join。
  4. 參數timeout爲線程的阻塞時間,如 timeout=2 就是罩着這個線程2s 之後,就無論他了,繼續執行下面的代碼。

六.線程鎖(互斥鎖)

一個進程能夠開啓多個線程,那麼多麼多個進程操做相同數據,勢必會出現衝突.那如何避免這種問題呢?

import threading,time

num = 10 #共享變量

def func():
    global num
    lock.acquire() # 加鎖
    num = num - 1
    lock.release() # 解鎖
    print(num)

threads = []
lock = threading.Lock() #生成全局鎖
for x in range(10):
    t = threading.Thread(target=func)
    threads.append(t)
    t.start()

for t in threads:
    t.join()

經過 threading.Lock() 咱們能夠申請一個鎖。而後 acquire 方法進入臨界區.操做完共享數據 使用 release 方法退出.

臨界區的概念: 百度百科

在這裏補充一下:Python的Queue模塊是線程安全的.能夠不對它加鎖操做.

聰明的同窗 會發現一個問題? 我們不是有 GIL 嗎 爲何還要加鎖?

這個問題問的好!咱們下一節,將對這個問題進行探討.

七.LOCK 和 GIL

GIL的鎖是對於一個解釋器,只能有一個thread在執行bytecode。因此每時每刻只有一條bytecode在被執行一個thread。GIL保證了bytecode 這層面上是線程是安全的.

可是若是你有個操做一個共享 x += 1,這個操做須要多個bytecodes操做,在執行這個操做的多條bytecodes期間的時候可能中途就換thread了,這樣就出現了線程不安全的狀況了。

總結:同一時刻CPU上只有單個執行流不表明線程安全。

八.信號量

互斥鎖 同時只容許一個線程更改數據,而Semaphore是同時容許必定數量的線程更改數據 ,好比廁全部3個坑,那最多隻容許3我的上廁所,後面的人只能等裏面有人出來了才能再進去。

import threading,time

num = 10

def func():
    global num
    lock.acquire()
    time.sleep(2)
    num = num - 1
    lock.release()
    print(num)

threads = []
lock = threading.BoundedSemaphore(5) #最多容許5個線程同時運行
for x in range(10):
    t = threading.Thread(target=func)
    threads.append(t)
    t.start()

for t in threads:
    t.join()

print("主線程:", threading.current_thread().name)

運行一下上面的代碼.你會很明顯的發現 每次只執行五個線程。

參考文獻

淺談多進程多線程的選擇: 文章連接

python-多線程(原理篇): 文章連接

Python有GIL爲何還須要線程同步?: 文章連接

相關文章
相關標籤/搜索