併發編程之多線程篇之三

主要內容:python

  1、GIL全局解釋器鎖linux

  2、死鎖現象和遞歸鎖web

 

1️⃣ GIL全局解釋器鎖windows

  一、Cpython的GIL解釋器鎖的工做機制   安全

GIL本質就是一把互斥鎖,既然是互斥鎖,全部互斥鎖的本質都同樣,都是將併發運行變成串行,以此來控制
  同一時間內共享數據只能被一個任務所修改,進而保證數據安全。能夠確定的一點是:保護不一樣的數據的安全,就
  應該加不一樣的鎖。要想了解GIL,首先肯定一點:每次執行python程序,都會產生一個獨立的進程。例如python test1.py,
  python test2.py,python test3.py會產生3個不一樣的python進程。
    驗證進程的方法:    1.1 在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。
    1.2 GIL的介紹
    
#test.py內容
import os,time print(os.getpid()) time.sleep(1000) #打開終端執行
python3 test.py #在windows下查看
tasklist |findstr python #在linux下下查看
ps aux |grep python
View Code

    在一個python的進程內,不只有test.py的主線程或者由該主線程開啓的其餘線程,還有解釋器開啓的多線程

  垃圾回收等解釋器級別的線程,總之,毫無疑問,全部線程都運行在這一個進程內。併發

    1.3 GIL原理分析

    第一點:
若是多個線程的target=work,那麼執行流程是多個線程先訪問到解釋器的代碼,即拿到執行權限,
  而後將target的代碼交給解釋器的代碼去執行。
    重點:
        全部數據都是共享的,這其中,代碼做爲一種數據也是被全部線程共享的(test.py的全部代碼以及Cpython解釋器的全部代碼)     例如:test.py定義一個函數work(代碼內容以下圖),在進程內全部線程都能訪問到work的代碼,因而咱們能夠開啓三個線程
  而後target都指向該代碼,能訪問到意味着就是能夠執行。

    第二點:

     全部線程的任務,都須要將任務的代碼當作參數傳給解釋器的代碼去執行,即全部的線程要想運行本身的任務,
   首先須要解決的是可以訪問到解釋器的代碼。
  
    由以上兩點不可貴知:
    
解釋器的代碼是全部線程共享的,因此垃圾回收線程也可能訪問到解釋器的代碼而去執行,這就致使了一個問題:對於同一個數據100,可能線程1執行x=100的同時,而垃圾回收執行的是回收100的操做,解決這種問題沒有什麼高明的方法,就是加鎖處理,即GIL,保證python解釋器同一時間只能執行一個任務的代碼。

    如圖所示:app

  

  二、 GIL 和 Lock
    Python已經有一個GIL來保證同一時間只能有一個線程來執行了,爲何這裏還須要lock?
    咱們已經知道:鎖的目的是爲了保護共享的數據,同一時間只能有一個線程來修改共享的數據。保護不一樣的數據就應該加不一樣的鎖。
    因此,解釋就是GIL 與Lock是兩把鎖,保護的數據不同,前者是解釋器級別的(固然保護的就是解釋器級別的數據,好比垃圾回收的數據),
  後者是保護用戶本身開發的應用程序的數據,很明顯GIL不負責這件事,只能用戶自定義加鎖處理,即Lock。以下圖:

  
    解釋以下:
    
1、100個線程去搶GIL鎖,即搶執行權限 2、確定有一個線程先搶到GIL(暫且稱爲線程1),而後開始執行,一旦執行就會拿到lock.acquire() 3、極有可能線程1還未運行完畢,就有另一個線程2搶到GIL,而後開始運行,但線程2發現互斥鎖lock還未被線程1釋放,因而阻塞,被迫交出執行權限,即釋放GIL 四、直到線程1從新搶到GIL,開始從上次暫停的位置繼續執行,直到正常釋放互斥鎖lock,而後其餘的線程再重複2 3 4的過程

   代碼示例:socket

from threading import Thread,Lock
import os,time
def work():
    global n
    lock.acquire()
    temp=n
    time.sleep(0.1)
    n=temp-1
    lock.release()
if __name__ == '__main__':
    lock=Lock()
    n=100
    l=[]
    for i in range(100):
        p=Thread(target=work)
        l.append(p)
        p.start()
    for p in l:
        p.join()

    print(n) #結果確定爲0,由原來的併發執行變成串行,犧牲了執行效率保證了數據安全,不加鎖則結果可能爲99
GIL與多線程
三、
  有了GIL的存在,意味着同一時刻同一進程中只有一個線程被執行。
  咱們已經知道進程能夠利用多核,可是開銷大,而python的多線程開銷小,但卻沒法利用多核優點。也許
你會認爲那python多線程還有什麼存在的意義呢?壓根毛線用沒有。。。
  可事實並不是如此...
  在開講以前,咱們先來關注一下這幾點:
一、cpu究竟是用來作計算的,仍是用來作I/O的? 2、多cpu,意味着能夠有多個核並行完成計算,因此多核提高的是計算性能 三、每一個cpu一旦遇到I/O阻塞,仍然須要等待,因此多核對I/O操做沒什麼用處

  通俗的來理解的話,能夠把一個工人比做CPU,ide

此時計算至關於工人在幹活,I/O阻塞至關於爲工人幹活提供所需原材料的過程,工人幹活的過程當中若是沒有原材料了,

則工人幹活的過程須要中止,直到等待原材料的到來。若是你的工廠乾的大多數任務都要有準備原材料的過程(I/O密集型),

那麼你有再多的工人,意義也不大,還不如一我的,在等材料的過程當中讓工人去幹別的活,反過來說,若是你的工廠原材料都齊全,

那固然是工人越多,效率越高。

 由此咱們可知:
一、對計算來講,cpu越多越好,可是對於I/O來講,再多的cpu也沒用 二、固然對運行一個程序來講,隨着cpu的增多執行效率確定會有所提升(無論提升幅度多大,總會有所提升),這是由於一個程序基本上不會是純計算或者純I/O,因此咱們只能相對的去看一個程序究竟是計算密集型仍是I/O密集型,從而進一步分析python的多線程到底有無用武之地
 假設咱們有四個任務須要處理,處理方式確定是要玩出併發的效果,解決方案能夠是
方案一:開啓四個進程
方案二:一個進程下,開啓四個線程

  單核狀況下,分析結果:

若是四個任務是計算密集型,沒有多核來並行計算,方案一徒增了建立進程的開銷,方案二勝 若是四個任務是I/O密集型,方案一建立進程的開銷大,且進程的切換速度遠不如線程,方案二勝

  多核狀況下,分析結果:

若是四個任務是計算密集型,多核意味着並行計算,在python中一個進程中同一時刻只有一個線程執行用不上多核,方案一勝 若是四個任務是I/O密集型,再多的核也解決不了I/O問題,方案二勝

  結論:  如今的計算機基本上都是多核,python對於計算密集型的任務開多線程的效率並不能帶來多大性能上的提高,

    甚至不如串行(沒有大量切換),可是,對於IO密集型的任務效率仍是有顯著提高的。

  實例測試:

    一、 計算密集型:用多進程   

#!/usr/bin/env python3 #-*- coding:utf-8 -*- # write by congcong

from multiprocessing import Process from threading import Thread import time,os def task(): res = 10
    for i in range(10000000): res *= i if __name__ == '__main__': t_list = [] print(os.cpu_count()) # 顯示本機cpu核心數 4
    start = time.time() for i in range(16): p = Process(target=task) # 耗時 6.345133304595947
        #p = Thread(target=task) # 耗時:9.412602186203003
 t_list.append(p) p.start() for p in t_list: p.join() stop = time.time() print('Running time is %s'%(stop-start))
View Code

    二、IO密集型:用多線程

#!/usr/bin/env python3
#-*- coding:utf-8 -*-
# write by congcong

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

def work():
    time.sleep(2)

if __name__ == '__main__':
    w_list = []
    start = time.time()
    for i in range(1000):
        p = Thread(target=work) # 耗時:2.1222898960113525
        #p = Process(target=work) # 耗時:255.03309321403503 大部分時間都消耗在建立進程上
        w_list.append(p)
        p.start()
    for p in w_list:
        p.join()
    stop = time.time()
    print('Running time is %s'%(stop-start))
View Code

    三、實際應用

    多線程用於IO密集型,如socket,爬蟲,web;

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

 

2️⃣ 死鎖現象和遞歸鎖

  一、死鎖現象

  所謂死鎖: 是指兩個或兩個以上的進程或線程在執行過程當中,因爭奪資源而形成的一種互相等待的現象,

若無外力做用,它們都將沒法推動下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的

進程稱爲死鎖進程,以下就是死鎖: 

#!/usr/bin/env python3 #-*- coding:utf-8 -*- # write by congcong

from threading import Thread,Lock,RLock import time muteA = Lock() muteB = Lock() class MyThread(Thread): def run(self): # run方法是固定方法
 self.f1() self.f2() def f1(self): muteA.acquire() print('%s 拿到了鎖A'%self.name) muteB.acquire() print('%s 拿到了鎖B'%self.name) muteB.release() muteA.release() def f2(self): muteB.acquire() print('%s 拿到了鎖B'%self.name) time.sleep(1) muteA.acquire() print('%s 拿到了鎖A'%self.name) muteA.release() muteB.release() if __name__ == '__main__': for t in range(10): t = MyThread() t.start()

輸出結果:

Thread-1 拿到了鎖A Thread-1 拿到了鎖B Thread-1 拿到了鎖B Thread-2 拿到了鎖A 緣由分析: 此時線程卡住不動,即死鎖(緣由:Thread-1拿到了鎖A,拿到了鎖B徹底沒有競爭,Thread-1把f1執行完, 此時鎖A鎖B都釋放,接着執行f2去拿鎖B,則Thread-2此時能夠拿到鎖A,Thread-2又準備接着拿鎖B,但鎖B在Thread-1手中 ,即便Thread-1 中止休眠了,去取鎖A,但鎖A在Thread-2手中,因此兩個線程都沒法繼續運行)    

 

  二、遞歸鎖

    針對上述死鎖現象的解決方法就是遞歸鎖。

    遞歸鎖,在Python中爲了支持在同一線程中屢次請求同一資源,python提供了可重入鎖RLock。

    這個RLock內部維護着一個Lock和一個counter變量,counter記錄了acquire的次數,從而使得資源

  能夠被屢次require。直到一個線程全部的acquire都被release,其餘的線程才能得到資源。上面的例子

  若是使用RLock代替Lock,則不會發生死鎖。

    互斥鎖和遞歸鎖的區別:

       遞歸鎖能夠連續acquire屢次,而互斥鎖只能acquire一次。

   代碼實例以下:
#!/usr/bin/env python3 #-*- coding:utf-8 -*- # write by congcong

# 遞歸鎖

from threading import Thread,Lock,RLock import time muteA = muteB = RLock() # 此時muteA和muteB是同一個互斥鎖,初始鎖爲0,每acquire一次,自加1,每release一次,自減1,僅當值爲0時,其餘線程才能再得到鎖運行
class MyThread(Thread): def run(self):  # run方法是固定方法
 self.f1() self.f2() def f1(self): muteA.acquire() # 鎖爲1
        print('%s 拿到了鎖A' % self.name) muteB.acquire() # 鎖爲2
        print('%s 拿到了鎖B' % self.name) muteB.release() # 鎖減1
 muteA.release() # 鎖減1,變爲0,徹底釋放

    def f2(self): muteB.acquire() print('%s 拿到了鎖B' % self.name) time.sleep(1) muteA.acquire() print('%s 拿到了鎖A' % self.name) muteA.release() muteB.release() if __name__ == '__main__': for t in range(5): t = MyThread() t.start()
 

思考一下,看你想的是否與下述結果相同:

Thread-1 拿到了鎖A
Thread-1 拿到了鎖B
Thread-1 拿到了鎖B
Thread-1 拿到了鎖A
Thread-2 拿到了鎖A
Thread-2 拿到了鎖B
Thread-2 拿到了鎖B
Thread-2 拿到了鎖A
Thread-4 拿到了鎖A
Thread-4 拿到了鎖B
Thread-4 拿到了鎖B
Thread-4 拿到了鎖A
Thread-3 拿到了鎖A
Thread-3 拿到了鎖B
Thread-3 拿到了鎖B
Thread-3 拿到了鎖A
Thread-5 拿到了鎖A
Thread-5 拿到了鎖B
Thread-5 拿到了鎖B
Thread-5 拿到了鎖A
View Code
相關文章
相關標籤/搜索