python多線程教程:python線程及多線程實例講解

進程和線程
一、進程
進程是程序的分配資源的最小單元;一個程序可以有多個進程,但只有一個主進程;進程由程序、數據集、控制器三部分組成。
二、線程
線程是程序最小的執行單元;一個進程可以有多個線程,但是隻有一個主線程;線程切換分爲兩種:一種是I/O切換,一種是時間切換(I/O切換:一旦運行I/O任務時便進行線程切換,CPU開始執行其他線程;時間切換:一旦到了一定時間,線程也進行切換,CPU開始執行其他線程)。
三、總結
一個程序至少有一個進程和一個線程;
程序的工作方式:
1.單進程單線程;2.單進程多線程;3.多進程多線程;
考慮到實現的複雜性,一般最多隻會採用單進程多線程的工作方式;
四、爲什麼要使用多線程
我們在實際生活中,希望既能一邊瀏覽網頁,一邊聽歌,一邊打遊戲。這時,如果只開一個進程,爲了滿足需求,CPU只能快速切換進程,但是在切換進程時會造成大量資源浪費。所以,如果是多核CPU,可以在同時運行多個進程而不用進行進程之間的切換。
然而,在實際中,比如:你在玩遊戲的時候,電腦需要一邊顯示遊戲的動態,一邊你還得和同伴進行語音或語言進行溝通。這時,如果是單線程的工作方式,將會造成在操作遊戲的時候就無法給同伴溝通,在和同伴溝通的時候就無法操作遊戲。爲了解決該問題,我們可以開啓多線程來共享遊戲資源,同時進行遊戲操作和溝通。
五、實例
場景一:併發依次執行在這裏插入圖片描述
如上圖所示:有兩個簡單的函數,一個是聽音樂一個是打遊戲的函數。
如果按照之前的單線程方式,將會是先運行完聽音樂的函數再去運行打遊戲的函數,最後打印Ending。如下圖所示:在這裏插入圖片描述
一共的運行時間是6秒。並且是隻能單一按照順序依次去執行。而使用多線時,運行時間是3秒,並且是並行執行。
該情況下的多線程運行方式是,先創建線程1,再創建線程2,然後去啓動線程1和線程2,並和主線程同時運行。此種情況下,若子線程先於主線程運行完畢,則子線程先關閉後主線程運行完畢關閉;若主線程先於子線程結束,則主線程要等待所有的子線程運行完畢後再關閉。
該部分代碼塊:

import threading
import time
def music(name):
    print('%s begin listen music%s'%(name,time.ctime()))
    time.sleep(3)
    print('%s stop listen music%s' % (name, time.ctime()))
def game(name):
    print('%s begin play game%s'%(name,time.ctime()))
    time.sleep(3)
    print('%s stop play game%s' % (name,time.ctime()))
if __name__ == '__main__':
    # threadl = []
    # t1 = threading.Thread(target=music,args=('zhang',))
    # t2 = threading.Thread(target=game,args=('zhang',))
    # t1.start()
    # t2.start()
    music('zhang')
    game('zhang')
    print('Ending now %s'%time.ctime())

場景二:主線程等待某子線程結束後才能執行(join()函數的用法)
例如:在實際中,需要子線程在插入數據,主線程需要等待數據插入結束後才能進行查詢驗證操作(測試驗證數據)在這裏插入圖片描述
該部分代碼塊爲:
import threading
import time
def music(name):
print(’%s begin listen music%s’%(name,time.ctime()))
time.sleep(5)
print(’%s stop listen music%s’ % (name, time.ctime()))
def game(name):
print(’%s begin play game%s’%(name,time.ctime()))
time.sleep(3)
print(’%s stop play game%s’ % (name,time.ctime()))
if name == ‘main’:
threadl = [] #線程列表,用例存放線程
#產生線程的實例
t1 = threading.Thread(target=music,args=(‘zhang’,)) #target是要執行的函數名(不是函數),args是函數對應的參數,以元組的形式;
t2 = threading.Thread(target=game,args=(‘zhang’,))
threadl.append(t1)
threadl.append(t2)
#循環列表,依次執行各個子線程
for x in threadl:
x.start()
#將最後一個子線程阻塞主線程,只有當該子線程完成後主線程才能往下執行
x.join()
print(‘Ending now %s’%time.ctime())
該部分代碼塊爲:

import threading
import time
def music(name):
print(’%s begin listen music%s’%(name,time.ctime()))
time.sleep(2)
print(’%s stop listen music%s’ % (name, time.ctime()))
def game(name):
print(’%s begin play game%s’%(name,time.ctime()))
time.sleep(5)
print(’%s stop play game%s’ % (name,time.ctime()))
if name == ‘main’:
threadl = [] #線程列表,用例存放線程
#產生線程的實例
t1 = threading.Thread(target=music,args=(‘zhang’,)) #target是要執行的函數名(不是函數),args是函數對應的參數,以元組的形式;
t2 = threading.Thread(target=game,args=(‘zhang’,))
threadl.append(t1)
threadl.append(t2)
#循環列表,依次執行各個子線程
for x in threadl:
x.start()
#將子線程t1阻塞主線程,只有當該子線程完成後主線程才能往下執行
t1.join()
print(‘Ending now %s’%time.ctime())
六、線程守護(setDaemon()函數)
前面不管是不是用到了join()函數,主線程最後總是要得所有的子線程執行完成後且自己執行完才能關閉(以子線程爲主來結束主線程)。下面,我們講述一種以主線程爲主的方法來結束主線程。
圖1:無線程守護
python線程及多線程實例講解
圖2:t2線程守護
python線程及多線程實例講解
該部分代碼塊爲:

import threading
import time
def music(name):
print(’%s begin listen music%s’%(name,time.ctime()))
time.sleep(2)
print(’%s stop listen music%s’ % (name, time.ctime()))
def game(name):
print(’%s begin play game%s’%(name,time.ctime()))
time.sleep(5)
print(’%s stop play game%s’ % (name,time.ctime()))
if name == ‘main’:
threadl = [] #線程列表,用例存放線程
#產生線程的實例
t1 = threading.Thread(target=music,args=(‘zhang’,)) #target是要執行的函數名(不是函數),args是函數對應的參數,以元組的形式;
t2 = threading.Thread(target=game,args=(‘zhang’,))
threadl.append(t1)
threadl.append(t2)
#循環列表,依次執行各個子線程
t2.setDaemon(True) #t2線程守護
for x in threadl:
x.start()
#將子線程t1阻塞主線程,只有當該子線程完成後主線程才能往下執行
print(‘Ending now %s’%time.ctime())
所謂’線程守護’,就是主線程不管該線程的執行情況,只要是其他子線程結束且主線程執行完畢,主線程都會關閉。也就是說:主線程不等待該守護線程的執行完再去關閉。
注意:setDaemon方法必須在start之前且要帶一個必填的布爾型參數
七、自定義的方式來產生多線程
python線程及多線程實例講解
該部分代碼塊爲:

import threading
import time
class mythread1(threading.Thread):
‘自定義線程’
def init(self,name):
threading.Thread.init(self)
self.name=name
def run(self):
‘定義每個線程要運行的函數,此處爲music函數’
print(’%s begin listen music, %s’ % (self.name, time.ctime()))
time.sleep(5)
print(’%s stop listen music, %s’ % (self.name, time.ctime()))

class mythread2(threading.Thread):
‘自定義線程’
def init(self,name):
threading.Thread.init(self)
self.name=name
def run(self):
‘定義每個線程要運行的函數,此處爲game函數’
print(’%s begin play game, %s’ % (self.name, time.ctime()))
time.sleep(2)
print(’%s stop play game, %s’ % (self.name, time.ctime()))
if name == ‘main’:
threadl = []
t1 = mythread1(‘zhang’)
t2 = mythread2(‘zhang’)
threadl.append(t1)
threadl.append(t2)
for x in threadl:
x.start()
print(‘Ending now %s’ % time.ctime())
八、Threading的其他常用方法
getName() :獲取線程名稱
setName():設置線程名稱
run():用以表示線程活動的方法(見七中自定義線程的run方法)
rtart():啓動線程活動
is_alive():表示線程是否處於活動的狀態,結果爲布爾值;
threading.active_count():返回正在運行線程的數量
Threading.enumerate():返回正在運行線程的列表
python線程及多線程實例講解
該部分代碼塊爲;

import threading
import time
def music(name):
print(’%s begin listen music%s’%(name,time.ctime()))
time.sleep(2)
print(’%s stop listen music%s’ % (name, time.ctime()))
def game(name):
print(’%s begin play game%s’%(name,time.ctime()))
time.sleep(5)
print(’%s stop play game%s’ % (name,time.ctime()))
if name == ‘main’:
threadl = [] #線程列表,用例存放線程
#產生線程的實例
t1 = threading.Thread(target=music,args=(‘zhang’,)) #target是要執行的函數名(不是函數),args是函數對應的參數,以元組的形式;
t2 = threading.Thread(target=game,args=(‘zhang’,))
threadl.append(t1)
threadl.append(t2)
#循環列表,依次執行各個子線程
t2.setDaemon(True) #t2線程守護,setDaemon方法必須在start之前且要帶一個必填的布爾型參數
t1.setName(‘線程1’) #設置線程的名字
for x in threadl:
print(‘線程爲:’,x.getName()) #獲取線程的名字
print(‘線程t1是否活動:’,t1.is_alive()) #判斷線t1是否處於活動狀態
x.start()
print(‘正在運行線程的數量爲:’,threading.active_count()) #獲取正處於活動狀態線程的數量
print(‘正在運行線程的數量爲:’,threading.activeCount) #獲取正處於活動狀態線程的數量
print(‘正在運行線程的list爲:’,threading.enumerate()) #獲取正處於活動狀態線程的list
print(‘正在運行線程的list爲:’,threading._enumerate()) #獲取正處於活動狀態線程的list
#將子線程t1阻塞主線程,只有當該子線程完成後主線程才能往下執行
print(‘正在運行的線程爲:’,threading.current_thread().getName()) #獲取當前線程的名字
print(‘Ending now %s’%time.ctime())
九、GIL:cpython解釋器的’BUG’
首先需要明確的一點是GIL並不是Python的特性,它是在實現Python解析器(CPython)時所引入的一個概念。在其中的JPython就沒有GIL。然而因爲CPython是大部分環境下默認的Python執行環境。所以在很多人的概念裏CPython就是Python,也就想當然的把GIL歸結爲Python語言的缺陷。所以這裏要先明確一點:GIL並不是Python的特性,Python完全可以不依賴於GIL。
GIL:global interpreter lock,全局解釋器鎖。原文:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
也就是說:無論有多少個CPU,開啓多少線程,每次只能執行一個線程。
基於此設計原理上,我們會覺得python的多線程其實完全沒有用,如下圖不開多線程執行的時間:
python線程及多線程實例講解
如下圖開啓多線程執行的時間:
python線程及多線程實例講解
好吧,前者是0.3秒,後者是20秒,這個結果是不是無法接受…
該部分代碼塊:

import threading
import time
def add(n):
sum=0
for x in range(1,n+1):
sum+=x
print(‘sum =’,sum)
def accumulate(n):
mul=1
for x in range(1,n+1):
mul*=x
print(‘mul =’,mul)
if name == ‘main’:
thread=[]
t1=threading.Thread(target=add,args=(10000001,))
t2=threading.Thread(target=accumulate,args=(100001,))
thread.append(t1)
thread.append(t2)
starttime=time.time()
for i in thread:
i.start()
for i in thread:
i.join()
# add(1000001)
# accumulate(10001)
endtime = time.time()
print(‘spendtime:’, endtime-starttime)
GIL原理:
python線程及多線程實例講解
前者是單線程,任務串行,執行完add函數後再執行accumulate函數,不用進行線程間的切換。而在後者中,線程add和線程accumulate及主線程三者需要不斷的切換來執行,其中切換線程需要消耗大量時間和資源。所以,我們看到是後者的時間是前者的7倍左右。
但是,我們在上面的music和game線程中卻發現多線程能大大的節省時間,提高效率,那又是爲什麼呢?其實,主要要看任務的類型,我們把任務分爲I/O密集型和計算密集型,而多線程在切換中又分爲I/O切換和時間切換。如果任務屬於是I/O密集型,若不採用多線程,我們在進行I/O操作時,勢必要等待前面一個I/O任務完成後面的I/O任務才能進行,在這個等待的過程中,CPU處於等待狀態,這時如果採用多線程的話,剛好可以切換到進行另一個I/O任務。這樣就剛好可以充分利用CPU避免CPU處於閒置狀態,提高效率。但是如果多線程任務都是計算型,CPU會一直在進行工作,直到一定的時間後採取多線程時間切換的方式進行切換線程,此時CPU一直處於工作狀態,此種情況下並不能提高性能,相反在切換多線程任務時,可能還會造成時間和資源的浪費,導致效能下降。這就是造成上面兩種多線程結果不能的解釋。
結論:I/O密集型任務,建議採取多線程,還可以採用多進程+協程的方式(例如:爬蟲多采用多線程處理爬取的數據);對於計算密集型任務,python此時就不適用了。
十、線程同步鎖
1.爲什麼需要同步鎖
看下面例子,我們自定義一個減1的函數,初始賦值100,使用多線程,開啓100個線程,那麼期望的結果是最終結果爲0,看下圖:
python線程及多線程實例講解
該部分代碼塊爲:

#進程鎖
import threading
import time

def subtraction():
global sum
tmp=sum
time.sleep(0.001)
sum=tmp-1

sum=100
if name == ‘main’:
thread=[]
for x in range(100):
t=threading.Thread(target=subtraction)
thread.append(t)
t.start()
for x in thread:
t.join()
print('sum = ',sum)
上面現象產生的原因爲:我們在開啓100個線程的時候,當100個線程在進行subtraction函數操作時,首先要獲取各自的sum(漏洞:共同的數據不能共享同時被多線程操作)和tmp,但是此時多線程會按照時間規則來進行切換,如果當前面某些線程在處理sum時未結束,後面的進程已經開始了(上面例子中的代碼增加了休眠時間來體現該效果),此時拿到的sum就不再是sum-1的期望結果了,而是拿到了sum的值。這樣就會導致,此次的線程進行自減1的操作失效了。So,就會導致上圖的現象了,下面就講述該如何通過加同步鎖來解決該問題。
2.增加同步鎖進行處理共同數據
如下圖:
python線程及多線程實例講解
該部分代碼塊如下:

#進程鎖
import threading
import time
l=threading.Lock()
def subtraction():
global sum
l.acquire()
tmp=sum
time.sleep(0.001)
sum=tmp-1
l.release()
sum=100
if name == ‘main’:
thread=[]
for x in range(100):
t=threading.Thread(target=subtraction)
thread.append(t)
t.start()
for x in thread:
t.join()
print('sum = ',sum)
難點:2.1何處加鎖?何處釋放鎖?簡單的原則就是:需要在引起多線程相互矛盾的共同數據部分枷鎖,例如上面例子中的sum多個線程都要使用且後面線程期望使用的應該是前面線程減1的結果;還有在數據庫操作時,使用自增主鍵時,也要對插入的數據進行加鎖,否則將可能會導致主鍵重複。
2.2加鎖的部分代碼相當於是單線程串行運行了。
3.進程死鎖
在線程間共享多個資源的時候,如果分別佔有一部分資源並且同時在等待對方的資源,就會造成死鎖。例如;數據庫操作時A線程需要B線程的結果進行操作,B線程的需要A線程的結果進行操作,當A,B線程同時在進行操作還沒有結果出來時,此時A,B線程將會一直處於等待對方結束的狀態。
現象如下圖:
python線程及多線程實例講解
該部分代碼塊如下:

#死鎖
import threading
lockA = threading.Lock()
lockB = threading.Lock()
class Mythread(threading.Thread):
‘自定義線程類’
def actionA(self):
‘actionA函數中運行actionB函數,運行actionB函數前加鎖,運行actionB函數結束後釋放鎖’
lockA.acquire()
print(self.name,‘運行actionA’)
self.actionB()
lockA.release()
def actionB(self):
‘actionB函數中運行actionA函數,運行actionA函數前加鎖,運行actionA函數結束後釋放鎖’
lockB.acquire()
print(self.name,‘運行actionB’)
self.actionA()
lockB.release()
def run(self):
‘運行函數’
self.actionA()
self.actionB()
if name == ‘main’:
thread = []
for x in range(3):
t = Mythread()
thread.append(t)
print(‘以啓動線程:’, t.getName())
t.start()
for t in thread:
t.join()
print(‘ending…’)
十一、多線程利器-隊列(squeue)
場景:定義一個函數用例刪除列表中最後一個元素,使用多線程來刪除一個列表中的數據,現象如下圖所示:
python線程及多線程實例講解
該部分的代碼塊如下:

import threading,time
l=[1,3,4,6,8]
def pop(l):
a=l[-1]
print(a)
time.sleep(0.001)
l.remove(a)
if name == ‘main’:
th=[]
for x in range(3):
t = threading.Thread(target=pop, args=(l,))
th.append(t)
print(t.getName())
t.start()
for x in th:
x.join()
# pop(l)
print('l = ',l)
此處由於多線程在操作時可能拿到相同的最後一個元素值,此時若前者的線程已經刪除了該元素,則後面線程的函數則無法刪除該元素(remove是按元素來進行刪除的)。爲了解決此次共享數據導致的多線程問題,我們可以利用前面的進程同步鎖來處理,我們可以在獲取和刪除數據的時候加鎖,代碼如下:
import threading,time
lock = threading.Lock()
l=[1,3,4,6,8]
def pop(l):
# lock.acquire()
a=l[-1]
print(a)
time.sleep(0.001)
l.remove(a)
# lock.release()
if name == ‘main’:
th=[]
for x in range(3):
t = threading.Thread(target=pop, args=(l,))
th.append(t)
print(t.getName())
t.start()
for x in th:
x.join()
# pop(l)
print('l = ',l)
在該部分,我們引入新的模塊queue(線程隊列)來解決該問題,如下圖所示:

該部分代碼塊如下:

import threading
import time
def music(name):
    print('%s begin listen music%s'%(name,time.ctime()))
    time.sleep(2)
    print('%s stop listen music%s' % (name, time.ctime()))
def game(name):
    print('%s begin play game%s'%(name,time.ctime()))
    time.sleep(5)
    print('%s stop play game%s' % (name,time.ctime()))
if __name__ == '__main__':
    threadl = []    #線程列表,用例存放線程
    #產生線程的實例
    t1 = threading.Thread(target=music,args=('zhang',)) #target是要執行的函數名(不是函數),args是函數對應的參數,以元組的形式;
    t2 = threading.Thread(target=game,args=('zhang',))
    threadl.append(t1)
    threadl.append(t2)
    #循環列表,依次執行各個子線程
    t2.setDaemon(True) #t2線程守護
    for x in threadl:
        x.start()
    #將子線程t1阻塞主線程,只有當該子線程完成後主線程才能往下執行
    print('Ending now %s'%time.ctime())

所謂’線程守護’,就是主線程不管該線程的執行情況,只要是其他子線程結束且主線程執行完畢,主線程都會關閉。也就是說:主線程不等待該守護線程的執行完再去關閉。
注意:setDaemon方法必須在start之前且要帶一個必填的布爾型參數
七、自定義的方式來產生多線程在這裏插入圖片描述
該部分代碼塊爲:

import threading
import time
class mythread1(threading.Thread):
    '自定義線程'
    def __init__(self,name):
        threading.Thread.__init__(self)
        self.name=name
    def run(self):
        '定義每個線程要運行的函數,此處爲music函數'
        print('%s begin listen music, %s' % (self.name, time.ctime()))
        time.sleep(5)
        print('%s stop listen music, %s' % (self.name, time.ctime()))

class mythread2(threading.Thread):
    '自定義線程'
    def __init__(self,name):
        threading.Thread.__init__(self)
        self.name=name
    def run(self):
        '定義每個線程要運行的函數,此處爲game函數'
        print('%s begin play game, %s' % (self.name, time.ctime()))
        time.sleep(2)
        print('%s stop play game, %s' % (self.name, time.ctime()))
if __name__ == '__main__':
    threadl = []
    t1 = mythread1('zhang')
    t2 = mythread2('zhang')
    threadl.append(t1)
    threadl.append(t2)
    for x in threadl:
        x.start()
    print('Ending now %s' % time.ctime())

八、Threading的其他常用方法
getName() :獲取線程名稱
setName():設置線程名稱
run():用以表示線程活動的方法(見七中自定義線程的run方法)
rtart():啓動線程活動
is_alive():表示線程是否處於活動的狀態,結果爲布爾值;
threading.active_count():返回正在運行線程的數量
Threading.enumerate():返回正在運行線程的列表在這裏插入圖片描述
該部分代碼塊爲;

import threading
import time
def music(name):
    print('%s begin listen music%s'%(name,time.ctime()))
    time.sleep(2)
    print('%s stop listen music%s' % (name, time.ctime()))
def game(name):
    print('%s begin play game%s'%(name,time.ctime()))
    time.sleep(5)
    print('%s stop play game%s' % (name,time.ctime()))
if __name__ == '__main__':
    threadl = []    #線程列表,用例存放線程
    #產生線程的實例
    t1 = threading.Thread(target=music,args=('zhang',)) #target是要執行的函數名(不是函數),args是函數對應的參數,以元組的形式;
    t2 = threading.Thread(target=game,args=('zhang',))
    threadl.append(t1)
    threadl.append(t2)
    #循環列表,依次執行各個子線程
    t2.setDaemon(True) #t2線程守護,setDaemon方法必須在start之前且要帶一個必填的布爾型參數
    t1.setName('線程1')   #設置線程的名字
    for x in threadl:
        print('線程爲:',x.getName())   #獲取線程的名字
        print('線程t1是否活動:',t1.is_alive())    #判斷線t1是否處於活動狀態
        x.start()
    print('正在運行線程的數量爲:',threading.active_count())   #獲取正處於活動狀態線程的數量
    print('正在運行線程的數量爲:',threading.activeCount)       #獲取正處於活動狀態線程的數量
    print('正在運行線程的list爲:',threading.enumerate())     #獲取正處於活動狀態線程的list
    print('正在運行線程的list爲:',threading._enumerate())   #獲取正處於活動狀態線程的list
    #將子線程t1阻塞主線程,只有當該子線程完成後主線程才能往下執行
    print('正在運行的線程爲:',threading.current_thread().getName()) #獲取當前線程的名字
    print('Ending now %s'%time.ctime())

九、GIL:cpython解釋器的’BUG’
首先需要明確的一點是GIL並不是Python的特性,它是在實現Python解析器(CPython)時所引入的一個概念。在其中的JPython就沒有GIL。然而因爲CPython是大部分環境下默認的Python執行環境。所以在很多人的概念裏CPython就是Python,也就想當然的把GIL歸結爲Python語言的缺陷。所以這裏要先明確一點:GIL並不是Python的特性,Python完全可以不依賴於GIL

給大家推薦一個python學習交流裙913****066###266

GIL:global interpreter lock,全局解釋器鎖。原文:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
也就是說:無論有多少個CPU,開啓多少線程,每次只能執行一個線程。
基於此設計原理上,我們會覺得python的多線程其實完全沒有用,如下圖不開多線程執行的時間:在這裏插入圖片描述
如下圖開啓多線程執行的時間:在這裏插入圖片描述
好吧,前者是0.3秒,後者是20秒,這個結果是不是無法接受…
該部分代碼塊

:mport threading
import time
def add(n):
    sum=0
    for x in range(1,n+1):
        sum+=x
    print('sum =',sum)
def accumulate(n):
    mul=1
    for x in range(1,n+1):
        mul*=x
    print('mul =',mul)
if __name__ == '__main__':
    thread=[]
    t1=threading.Thread(target=add,args=(10000001,))
    t2=threading.Thread(target=accumulate,args=(100001,))
    thread.append(t1)
    thread.append(t2)
    starttime=time.time()
    for i in thread:
        i.start()
    for i in thread:
        i.join()
    # add(1000001)
    # accumulate(10001)
    endtime = time.time()
    print('spendtime:', endtime-starttime)

GIL原理:在這裏插入圖片描述
前者是單線程,任務串行,執行完add函數後再執行accumulate函數,不用進行線程間的切換。而在後者中,線程add和線程accumulate及主線程三者需要不斷的切換來執行,其中切換線程需要消耗大量時間和資源。所以,我們看到是後者的時間是前者的7倍左右。
但是,我們在上面的music和game線程中卻發現多線程能大大的節省時間,提高效率,那又是爲什麼呢?其實,主要要看任務的類型,我們把任務分爲I/O密集型和計算密集型,而多線程在切換中又分爲I/O切換和時間切換。如果任務屬於是I/O密集型,若不採用多線程,我們在進行I/O操作時,勢必要等待前面一個I/O任務完成後面的I/O任務才能進行,在這個等待的過程中,CPU處於等待狀態,這時如果採用多線程的話,剛好可以切換到進行另一個I/O任務。這樣就剛好可以充分利用CPU避免CPU處於閒置狀態,提高效率。但是如果多線程任務都是計算型,CPU會一直在進行工作,直到一定的時間後採取多線程時間切換的方式進行切換線程,此時CPU一直處於工作狀態,此種情況下並不能提高性能,相反在切換多線程任務時,可能還會造成時間和資源的浪費,導致效能下降。這就是造成上面兩種多線程結果不能的解釋。
結論:I/O密集型任務,建議採取多線程,還可以採用多進程+協程的方式(例如:爬蟲多采用多線程處理爬取的數據);對於計算密集型任務,python此時就不適用了。
十、線程同步鎖
1.爲什麼需要同步鎖
看下面例子,我們自定義一個減1的函數,初始賦值100,使用多線程,開啓100個線程,那麼期望的結果是最終結果爲0,看下圖:在這裏插入圖片描述
該部分代碼塊爲:

#進程鎖
import threading
import time

def subtraction():
    global sum
    tmp=sum
    time.sleep(0.001)
    sum=tmp-1

sum=100
if __name__ == '__main__':
    thread=[]
    for x in range(100):
        t=threading.Thread(target=subtraction)
        thread.append(t)
        t.start()
    for x in thread:
        t.join()
    print('sum = ',sum)

上面現象產生的原因爲:我們在開啓100個線程的時候,當100個線程在進行subtraction函數操作時,首先要獲取各自的sum(漏洞:共同的數據不能共享同時被多線程操作)和tmp,但是此時多線程會按照時間規則來進行切換,如果當前面某些線程在處理sum時未結束,後面的進程已經開始了(上面例子中的代碼增加了休眠時間來體現該效果),此時拿到的sum就不再是sum-1的期望結果了,而是拿到了sum的值。這樣就會導致,此次的線程進行自減1的操作失效了。So,就會導致上圖的現象了,下面就講述該如何通過加同步鎖來解決該問題。
**

給大家推薦一個python學習交流裙913****066###266

**
2.增加同步鎖進行處理共同數據
如下圖在這裏插入圖片描述
該部分代碼塊如下:

#進程鎖
import threading
import time
l=threading.Lock()
def subtraction():
    global sum
    l.acquire()
    tmp=sum
    time.sleep(0.001)
    sum=tmp-1
    l.release()
sum=100
if __name__ == '__main__':
    thread=[]
    for x in range(100):
        t=threading.Thread(target=subtraction)
        thread.append(t)
        t.start()
    for x in thread:
        t.join()
            print('sum = ',sum)

難點:2.1何處加鎖?何處釋放鎖?簡單的原則就是:需要在引起多線程相互矛盾的共同數據部分枷鎖,例如上面例子中的sum多個線程都要使用且後面線程期望使用的應該是前面線程減1的結果;還有在數據庫操作時,使用自增主鍵時,也要對插入的數據進行加鎖,否則將可能會導致主鍵重複。
2.2加鎖的部分代碼相當於是單線程串行運行了。
3.進程死鎖
在線程間共享多個資源的時候,如果分別佔有一部分資源並且同時在等待對方的資源,就會造成死鎖。例如;數據庫操作時A線程需要B線程的結果進行操作,B線程的需要A線程的結果進行操作,當A,B線程同時在進行操作還沒有結果出來時,此時A,B線程將會一直處於等待對方結束的狀態。
現象如下圖:在這裏插入圖片描述
該部分代碼塊如下:

#死鎖
import threading
lockA = threading.Lock()
lockB = threading.Lock()
class Mythread(threading.Thread):
    '自定義線程類'
    def actionA(self):
        'actionA函數中運行actionB函數,運行actionB函數前加鎖,運行actionB函數結束後釋放鎖'
        lockA.acquire()
        print(self.name,'運行actionA')
        self.actionB()
        lockA.release()
    def actionB(self):
        'actionB函數中運行actionA函數,運行actionA函數前加鎖,運行actionA函數結束後釋放鎖'
        lockB.acquire()
        print(self.name,'運行actionB')
        self.actionA()
        lockB.release()
    def run(self):
        '運行函數'
        self.actionA()
        self.actionB()
if __name__ == '__main__':
    thread = []
    for x in range(3):
        t = Mythread()
        thread.append(t)
        print('以啓動線程:', t.getName())
        t.start()
    for t in thread:
        t.join()
    print('ending......')

十一、多線程利器-隊列(squeue)
場景:定義一個函數用例刪除列表中最後一個元素,使用多線程來刪除一個列表中的數據,現象如下圖所示:
在這裏插入圖片描述
該部分的代碼塊如下:

import threading,time
l=[1,3,4,6,8]
def pop(l):
    a=l[-1]
    print(a)
    time.sleep(0.001)
    l.remove(a)
if __name__ == '__main__':
    th=[]
    for x in range(3):
        t = threading.Thread(target=pop, args=(l,))
        th.append(t)
        print(t.getName())
        t.start()
    for x in th:
        x.join()
    # pop(l)
    print('l = ',l)

此處由於多線程在操作時可能拿到相同的最後一個元素值,此時若前者的線程已經刪除了該元素,則後面線程的函數則無法刪除該元素(remove是按元素來進行刪除的)。爲了解決此次共享數據導致的多線程問題,我們可以利用前面的進程同步鎖來處理,我們可以在獲取和刪除數據的時候加鎖,代碼如下

:mport threading,time
lock = threading.Lock()
l=[1,3,4,6,8]
def pop(l):
    # lock.acquire()
    a=l[-1]
    print(a)
    time.sleep(0.001)
    l.remove(a)
    # lock.release()
if __name__ == '__main__':
    th=[]
    for x in range(3):
        t = threading.Thread(target=pop, args=(l,))
        th.append(t)
        print(t.getName())
        t.start()
    for x in th:
        x.join()
    # pop(l)
    print('l = ',l)
 在該部分,我們引入新的模塊queue(線程隊列)來解決該問題,如下圖所示:
    ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9zMS41MWN0by5jb20vaW1hZ2VzL2Jsb2cvMjAxODA5LzE2L2ZlMDM2MWUzOTFiYmU5YWMzODI2NDdjYjk1ODM0YWVhLnBuZw?x-oss-process=image/format,png)
    該部分代碼塊如下:
   

 import threading,time
import queue    #線程隊列
l=[1,3,4,6,8]
def pop(l):
    a=l[-1]
    print('a = ',a)
    time.sleep(0.001)
    l.remove(a)
if __name__ == '__main__':
    q = queue.Queue()
    for x in range(3):
        t = threading.Thread(target=pop, args=(l,))
        q.put(t)
    while not q.empty():
        data = q.get()
        print('當前執行的線程:', data.getName())
        data.run()
    print('l = ',l)

Queue線程隊列存放數據的三種方式:
1.1先進先出(FIFO)
q=queue.Queue()
q.put(maxsize)
1.2先進後出(LIFO)
q=queue.LifoQueue()
q.put(maxsize)
1.3按照優先級進出
q = queue.PriorityQueue()
q.put(list) #以長度爲2的list存數據,第一個元素表示優先級,第二個元素表示存放的對應的值
代碼塊如下:

import queue    #線程隊列
num=5
#num用例限制隊列中插入元素的個數,可不填
q1 = queue.Queue(num)   #三種存取數據的順序,1.先進先出(FIFO,不指明方式則默認該方式);2.先進後出(LIFO);3.按優先級進出()
q1.put(123)
q1.put('you')
q1.put({'name':'zhangzhou'})

q2 = queue.LifoQueue(num)
q2.put(123)
q2.put('you')
q2.put({'name':'zhangzhou'})

q3 = queue.PriorityQueue()
q3.put([2,123])
q3.put([3,'you'])
q3.put([1,{'name':'zhangzhou'}])

if __name__ == '__main__':
    while not q1.empty():
        data = q1.get()
        print('------------',data,'------------')
    while not q2.empty():
        data = q2.get()
        print('------------',data,'------------')
    while not q3.empty():
        data = q3.get()
        print('------Priority=',data[0],'value=',data[1],'-------')

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章