程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式師世界 >> 編程語言 >  >> 更多編程語言 >> Python

Python之進程+線程+協程(並發與並行、GIL鎖、同步鎖、死鎖、遞歸鎖)

編輯:Python


文章目錄

  •  ​一、並發與並行​​
  •  ​二、同步與異步​​
  •  ​三、線程鎖​​
  •  ​1、GIL全局解釋器鎖​​
  •  ​2、同步鎖​​
  •  ​3、死鎖​​
  •  ​4、遞歸鎖​​


在Python中GIL解釋器鎖、同步鎖、死鎖、遞歸鎖都是什麼?怎麼這麼多鎖,它們都是用來控制進程、線程的嗎?作為一個程序猿,你真的理解並行與並發,同步與異步了麼?

希望本篇文章能給你想要的答案…

一、並發與並行

1、並發
一個系統具有處理多個任務的能力,一個CPU可以處理多個任務也叫並發,因為CPU的切換速度極其快

2、並行
一個系統具有 同時 處理多個任務的能力,同時是嚴格意義上的同時進行

3、區別: 因為一個CPU是可以不斷切換任務的,可以實現並發效果,但一個CPU某一個微小的時刻只能正在處理一個任務。
因此,多核的CPU才具有並行的能力,並行是並發的一個子集,並行一定能並發

4、測試代碼:

import
threading

import time

#定義累加和累乘的函數
def add():
sum1 = 0
for i in range( 10000):
sum1 += i
print( '累加結果:\n', sum1)

def mul():
mul2 = 1
for i in range( 1, 1000):
mul2 *= i
print( '累乘結果:', '\n', mul2)

start = time. time()

#實例化線程對象
t1 = threading. Thread( target = add)
t2 = threading. Thread( target = mul)

#將每個線程都追加到列表中
l = []
l. append( t1)
l. append( t2)

#初始化每個線程對象
for t in l:
t. start()

#加入join設置
for t in l:
t. join()

print( '程序一共運行{}秒'. format( time. time() - start))
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.

二、同步與異步

1、同步
一個進程需要接受外部數據,即執行到一個IO操作(等待外部數據)的時候,程序就一直“等著”,不會往下執行;直到接收到了數據

2、異步
一個進程當執行到一個IO操作時,不會等著,會繼續執行後續的操作,某時刻接收到了數據,再返回來處理;很多等待都是沒有意義的,因此異步效率更高

三、線程鎖

1、GIL全局解釋器鎖

概念分析
(1)Cpython用來組織多線程同時運行的一種手段,是Cpython解釋器默認的一種設定。也就說明無論我們開啟多少個線程,有多少個CPU(或者多核),Python在執行的時候都會在同一時刻只運行一個線程。

(2)之所以加了這個解釋器鎖,是python的發明者為了維持解釋器的穩定運行,但對於用戶而言,卻是一道障礙,會讓我們的進程運行效率不高。

(3)對GIL的精簡解釋:因為有GIL,所以同一時刻,只有一個線程被CPU執行,導致我們的多核CPU能力無法展現。

(4)但是GIL是無法去除的,只能從其他方面來提高效率,比如多進程+協程,因為我們無法實現一個進程裡面跑多個線程,那就直接開多個進程。

任務的分類
(1)IO密集型:需要多次等待外部的數據,所以CPU有非常多的空閒時間,我們就可以利用那些空閒時間來做其他任務,因此有GIL的python適合執行IO密集型的任務,還可以采用多進程+協程來提高效率。

(2)計算密集型:需要連續執行下去不能中斷的任務,如計算累乘、計算π的值。

(3)總結:Python的多線程對於IO密集型任務相當有意義,而對於計算密集型的任務則比較低效率,Python不適用。

2、同步鎖

(1)串行計算:

import
threading

import time



#定義累加和累乘的函數
def sub():
global num
temp = num
time. sleep( 0.0001)
num = temp - 1

start = time. time()
num = 100
l = []


#實例化線程對象
for i in range( 100):
t1 = threading. Thread( target = sub)
t1. start()
l. append( t1)

#加入join設置
for t in l:
t. join()

print( '運行結果:{},程序一共運行{}秒'. format( num, time. time() - start))
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.

(2)串行計算結果:


可以看到從100減去1減了100次計算的結果本應該是0,可這裡結果卻是56,而且是變化的數字;

原因就是函數執行中途睡了0.001秒,這個時間不長不短,接近CPU在同一個進程中的切換速度;

就使一部分計算切換過去了,一部分計算還保留原來的值,即有些減去了1,有些還沒減1線程就被切換走了,導致了結算結果的錯誤。

(3)處理方法——加一個同步鎖:
將CPU鎖死,一瞬間只能有一個線程在執行,在這個線程處理完之前都不會讓CPU在線程中切換

將函數內容改成以下代碼就可以防止串行:

#加一個同步鎖

lock = threading. Lock()

#定義累加和累乘的函數
def sub():
#用同步鎖將線程鎖住
lock. acquire()

global num
temp = num
time. sleep( 0.001)
num = temp - 1

#釋放同步鎖
lock. release()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

這樣改一下,就能得出正確結果0了

3、死鎖

(1)死鎖案例:

import
threading

import time

#繼承式線程定義
class MyThread( threading. Thread):
#先獲得A鎖的函數
def actionA( self):
A. acquire() #獲得鎖
print( self. name, 'gotA', time. ctime()) #輸出線程名和時間
time. sleep( 2)

#在沒有釋放A鎖的情況下再獲取B鎖
B. acquire()
print( self. name, 'gotB', time. ctime())
time. sleep( 1)

#釋放A鎖和B鎖
B. release()
A. release()

#先獲得B鎖的函數
def actionB( self):
B. acquire() #獲得鎖
print( self. name, 'gotB', time. ctime()) #輸出線程名和時間
time. sleep( 2)

#在沒有釋放A鎖的情況下再獲取B鎖
A. acquire()
print( self. name, 'gotA', time. ctime())
time. sleep( 1)

#釋放A鎖和B鎖
A. release()
B. release()

def run( self):
#執行兩個動作
self. actionA()
self. actionB()

if __name__ == '__main__':
#創建兩個鎖
A = threading. Lock()
B = threading. Lock()
L = []

#執行5次實例化對象,但因為死鎖,實際只會執行一次
for i in range( 5):
t = MyThread()
t. start()
L. append( t)

for i in L:
i. join()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.

(2)死鎖結果:


Thread 1在actionB中將B鎖鎖住了,沒有釋放,然後Thread 2將A鎖鎖住;

之後先釋放A鎖,但此時進程正處於Thread 2上,該進程上沒有A鎖,無法釋放A鎖;

而Thread 1上是B鎖,由於Thread 2沒有執行完,被鎖住的,所以Thread 1上的B鎖也不會被釋放;

就這樣一直僵持著,導致程序只會執行一遍,之後一直處於死鎖狀態。

4、遞歸鎖

(1)遞歸鎖測試:

import
threading

import time

#繼承式線程定義
class MyThread( threading. Thread):
#先獲得A鎖的函數
def actionA( self):
r_lock. acquire() #獲得鎖
print( self. name, 'gotA', time. ctime()) #輸出線程名和時間
time. sleep( 2)

#在沒有釋放A鎖的情況下再獲取B鎖
r_lock. acquire()
print( self. name, 'gotB', time. ctime())
time. sleep( 1)

#釋放A鎖和B鎖
r_lock. release()
r_lock. release()

#先獲得B鎖的函數
def actionB( self):
r_lock. acquire() #獲得鎖
print( self. name, 'gotB', time. ctime()) #輸出線程名和時間
time. sleep( 2)

#在沒有釋放A鎖的情況下再獲取B鎖
r_lock. acquire()
print( self. name, 'gotA', time. ctime())
time. sleep( 1)

#釋放A鎖和B鎖
r_lock. release()
r_lock. release()

def run( self):
#執行兩個動作
self. actionA()
self. actionB()

if __name__ == '__main__':
start = time. time()


#創建兩個鎖
#A= threading.Lock()
#B= threading.Lock()

#創建一個遞歸鎖
r_lock = threading. RLock()

L = []

#執行5次實例化對象,但因為死鎖,實際只會執行一次
for i in range( 5):
t = MyThread()
t. start()
L. append( t)

for i in L:
i. join()

print( '程序一共執行%s秒' %( time. time() - start))
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.

(2)遞歸鎖結果:


這樣它總共會創建5個線程,5個線程接替遞歸使用,某個進程被占用時就用另外的空閒線程來執行任務,就不會有相互競爭的死鎖狀態;

就相當於遞歸鎖這一把鎖就代表了5把鎖,5把鎖有空閒的都能用。



  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式師世界 All Rights Reserved