python線程及多線程實例講解

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

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()函數的用法)
例如:在實際中,須要子線程在插入數據,主線程須要等待數據插入結束後才能進行查詢驗證操做(測試驗證數據)
python線程及多線程實例講解
該部分代碼塊爲:數據庫

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())

6、線程守護(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以前且要帶一個必填的布爾型參數
7、自定義的方式來產生多線程
python線程及多線程實例講解
該部分代碼塊爲:app

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())

8、Threading的其餘經常使用方法
getName() :獲取線程名稱
setName():設置線程名稱
run():用以表示線程活動的方法(見七中自定義線程的run方法)
rtart():啓動線程活動
is_alive():表示線程是否處於活動的狀態,結果爲布爾值;
threading.active_count():返回正在運行線程的數量
Threading.enumerate():返回正在運行線程的列表
python線程及多線程實例講解
該部分代碼塊爲;ide

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())

9、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此時就不適用了。
10、線程同步鎖
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線程及多線程實例講解
該部分代碼塊以下:ui

#死鎖
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......')

11、多線程利器-隊列(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(線程隊列)來解決該問題,以下圖所示:
    ![](https://s4.51cto.com/images/blog/201809/16/fe0361e391bbe9ac382647cb95834aea.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
    該部分代碼塊以下:
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],'-------')
相關文章
相關標籤/搜索