併發編程(五)——GIL全局解釋器鎖、死鎖現象與遞歸鎖、信號量、Event事件、線程queue

 GIL、死鎖現象與遞歸鎖、信號量、Event事件、線程queue

1、GIL全局解釋器鎖python

一、什麼是全局解釋器鎖web

GIL本質就是一把互斥鎖,至關於執行權限,每一個進程內都會存在一把GIL,同一進程內的多個線程,必須搶到GIL以後才能使用Cpython解釋器來執行本身的代碼,即同一進程下的多個線程沒法實現並行,可是能夠實現併發。安全

#1 全部數據都是共享的,這其中,代碼做爲一種數據也是被全部線程共享的(test.py的全部代碼以及Cpython解釋器的全部代碼)

#2 全部線程的任務,都須要將任務的代碼當作參數傳給解釋器的代碼去執行,即全部的線程要想運行本身的任務,首先須要解決的是可以訪問到解釋器的代碼。

例以下面多個線程的執行過程:多線程

多個線程先訪問到解釋器的代碼,即拿到執行權限,而後將target的代碼交給解釋器的代碼去執行併發

解釋器的代碼是全部線程共享的,因此垃圾回收線程也可能訪問到解釋器的代碼而去執行,這就致使了一個問題:對於同一個數據100,可能線程1執行x=100的同時,而垃圾回收執行的是回收100的操做,解決這種問題沒有什麼高明的方法,就是加鎖處理,以下圖的GIL,保證python解釋器同一時間只能執行一個任務的代碼app

二、爲何要用GILdom

由於CPython解釋器的垃圾回收機制不是線程安全的socket

2、GIL與LOCKide

鎖的目的 :鎖的目的是爲了保護共享的數據,同一時間只能有一個線程來修改共享的數據性能

(1)GIL是保護解釋器級別的鎖,使利用CPython解釋器時併發使用

(2)LOCK是自定義的鎖,用來保證多線程/進程對同一個數據進行修改時的數據安全,使修改數據時串行

全部線程搶的是GIL鎖,或者說全部線程搶的是執行權限

  線程1搶到GIL鎖,拿到執行權限,開始執行,而後加了一把Lock,尚未執行完畢,即線程1還未釋放Lock,有可能線程2搶到GIL鎖,開始執行,執行過程當中發現Lock尚未被線程1釋放,因而線程2進入阻塞,被奪走執行權限,有可能線程1拿到GIL,而後正常執行到釋放Lock。。。這就致使了串行運行的效果

  既然是串行,那咱們執行

  t1.start()

  t1.join

  t2.start()

  t2.join()

  這也是串行執行啊,爲什麼還要加Lock呢,需知join是等待t1全部的代碼執行完,至關於鎖住了t1的全部代碼,而Lock只是鎖住一部分操做共享數據的代碼
分析

 

注意點 :

 

#1.線程搶的是GIL鎖,GIL鎖至關於執行權限,拿到執行權限後才能拿到互斥鎖Lock,其餘線程也能夠搶到GIL,但若是發現Lock仍然沒有被釋放則阻塞,即使是拿到執行權限GIL也要馬上交出來

#2.join是等待全部,即總體串行,而鎖只是鎖住修改共享數據的部分,即部分串行,要想保證數據安全的根本原理在於讓併發變成串行,join與互斥鎖均可以實現,毫無疑問,互斥鎖的部分串行效率要更高

 

3、GIL與多線程

一、進程能夠利用多核,可是開銷大,而python的多線程開銷小,但卻沒法利用多核優點

===>

#1. cpu究竟是用來作計算的
#2. 多cpu,意味着能夠有多個核並行完成計算,因此多核提高的是計算性能
#3. 每一個cpu一旦遇到I/O阻塞,仍然須要等待,因此多核對I/O操做沒什麼用處 

所以,對計算來講,cpu越多越好,可是對於I/O來講,再多的cpu也沒用。固然對運行一個程序來講,隨着cpu的增多執行效率確定會有所提升(無論提升幅度多大,總會有所提升),這是由於一個程序基本上不會是純計算或者純I/O,因此咱們只能相對的去看一個程序究竟是計算密集型仍是I/O密集型,從而進一步分析python的多線程到底有無用武之地

#分析:
咱們有四個任務須要處理,處理方式確定是要玩出併發的效果,解決方案能夠是:
方案一:開啓四個進程
方案二:一個進程下,開啓四個線程

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

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

 
#結論:
如今的計算機基本上都是多核,python對於計算密集型的任務開多線程的效率並不能帶來多大性能上的提高,甚至不如串行(沒有大量切換),可是,對於IO密集型的任務效率仍是有顯著提高的。
===>多核狀況下,Python對於計算密集型,使用多進程的效率高
        而對於I/O密集型,Python使用多線程的效率高一點

二、測試

from multiprocessing import Process
from threading import Thread
import os,time
def work():
    res=0
    for i in range(10000):
        res*=i


if __name__ == '__main__':
    l=[]
    print(os.cpu_count())
    start=time.time()
    for i in range(4):
        p=Process(target=work)
        #p=Thread(target=work)
        l.append(p)
        p.start()
    for p in l:
        p.join()
    stop=time.time()
    print('run time is %s' %(stop-start))
計算密集型,多進程效率高
from multiprocessing import Process
from threading import Thread
import threading
import os,time
def work():
    time.sleep(2)
    print('===>')

if __name__ == '__main__':
    l=[]
    print(os.cpu_count())
    start=time.time()
    for i in range(400):
        # p=Process(target=work)
        p=Thread(target=work)
        l.append(p)
        p.start()
    for p in l:
        p.join()
    stop=time.time()
    print('run time is %s' %(stop-start))
I/O密集型,多線程效率高

應用場景:

  多線程用於IO密集型,如socket,爬蟲,web
  多進程用於計算密集型,如金融分析

4、死鎖現象與遞歸鎖

一、死鎖現象

兩個或兩個以上的進程或線程在執行過程當中,因爭奪資源而形成的一種互相等待的現象,若無外力做用,它們都將沒法推動下去。此時稱系統處於死鎖狀態或系統產生了死鎖(例如a持有B房間的鑰匙,卻被所在A房間,而b持有A房間的鑰匙,卻被鎖在B房間,兩人都被鎖了),這些永遠在互相等待的進程稱爲死鎖進程

以下就是死鎖

from threading import Thread,Lock,RLock
import time

mutexA=Lock()
mutexB=Lock()



class Mythead(Thread):
    def run(self):
        self.f1()
        self.f2()

    def f1(self):
        mutexA.acquire()
        print('%s 搶到A鎖' %self.name)
        mutexB.acquire()
        print('%s 搶到B鎖' %self.name)
        mutexB.release()
        mutexA.release()

    def f2(self):
        mutexB.acquire()
        print('%s 搶到了B鎖' %self.name)
        time.sleep(2)
        mutexA.acquire()
        print('%s 搶到了A鎖' %self.name)
        mutexA.release()
        mutexB.release()

if __name__ == '__main__':
    for i in range(100):
        t=Mythead()
        t.start()
死鎖

二、死鎖的解決方法——遞歸鎖

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

這個RLock內部維護着一個Lock和一個counter變量,counter記錄了acquire的次數,從而使得資源能夠被屢次require。直到一個線程全部的acquire都被release,其餘的線程才能得到資源。上面的例子若是使用RLock代替Lock,則不會發生死鎖:

mutexA=mutexB=threading.RLock() #一個線程拿到鎖,counter加1,該線程內又碰到加鎖的狀況,則counter繼續加1,這期間全部其餘線程都只能等待,等待該線程釋放全部鎖,即counter遞減到0爲止
from threading import Thread,Lock,RLock
import time

# mutexA=Lock()
# mutexB=Lock()
mutexB=mutexA=RLock()


class Mythead(Thread):
    def run(self):
        self.f1()
        self.f2()

    def f1(self):
        mutexA.acquire()
        print('%s 搶到A鎖' %self.name)
        mutexB.acquire()
        print('%s 搶到B鎖' %self.name)
        mutexB.release()
        mutexA.release()

    def f2(self):
        mutexB.acquire()
        print('%s 搶到了B鎖' %self.name)
        time.sleep(2)
        mutexA.acquire()
        print('%s 搶到了A鎖' %self.name)
        mutexA.release()
        mutexB.release()

if __name__ == '__main__':
    for i in range(100):
        t=Mythead()
        t.start()
利用遞歸鎖解決死鎖

5、信號量

Semaphore管理一個內置的計數器,
每當調用acquire()時內置計數器-1;
調用release() 時內置計數器+1;
計數器不能小於0;當計數器爲0時,acquire()將阻塞線程直到其餘線程調用release()。

===>一直有n個線程併發運行,可是這n個線程是變化的,不是最初的n個線程在運行

from threading import Thread,Semaphore
import time,random
sm=Semaphore(5)     #容許5個線程併發運行,可是5個線程能夠不是原來那5個

def task(name):
    sm.acquire()
    print('%s 正在上廁所' %name)
    time.sleep(random.randint(1,3))
    sm.release()

if __name__ == '__main__':
    for i in range(20):
        t=Thread(target=task,args=('路人%s' %i,))
        t.start()
View Code

6、Event

當一個線程須要經過判斷其餘線程的狀態來決定下一步的操做,能夠利用Event來實現

event.isSet():返回event的狀態值;

event.wait():若是 event.isSet()==False將阻塞線程;

event.set(): 設置event的狀態值爲True,全部阻塞池的線程激活進入就緒狀態, 等待操做系統調度;

event.clear():恢復event的狀態值爲False。
from threading import Thread,Event
import time

event=Event()

def light():
    print('紅燈正亮着')
    time.sleep(3)
    event.set() #綠燈亮

def car(name):
    print('車%s正在等綠燈' %name)
    event.wait() #等燈綠
    print('車%s通行' %name)

if __name__ == '__main__':
    # 紅綠燈
    t1=Thread(target=light)
    t1.start()
    #
    for i in range(10):
        t=Thread(target=car,args=(i,))
        t.start()
車與紅綠燈問題

7、進程隊列(比較互斥鎖,推薦使用隊列)

一、queue隊列 :使用import queue,用法與進程Queue同樣

二、三種隊列

(1). Queue:先進先出隊列

import queue

q=queue.Queue()
q.put('first')
q.put('second')
q.put('third')

print(q.get())
print(q.get())
print(q.get())


'''
結果(先進先出):
first
second
third
'''
Queue隊列

(2). LifoQueue:先進後出隊列(至關於堆棧)

import queue

q=queue.Queue()
q.put('first')
q.put('second')
q.put('third')

print(q.get())
print(q.get())
print(q.get())



'''
結果(先進先出):
third
second
first
'''
先進後出隊列

(3). ProirityQueue:優先級隊列

put進入一個元組,元組的第一個元素是優先級(一般是數字,也能夠是非數字之間的比較),數字越小優先級越高

import queue

q=queue.PriorityQueue()
#put進入一個元組,元組的第一個元素是優先級(一般是數字,也能夠是非數字之間的比較),數字越小優先級越高
q.put((20,'a'))
q.put((10,'b'))
q.put((30,'c'))

print(q.get())
print(q.get())
print(q.get())


'''
結果(數字越小優先級越高,優先級高的優先出隊):
(10, 'b')
(20, 'a')
(30, 'c')
'''
優先級隊列
相關文章
相關標籤/搜索