Python-線程(2)

GIL全局解釋器鎖

在Cpython解釋器中,同一個進程下開啓的多線程,同一時刻只能有一個線程執行,沒法利用多核優點編程

首先須要明確的一點是GIL並非Python的特性,它是在實現Python解析器(CPython)時所引入的一個概念。就比如C++是一套語言(語法)標準,可是能夠用不一樣的編譯器來編譯成可執行代碼。>有名的編譯器例如GCC,INTEL C++,Visual C++等。Python也同樣,一樣一段代碼能夠經過CPython,PyPy,Psyco等不一樣的Python執行環境來執行。像其中的JPython就沒有GIL。然而由於CPython是大部分環境下默認的Python執行環境。因此在不少人的概念裏CPython就是Python,也就想固然的把GIL歸結爲Python語言的缺陷。因此這裏要先明確一點:GIL並非Python的特性,Python徹底能夠不依賴於GIL安全

GIL 本質上就是一把互斥鎖,將併發運行編程串行,以此來控制同一時間內共享的數據只能被一個任務鎖修改,進而保證數據的安全多線程

能夠確定的是,保護數據的安全,就應該加鎖併發

'''
驗證全局解釋器鎖
'''
import time
from threading import Thread,current_thread

n = 100

def task():
    global n
    n2 = n
    # 全局解釋器鎖碰到 IO阻塞就切換cpu執行並解鎖
    time.sleep(1)
    n = n2 - 1
    print(n,current_thread().name)

for line in range(100):
    t = Thread(target=task)
    t.start()

GIL 與 Lock

爲何有了解釋器的鎖來保證同一時間只能有一個線程來執行,爲何還須要有線程的lock鎖?app

首先,咱們要達成共識,鎖的目的就是爲了保護數據,同一時間只能有一個線程來修改共享的數據性能

而後,咱們就能夠獲得結論:保護不一樣的數據咱們須要加不一樣的鎖測試

那麼 GIL 與Lock是兩把鎖,保護的數據不同,GIL是解釋器裏的,保護的是解釋器級別的數據,好比垃圾回收的數據,Lock是保護用戶本身開發的應用程序的數據,很明顯GIL本身不負責這件事,只能用戶自定義加鎖處理,即Lock大數據

當咱們在沒有IO的程序裏面(純計算)不加鎖不會存在鎖的錯亂問題,由於GIL限制了同一時刻只能讓一個線程執行,因此不用加鎖ui

當咱們在IO密集的程序裏面,不加鎖會致使數據安全問題,由於程序遇到IO cpu會切斷使用權,讓另外一個線程執行,那麼這時候另外一個線程拿到的數據仍是以前的一份,不是最新的,致使全部線程修改數據不是正確的,引發數據安全問題,因此這個時候加上線程鎖來保證這個數據安全

多進程 VS 多線程

站在兩個角度看問題

計算密集型:

​ 單核:

​ 開啓進程:消耗資源大

​ 開啓線程:消耗資源小於進程

​ 多核:

​ 開啓進程:並行執行,效率高

​ 開啓線程:併發執行,效率低

IO密集型:

​ 單核:

​ 開啓進程:消耗資源大

​ 開啓線程:消耗資源小於進程

​ 多核:

​ 開啓進程:並行執行,效率小於多線程,由於遇到IO會當即切換CPU的執行權限

​ 開啓線程:併發執行,效率高於多進程

# coding=utf-8

from multiprocessing import Process
from threading import Thread
import os
import time

# 計算密集型
def work1():
    num = 0
    for i in range(40000000):
        num += 1

# IO密集型
def work2():
    time.sleep(1)

if __name__ == '__main__':
    # 測試計算密集型
    start_time = time.time()
    ls = []
    for i in range(10):
        # 測試多進程
        p = Process(target=work1)       # 程序執行時間爲7.479427814483643
        # 測試多線程
        # p = Thread(target=work1)       # 程序執行時間爲28.56563377380371
        ls.append(p)
        p.start()
    for l in ls:
        l.join()

    end_time = time.time()
    print(f"程序執行時間爲{end_time - start_time}")

    # 測試計算密集型結論:
    # 在計算較小數據時候使用多線程效率高
    # 在計算較大數據時候使用多進程效率高

    # 測試IO密集型
    start_time = time.time()
    ls = []
    for i in range(10):
        # 測試多進程
        # p = Process(target=work2)       # 程序執行時間爲2.749157190322876
        # 測試多線程
        p = Thread(target=work2)  # 程序執行時間爲1.0130579471588135
        ls.append(p)
        p.start()
    for l in ls:
        l.join()

    end_time = time.time()
    print(f"程序執行時間爲{end_time - start_time}")

    # 測試IO密集型結論:
    # 使用多線程效率要比使用多進程效率高

計算密集型狀況下:

​ 在計算較小數據時候使用多線程效率高

​ 在計算較大數據時候使用多進程效率高

IO密集型狀況下:

​ 使用多線程效率要比使用多進程效率高

高效執行程序:

​ 使用多線程和多進程

這裏打個比方:

一個工人至關於cpu,工廠原材料就至關於線程。

此時計算至關於工人在幹活,I/O阻塞至關於爲工人幹活提供所需原材料的過程,工人幹活的過程當中若是沒有原材料了,則工人幹活的過程須要中止,直到等待原材料的到來。

若是你的工廠乾的大多數任務都要有準備原材料的過程(I/O密集型),那麼你有再多的工人,意義也不大,還不如一我的,在等材料的過程當中讓工人去幹別的活,

反過來說,若是你的工廠原材料都齊全,那固然是工人越多,效率越高

結論:

對計算機來講:CPU越多越好,可是對於IO來講,再多的CPU也沒用

對於程序來講:隨着CPU的增多執行效率確定會有所提升(無論提升多大,總會有所提升)

假設咱們有四個任務須要處理,處理方式確定是要玩出併發的效果,解決方案能夠是:

方案一:開啓四個進程

方案二:一個進程下,開啓四個線程

結果:

​ 單核:

​ 若四個任務是計算密集型,方案一增長了建立進程時間,方案二遠小於方案一,方案二勝

​ 若四個任務是I/O密集型,方案一也增長了建立進程時間,且進程切換速度還不如線程,因此方 案二又勝

​ 多核:

​ 若四個任務是計算密集型,多核 開多個進程一塊兒計算是並行計算,在線程中執行用不上多核, 那麼方案一效率高,方案一勝

​ 若四個任務是I/O密集型,再多的核也解決不了I/O問題,方案二勝

結論:

​ 如今的計算機基本上都是多核,python對於計算密集型的程序來講,開多線程的效率並不能帶來多 大性能的提高,甚至還不如串行,可是對於I/O密集型的程序來講,開多線程效率就明顯提高

多線程用於I/O密集型:例如Socket、爬蟲、Web

多進程用於計算密集型,如金融分析,數據分析

死鎖現象

# coding=utf-8

from threading import Lock,Thread,current_thread
import time

mutex_a = Lock()
mutex_b = Lock()

class MyThread(Thread):
    # 線程執行任務
    def run(self):
        self.work1()
        self.work2()

    def work1(self):
        mutex_a.acquire()
        print(f"{self.name} 搶到了鎖a")
        mutex_b.acquire()
        print(f"{self.name} 搶到了鎖b")
        mutex_b.release()
        print(f"{self.name} 釋放了鎖b")
        mutex_a.release()
        print(f"{self.name} 釋放了鎖a")

    def work2(self):
        mutex_b.acquire()
        print(f"{self.name} 搶到了鎖b")
        
        # 模擬IO操做
        time.sleep(1)
        mutex_a.acquire()
        print(f"{self.name} 搶到了鎖a")
        mutex_a.release()
        print(f"{self.name} 釋放了鎖a")
        mutex_b.release()
        print(f"{self.name} 釋放了鎖b")

for i in range(2):
    t = MyThread()
    t.start()
    
    
Thread-1 搶到了鎖a
Thread-1 搶到了鎖b
Thread-1 釋放了鎖b
Thread-1 釋放了鎖a
Thread-1 搶到了鎖b
Thread-2 搶到了鎖a
.....卡主了

開啓兩個線程以後,每一個線程會搶cpu執行,
第一個線程搶到了,執行work1,搶到鎖a、b,釋放a、b
再回來執行work2,搶到鎖b,碰到有IO阻塞,切換線程執行

到第二個線程,能夠搶到鎖a,可是鎖還在第一個線程手裏拿着
鎖b麼有被釋放,線程二拿不到鎖b,因而卡主了

遞歸鎖

解決死鎖問題,須要用到遞歸鎖

RLock:只有一把鑰匙,能夠提供多個線程去使用,每次使用會計數+1,只有計數爲0 的時候 才能真正釋放讓另外一個線程使用

能夠理解爲遇到IO操做以後,若是身上有這個遞歸鎖,必須先把這個遞歸鎖解開以後而後你再去作其餘事情。

# coding=utf-8


from threading import Lock,Thread,current_thread,RLock
import time

# mutex_a = Lock()
# mutex_b = Lock()
mutex_a = mutex_b = RLock()

class MyThread(Thread):
    # 線程執行任務
    def run(self):
        self.work1()
        self.work2()

    def work1(self):
        mutex_a.acquire()
        print(f"{self.name} 搶到了鎖a")
        mutex_b.acquire()
        print(f"{self.name} 搶到了鎖b")
        mutex_b.release()
        print(f"{self.name} 釋放了鎖b")
        mutex_a.release()
        print(f"{self.name} 釋放了鎖a")

    def work2(self):
        mutex_b.acquire()
        print(f"{self.name} 搶到了鎖b")
        # 模擬IO操做
        time.sleep(1)
        mutex_a.acquire()
        print(f"{self.name} 搶到了鎖a")
        mutex_a.release()
        print(f"{self.name} 釋放了鎖a")
        mutex_b.release()
        print(f"{self.name} 釋放了鎖b")

for i in range(2):
    t = MyThread()
    t.start()


Thread-1 搶到了鎖a
Thread-1 搶到了鎖b
Thread-1 釋放了鎖b
Thread-1 釋放了鎖a
Thread-1 搶到了鎖b

Thread-1 搶到了鎖a
Thread-1 釋放了鎖a
Thread-1 釋放了鎖b
Thread-2 搶到了鎖a
Thread-2 搶到了鎖b
Thread-2 釋放了鎖b
Thread-2 釋放了鎖a
Thread-2 搶到了鎖b

Thread-2 搶到了鎖a
Thread-2 釋放了鎖a
Thread-2 釋放了鎖b

信號量

Semaphore

互斥鎖:只有一把鎖,只能一我的去使用

信號量:能夠自定義鎖的數量,提供多我的使用

# coding=utf-8


from threading import Semaphore,Lock
from threading import current_thread
from threading import Thread
import time

# 信號量:提供5個鎖
sm = Semaphore(5)

# 互斥鎖:提供一個鎖
mutex = Lock()

def task():
    sm.acquire()
    print(f"子線程{current_thread().name}")
    time.sleep(1)
    sm.release()


if __name__ == '__main__':
    for i in range(10):
        t = Thread(target=task)
        t.start()

# 五個五個執行
子線程Thread-1
子線程Thread-2
子線程Thread-3
子線程Thread-4
子線程Thread-5

子線程Thread-6
子線程Thread-7
子線程Thread-8
子線程Thread-9
子線程Thread-10

線程隊列

FIFO:先進先出

LIFO:後進先出

優先級隊列:根據參數中的數字字母排序,排在前面的優先級越高,優先取出

# coding=utf-8

import queue
from multiprocessing import Queue

# FIFO隊列:先進先出
q = queue.Queue()
q.put(4)
q.put(2)
q.put(3)

print(q.get())
print(q.get())
# 4
# 2

# LIFO隊列:後進先出
q = queue.LifoQueue()
q.put(1)
q.put(2)
q.put(3)

print(q.get())
print(q.get())
# 3
# 2

# 優先級隊列
q = queue.PriorityQueue()
q.put(3)
q.put(2)
q.put(11)
print(q.get())
# 2
相關文章
相關標籤/搜索