Python-全局解釋器鎖GIL原理和多線程產生緣由與原理-多線程通訊機制

GILpython

  全局解釋器鎖,這個鎖是個粗粒度的鎖,解釋器層面上的鎖,爲了保證線程安全,同一時刻只容許一個線程執行,但這個鎖並不能保存線程安全,由於GIL會釋放掉的而且切換到另一個線程上,不會徹底佔用,依據分配策略(時間片、執行字節碼行數、IO操做)。GIL只能保證同一時刻同一CPU上只有一個線程執行,但不能保證線程切換的時候能把一行代碼翻譯成的bytecode執行完,這就會出現問題,因此說只是必定程度上的保證線程安全。GIL 使得同一個時刻只有一個線程在一個CPU上執行字節碼,沒法將多個線程映射到多個CPU上,這也是爲何這麼說Python多線程雞肋,也就是說根本意義上來講Python是個單線程的,這個GIL只在CPython中才有,其餘版本中沒有。程序員

  但單線程有個很是大的優點就是在處理IO密集型的任務時候顯得不同凡響,Python中分線程和協程兩種方式,線程是由系統控制調用,協程是由程序員控制,也就是說在把控度角度上來看協程比線程更加具備優點。其一協程比線程更加輕量,其二協程切換徹底由程序員控制而不是操做系統控制,其三協程切換消耗遠比線程小。數據庫

 

北門吹雪:http://www.cnblogs.com/2bjiujiu/編程

 

Python 代碼執行原理緩存

  # Python代碼 --> 解釋器 --> bytecode --> 一行一行執行 bytecode安全

  

 

函數執行過程原理:網絡

import dis


def add(a):
    a += 1


def subtract(a):
    a -= 1


if __name__ == '__main__':
    # add 函數執行過程
    dis.dis(add)
    print()
    # subtract 函數執行過程
    dis.dis(subtract)

  結果:數據結構

  5           0 LOAD_FAST                0 (a)
              2 LOAD_CONST               1 (1)
              4 INPLACE_ADD
              6 STORE_FAST               0 (a)
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE

  9           0 LOAD_FAST                0 (a)
              2 LOAD_CONST               1 (1)
              4 INPLACE_SUBTRACT
              6 STORE_FAST               0 (a)
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE

  簡化一下執行結果過程:多線程

  def add(a):
    a += 1

  add
    1. load a
    2. load 1
    3. +
    4. 賦值給a a = a +1

  def desc(a):
    a -= 1

  desc
    1. load a
    2. load 1
    3. -
    4. 賦值給a a = a - 1架構

  以上過程 GIL會在其中某個過程釋放掉給其餘線程,問題出如今4賦值階段,多個線程切換,最終a的值處於各自線程最終結果,假如a=1,則add線程賦值 a = 2 然而 desc線程賦值 a = 0,那麼問題出現了,爲了解決這個問題,引入鎖的概念,線程同步機制,鎖住出現問題的代碼段,鎖住代碼塊之間串行。得到鎖和釋放鎖帶來性能問題,而且帶來死鎖問題。

 

beimenchuixue:http://www.cnblogs.com/2bjiujiu/ 

 


  1. 普通鎖 threading.Lock
  2. 重入鎖(遞歸鎖) threading.Rlodk
  # 重入鎖使用條件,必須在同一線程內,連續調用屢次acquire,必定要注意acquire和 release的次數相等

  # 重入鎖通常用於一個線程中嵌套一層邏輯,裏層邏輯使用了鎖機制

 

如何產生死鎖:
  1. 獲取鎖並無釋放鎖同時又獲取鎖
  2. 線程A鎖住線程B但願得到鎖的資源沒有釋放,線程B鎖住線程A的但願獲取的資源並無是否掉,也就是說兩個線程各自鎖住對方想要獲取鎖的資源沒有釋放掉。
  3. 一個線程函數中,得到鎖以後,再次調用另一個函數,另一個函數也得到鎖, 經過 Rlodk鎖解決這個問題

  # 死鎖本質緣由用土話說:吃着碗裏的看着鍋裏的或互相鎖住對象想要的

 

線程:

  1. 操做系統調度單的最小單位,對於IO操做多進程和多線程差異不大

  2. 線程出現的緣由是進程粒度太大,但願更小的單元使用CPU資源,CPU上執行的是線程,CPU執行線程代碼或指令集

  3. 線程是進程的一部分,進程至少有一個主線程

  4. 線程自己不能分配和擁有資源,只能訪問和使用進程資源,所以切換線程比進程切換更加快

  5. 多線程能夠利用多核和多個CPU性能資源優點,但在Python中不行,因爲GIL鎖的存在

  6. 多線程也是異步編程方式,大多數狀況下主線程不知道子線程是否執行往,主線程也不依賴子線程的執行結果

 北門吹雪:http://www.cnblogs.com/2bjiujiu/

CPU密集型

  1. 處理數據(計算)

 

IO密集型

  # 等待數據

  1. 查詢數據庫

  2. 請求網絡資源

  3. 讀寫文件

# 依據程序花費的時間是在CPU上仍是在等待數據上判斷這個程序是CPU密集型仍是IO密集型

 

IO操做主要分兩類: 網絡IO 和 磁盤IO

 

 

多線程模塊      threading

threading使用過程

  1. 實例線程

  2. 啓動線程

  3. 等待或者不等待子線程執行

這裏使用socket server線程實現方式實例:

import socket
from threading import Thread


class BeiMenChuiXueHandle(Thread):
    def __init__(self, conn, remote_address):
        super().__init__()
        self.conn = conn
        self.remote_address = remote_address

    def run(self):
        while True:
            try:
                data = self.conn.recv(1024).decode("utf-8")
            except ConnectionResetError as e:
                print("北門吹雪")
                print("%s:%s 斷開鏈接" % self.remote_address)
                break
            self.conn.send(bytes(data.upper(), encoding='utf-8'))
        self.conn.close()


class BeiMenChuiXueHandleSocket:
    def __init__(self):
        self.listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    def _handle(self, conn, remote_address):
        # 1實例線程
        handler = BeiMenChuiXueHandle(conn, remote_address)
        # 啓動線程
        handler.start()

    def socket_run(self, ip, port):
        self.listener.bind((ip, port))
        self.listener.listen()
        while True:
            conn, remote_address = self.listener.accept()
            print("北門吹雪")
            print("%s:%s 創建鏈接" % remote_address)
            # 3. 線程去處理鏈接,由於服務端是一直運行,也就是說是否等待進程完成已經不須要
            self._handle(conn, remote_address)


if __name__ == '__main__':
    listener = BeiMenChuiXueHandleSocket()
    listener.socket_run(ip='0.0.0.0', port=8000)

  對線程相關操做,這些操做都是在線程實例上的方法

    1. 設置爲守護線程        .setDaemon(True)

      # 主線程不等待子線程完成,其實不加上jion都是守護線程,加上語義更加明顯而已

    2. 堵塞等待線程完成       .join

      # 這個是寫在主線程中,在這一行若是線程沒有執行完則產生堵塞等待線程執行完,線程執行完則不堵塞執行後面的代碼

    3. 設置線程名字         .setname

      # 其實在實例化的時候能夠設置線程名字,有一個name參數

    4. 獲取線程名字          .getname  

import socket
from threading import Thread


def beimenchuixue_handler(conn, remote_address):
    """線程處理處理每一個鏈接"""
    # 處理客戶端端口異常
    while True:
        try:
            data = conn.recv(1024).decode("utf-8")
        except ConnectionResetError as e:
            print("%s:%s 斷開鏈接" % remote_address)
            break
        conn.send(bytes(data.upper(), encoding='utf-8'))
    conn.close()


def beimenchuixue_socket():
    listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listener.bind(('0.0.0.0', 8000))
    listener.listen()
    while True:
        conn, remote_address = listener.accept()
        print("%s:%s 創建鏈接" % remote_address)
        # 經過name設置線程名
        handler = Thread(target=beimenchuixue_handler, args=(conn, remote_address), name="北門吹雪")
        # 設置爲守護線程,必須在啓動前設置
        handler.setDaemon(True)
        handler.start()
        # 得到線程名
        print(handler.getName())
        # 線程是否激活
        print(handler.is_alive())
        # 判斷是否守護線程
        print(handler.isDaemon())
        # 和 is_active()同樣
        print(handler.isAlive())


if __name__ == '__main__':
    beimenchuixue_socket()

   5. 得到進程中主線程編號      threading.currennt_thread()

      # 這個能夠放到任何地方,結果是同樣的

 北_門_吹_雪:http://www.cnblogs.com/2bjiujiu/

線程通訊

  1. 共享變量

    在線程函數上一層做用域定義一個變量,這線程函數都能方法到並能作修改,或者經過把公用變量傳遞到線程函數中)線程安全性(相對操做的變量來講,須要加鎖讓線程依次操做變量),雖然有時沒有鎖的狀況下結果依然正確,由於GIL這把大鎖的存在

from threading import Thread, Lock
from itertools import chain

# 共享變量,本質上是上一級變量,要修改則經過global和 nonlocal申明不是當前局變量
number = 1
lock = Lock()


def add():
    global number
    # 獲取鎖
    lock.acquire()
    number += 1
    # 釋放鎖
    lock.release()


def subtract():
    global number
    lock.acquire()
    number -= 1
    lock.release()


if __name__ == '__main__':

    go_add_threads = {Thread(target=add, name='add_%s' % i) for i in range(100)}
    go_subtract_threads = {Thread(target=subtract, name="add_thread_%s" % i) for i in range(100)}
    # 啓動並等待線程
    for add_thread in chain(go_add_threads, go_subtract_threads):
        add_thread.start()
    for add_thread in chain(go_add_threads, go_subtract_threads):
        add_thread.join()
    # 得到共享變量值
    print(number)

  2. 經過Queue通訊,Queue是線程安全,底層是使用deque數據結構實現,相似Go語言中CSP線程之間通訊

   # 下面這個實例模仿包子生產店子實現一個生產者與消費者模型實例,經過隨機模塊模擬等待顧客的過程,經過緩存隊列模擬生產包子滿了處於等待狀態

from threading import Thread
import time
from queue import Queue
from random import randint


def producer(baozi_queue):
    count = 1
    while True:
        print("-->包子出籠了,第%s包子" % count)
        baozi_queue.put(count)
        # 模擬作包子過程
        time.sleep(1)
        count += 1


def consumer(baozi_queue):
    while True:
        count = baozi_queue.get()
        # 模擬吃包子過程,不是每時每刻都會有顧客,模擬等待顧客過程
        time.sleep(randint(1, 10))
        print("<--吃完第 %s 包子" % count)


if __name__ == '__main__':
    # 緩存的隊列, 作完10個包子就再也不作了
    baozi_queue = Queue(maxsize=10)
    # 生產者
    baozi_worker = Thread(target=producer, args=(baozi_queue, ), name="北門吹雪包子店,百年品牌老店")
    # 消費者
    baozi_consumer = Thread(target=consumer, args=(baozi_queue,), name="吃包子")
    # 啓動這兩個線程
    baozi_worker.start()
    baozi_consumer.start()
    baozi_worker.join()
    baozi_consumer.join()

  Queue隊列其餘相關方法和參數

    get      有兩個參數,block是否堵塞,timeout堵塞超時時間,其中一個條件觸發則拋出異常

    empty     判斷隊列是否爲空

    full      判斷隊列是否滿

 北_門_吹_雪:http://www.cnblogs.com/2bjiujiu/

經驗:

  1. Python多線程因爲GIL存在本質上只是單線程,沒法利用多核CPU性能優點,啓動多進程某種意義上來講只是多啓動了一個線程,啓動新進程自己消耗遠比線程資源消耗大多了,啓動進程來折中解決Python線程問題不是好辦法

  2. 死鎖本質緣由是吃着碗裏的看着鍋裏的或者互相鎖住對方想要的資源

  3. 採用Go語言中CSP線程通訊模式解決線程安全問題,我以爲是當下最優的選擇

  4. 區分程序是CPU密集型仍是IO密集型,主要看程序把時間花費在CPU上仍是在等待數據上

  5. 事件驅動,本質上是發送請求取數據,在等待取數據的時候去執行其餘函數或過程,跳出時候註冊一個事件,數據取回觸發事件從新回到跳出的部分執行往下的代碼,架構是:事件循環+回調函數

  6. 事件驅動異步編程,雖然性能很優秀,但打破了傳統上從下到下順序執行的邏輯,把代碼執行的邏輯完全打亂,出錯排錯都是很是棘手問題,究竟是選擇異步編程仍是同步編程,須要充分考慮技術人員技術能力和業務需求,若是不能知足則換成Go語言等自己支持異步編程的高性能語言進行處理業務需求,而不是死磕Python實現,這樣死磕Python則會在維護與開發成本上花費更多也可能達不到預期效果

bei_men_chui_xxue:http://www.cnblogs.com/2bjiujiu/

相關文章
相關標籤/搜索