1. 程式人生 > >Python多工之執行緒

Python多工之執行緒

多工介紹

我們先來看一下沒有多工的程式

import time


def sing():
    for i in range(5):
        print("我喜歡唱")
        time.sleep(1)


def dance():
    for i in range(5):
        print("我喜歡跳")
        time.sleep(1)


def main():
    sing()
    dance()
    pass


if __name__ == "__main__":
    main()
沒有多工的程式

執行結果:花了十秒鐘多,只能按順序執行,無法一起/同步執行

我喜歡唱
我喜歡唱
我喜歡唱
我喜歡唱
我喜歡唱
我喜歡跳
我喜歡跳
我喜歡跳
我喜歡跳
我喜歡跳

 

我們再來看一下使用了多執行緒的程式

import time
import threading


def sing():
    for i in range(5):
        print("我喜歡唱歌")
        time.sleep(1)


def dance():
    for i in range(5):
        print("我喜歡跳舞")
        time.sleep(1)


def main():
    t1 = threading.Thread(target=sing)
    t2 = threading.Thread(target=dance)
    t1.start()
    t2.start()


if __name__ == '__main__':
    main()
使用執行緒的多工

執行結果:花了五秒多一點,程式碼同步執行

我喜歡唱歌
我喜歡跳舞
我喜歡跳舞我喜歡唱歌

我喜歡跳舞
我喜歡唱歌
我喜歡跳舞
我喜歡唱歌
我喜歡跳舞
我喜歡唱歌

 

多工

在這裡我們可以由多工額外擴充套件一些知識,電腦是怎麼執行程式的?

單核cpu的執行原理:時間片輪轉

單核cpu同一時間只能執行一個程式,但你看到的能執行很多程式是因為單核cpu的快速切換,即把一個程式拿過來執行極短的時間比如0.00001秒,就換執行下一個程式,如此往復,就是你看到的同一時間執行多個程式。這是作業系統實現多工的一種方式,但其實是偽多工。

時間片輪轉的理念是,只要我切換的夠快,你看到的就是我同時做多件事情,這是作業系統的排程演算法。作業系統還有優先順序排程,比如聽歌要一直持續。

  • 如果是多核cpu同時執行多個任務,我們就稱之為並行,是真的多工;任務數少於cpu數量;
  • 如果是單核cpu切換著執行多個任務,我們就稱之為併發,是假的多工。任務數多於cpu數量;
  • 但因為日常中,任務數一般多於cpu核數,所以我們說的多工一般都是併發,即假的多工;

 

Thread多執行緒

在前面我們已經看過了執行緒實現多工,接下來我們學習執行緒的使用方法;

通過Thread(target=xxx)建立多執行緒

執行緒的使用步驟如下:

  1. 匯入threading模組;
  2. 編寫多工所需要的的函式;
  3. 建立threading.Thread類的例項物件並傳入函式引用;
  4. 呼叫例項物件的start方法,建立子執行緒。

如果你還不懂怎麼使用多執行緒?沒關係,看下面這個圖就知道了

程式碼如下:

import time
import threading


def sing():
    for i in range(5):
        print("我喜歡唱歌")
        time.sleep(1)


def dance():
    for i in range(5):
        print("我喜歡跳舞")
        time.sleep(1)


def main():
    t1 = threading.Thread(target=sing)
    t2 = threading.Thread(target=dance)
    t1.start()
    t2.start()


if __name__ == '__main__':
    main()
多執行緒的使用

注意:

  • 函式名() 表示函式的呼叫
  • 函式名 表示使用對函式的引用,告訴函式在哪;

 

程式碼解讀

def main():
    t1 = threading.Thread(target=sing)
    t2 = threading.Thread(target=dance)
    t1.start()
    t2.start()

每個函式在執行時都會有一個執行緒,我們稱之為主執行緒;
當我們執行到

t1 = threading.Thread(target=sing)

時,表示建立了一個Thread類的例項物件t1,並且給t1的Thread類中傳入了sing函式的引用。
同理,t2也是如此;
當我們執行到

t1.start()

時,這個例項物件就會建立一個子執行緒,去呼叫sing函式;然後主執行緒往下走,子執行緒去呼叫sing函式。
當主執行緒走到t2.start()時,再次建立一個子執行緒,子執行緒去呼叫dance函式,因為後面沒有程式碼了,然後主執行緒就會等待所有子執行緒的完成,再結束程式/主執行緒;可以理解為主執行緒要給子執行緒死了之後收屍,然後主執行緒再去死。

主執行緒要等子執行緒執行結束的原因:子執行緒在執行過程中會呼叫資源以及產生一些變數等,當子執行緒執行完之後,
主執行緒要將這些無用的資源及垃圾進行清理工作。

 

多執行緒建立執行理解

我們可以使用如下程式碼獲取當前程式中的所有執行緒;

threading.enumerate()

關於enumerate的使用,可以檢視我的上一篇部落格 python內建函式之enumerate函式 ,但這裡表示的是獲取當前程式中的所有執行緒,可以不必看;

 

讓某些執行緒先執行

因為 執行緒建立完後,執行緒的執行順序是不確定的,如果我們想要讓某個執行緒先執行,可以採用time.sleep的方法。程式碼如下

import time
import threading


def sing():
    for i in range(5):
        print("-----sing----%d" % i)


def dance():
    for i in range(5):
        print("-----dance----%d" % i)


def main():
    t1 = threading.Thread(target=sing)
    t2 = threading.Thread(target=dance)
    t1.start()
    time.sleep(1)
    print("sing")

    t2.start()
    time.sleep(1)
    print("dance")

    print(threading.enumerate())


if __name__ == '__main__':
    main()
讓某些執行緒先執行

執行結果

我們可以看到,sing執行緒已經先運行了,但是此時檢視的執行緒只有一個主執行緒,這是因為當子執行緒執行完了才執行到檢視執行緒的程式碼。

 

迴圈檢視當前執行的執行緒數

我們可以通過讓子執行緒延時執行多次,主執行緒死迴圈檢視當前執行緒數(適當延時),即可看到當前執行的執行緒數量,當執行緒數量小於等於1時,使用break結束主執行緒。

程式碼如下

import time
import threading


def sing():
    for i in range(5):
        print("-----sing--%d--" % i)
        time.sleep(1)


def dance():
    for i in range(5):
        print("-----dance--%d--" % i)
        time.sleep(1)


def main():
    t1 = threading.Thread(target=sing)
    t2 = threading.Thread(target=dance)
    t1.start()
    t2.start()
    while True:
        t_len = len(threading.enumerate())
        # print("當前執行執行緒數:%s" % t_len)
        print(threading.enumerate())
        if t_len <= 1:
            break
        time.sleep(1)


if __name__ == '__main__':
    main()
迴圈檢視當前執行的執行緒數

執行結果

可以看到,剛開始的時候只有一個是主執行緒,當子執行緒開始後,有三個執行緒,在sing子執行緒結束後,只剩兩個執行緒了,dance結束後,只有一個主執行緒。

 

驗證子執行緒的執行時間

為了驗證子執行緒的執行時間,我們可以在互動式python下執行程式碼,子執行緒呼叫的函式在何時執行即代表子執行緒在何時執行;

驗證結果如下

據此,我們可以判斷子執行緒的執行是線上程的示例物件呼叫start()方法之後執行的。

驗證程式碼

import threading
def sing():
    print("-----sing-----")
t1 = threading.Thread(target=sing)
t1.start()
-----sing-----

 

驗證子執行緒的建立時間

驗證原理:我們可以通過計算執行緒數在各個時間段的數量來判斷子執行緒的建立時間

驗證程式碼

import time
import threading


def sing():
    for i in range(5):
        print("----sing----")
        time.sleep(1)


def main():
    print("建立例項物件之前的執行緒數:", len(threading.enumerate()))
    t1 = threading.Thread(target=sing)
    print("建立例項物件之後/start方法之前的執行緒數:", len(threading.enumerate()))
    t1.start()
    print("呼叫start方法之後的執行緒數:", len(threading.enumerate()))


if __name__ == '__main__':
    main()
驗證子執行緒的建立時間

驗證結果

可以觀察到在呼叫start方法之前執行緒數一直都是1個主執行緒,由此我們可以判斷執行緒的建立時間是在呼叫了例項物件的start方法之後;

結合前面,我們可以得出結論,子執行緒的建立時間和執行時間是在Thread創建出來的例項物件呼叫了start方法之後,而子執行緒的結束時間是在呼叫的函式執行完成後。

 

通過繼承Thread類來建立程序

前面我們是通過子執行緒呼叫一個函式,那麼當函式過多時,想將那些函式封裝成一個類,我們可以不可以通過子執行緒呼叫一個類呢?

建立執行緒的第二種方法步驟

  1. 匯入threading模組;
  2. 定義一個類,類裡面繼承threading.Thread類,裡面定義一個run方法;
  3. 然後建立這個類的例項物件;
  4. 呼叫例項物件的start方法,就建立了一個執行緒。

如果你建立一個執行緒的時候是通過 類繼承一個Thread類來建立的,必須在裡面定義run方法,當你呼叫start方法的時候,會自動呼叫run方法,接下來執行緒執行的就是run方法裡面的程式碼。

通過繼承Thread類來建立程序示例程式碼

import time
import threading


class TestThread(threading.Thread):
    def run(self):
        print("---run---")
        for i in range(3):
            msg = "我是%s,i--->%s" % (self.name, str(i))  # self.name中儲存的是當前執行緒的名字
            print(msg)
            time.sleep(1)


def main():
    t1 = TestThread()
    t1.start()


if __name__ == '__main__':
    main()
通過繼承Thread類來建立程序

執行結果

---run---
我是Thread-1,i--->0
我是Thread-1,i--->1
我是Thread-1,i--->2

知識點

  • 這種方法適用於一個執行緒裡面要做的事情比較複雜,要封裝成幾個函式來做,那麼我們就將它封裝成一個類。
  • 在類中定義其他的幾個函式,可以在run裡面進行呼叫這幾個函式。
  • 建立執行緒時使用哪種方法比較好?哪個簡單使用哪個。

注意:

一個例項物件只能建立一個執行緒;
通過繼承Thread類來建立程序時,不會自動呼叫類中除run函式的其他函式,如果想要呼叫其他可數,可以在run方法中使用self.xxx()來呼叫。

多執行緒共享變數

在函式中修改全域性變數,如果是數字等不可變型別,要用global宣告之後才能修改,如果是列表等可變型別,就可以不用宣告,直接append等對列表內容進行修改,但,如果不是對列表內容進行修改,而是指向一個新的列表,就需要使用global宣告;

在全域性變數中,如果是對引用的資料進行修改,那麼不需要使用global,如果是對全域性變數的引用進行修改(直接換一個引用地址),那麼就需要使用global,同時,我們也應注意全域性變數是可變型別還是不可變型別,比如數字,不可變,就只能通過修改變數的引用來進行修改全域性變量了,所以需要global;

 

驗證多執行緒中共享全域性變數

驗證原理:

定義一個全域性變數,在函式1中加1,在函式2中檢視,讓執行緒控制的函式1先執行,如果執行緒函式2的檢視結果和函式1的檢視結果一樣,那麼就證明多執行緒之間共享全域性變數。

程式碼驗證

import time
import threading


g_num = 100


def sing():
    global g_num
    g_num += 1
    print("---sing中的g_num: %d---" % g_num)
    time.sleep(1)


def dance():
    print("---dance中的g_num: %d---" % g_num)
    time.sleep(1)


def main():
    t1 = threading.Thread(target=sing)
    t2 = threading.Thread(target=dance)
    t1.start()
    t2.start()
    print("---主執行緒中的g_num: %d---" % g_num)


if __name__ == '__main__':
    main()
多執行緒之間共享全域性變數驗證

執行結果

---sing中的g_num: 101---
---dance中的g_num: 101---
---主執行緒中的g_num: 101---

如上程式碼我們可知,多執行緒之間共享全域性變數。

 

我們可以將多執行緒之間共享去全域性變數理解為:
一個房子裡面有幾個人,一個人就是一個執行緒,每個人有自己私有的東西資源,但在這個大房子裡面,也有些共有的東西,比如說唯一一臺飲水機的水,有一個人喝了一半,拿下一個人來接水,也只剩下一半了,這個飲水機裡面的誰就是全域性變數。

 

多執行緒給子執行緒傳參

給子執行緒傳引數語法如下

g_nums = [11, 22]

t1 = threading.Thread(target=sing, args=(g_num,))

給子執行緒傳參示例程式碼

import time
import threading


def sing(temp):
    temp.append(33)
    print("---sing中的g_nums: %s---" % str(temp))
    time.sleep(1)


def dance(temp):
    print("---dance中的g_nums: %s---" % str(temp))
    time.sleep(1)


g_nums = [11, 22]


def main():
    t1 = threading.Thread(target=sing, args=(g_nums,))
    t2 = threading.Thread(target=dance, args=(g_nums,))
    t1.start()
    time.sleep(1)
    t2.start()
    time.sleep(1)
    print("---主執行緒中的g_nums: %s---" % str(g_nums))


if __name__ == '__main__':
    main()
給子執行緒傳引數

執行結果

---sing中的g_nums: [11, 22, 33]---
---dance中的g_nums: [11, 22, 33]---
---主執行緒中的g_nums: [11, 22, 33]---

 

多執行緒之間共享問題:資源競爭

共享全域性變數存在資源競爭的問題,兩個執行緒同時使用或者修改就會存在問題,一個修改一個使用不會存在;
傳參100的時候可能不會出現問題,因為數字較小,概率也小點;但傳參1000000的時候,數字變大,概率也變大;

num += 1可以分解為三句,獲取num的值,給值加1,給num重賦值;有可能當執行緒1執行12句,正打算執行3句的時候,cpu就將資源給了執行緒2,而執行緒2同理,然後又執行執行緒1的第3句,因此執行緒1 +1,儲存全域性變數為1;輪到執行緒2 +1,儲存全域性變數也為1;問題就出現了,本來加兩次應該是2的,但全域性變數還是1。

資源競爭程式碼示例

import time
import threading


g_num = 0


def add1(count):
    global g_num
    for i in range(count):
        g_num += 1
    print("the g_num of add1:", g_num)


def add2(count):
    global g_num
    for i in range(count):
        g_num += 1
    print("the g_num of add2:", g_num)


def main():
    t1 = threading.Thread(target=add1, args=(1000000,))
    t2 = threading.Thread(target=add2, args=(1000000,))

    t1.start()
    t2.start()
    time.sleep(3)
    print("the g_num of main:", g_num)


if __name__ == '__main__':
    main()
共享變數的資源競爭問題

執行結果

the g_num of add1: 1096322
the g_num of add2: 1294601
the g_num of main: 1294601

 

互斥鎖解決資源競爭問題

原子性操作:要麼不做,要麼做完;

互斥鎖:一個人做某事的時候,別人不允許做這件事,必須需得等到前面的人做完了這件事,才能接著做,例子景點上廁所。

互斥鎖語法

# 建立鎖:
mutex = threading.Lock()
# 上鎖:
mutex.acquire()
# 解鎖:
mutex.release()

使用互斥鎖解決資源競爭問題

import time
import threading


g_num = 0


def add1(num):
    global g_num
    for i in range(num):
        mutex.acquire()
        g_num += 1
        mutex.release()
    print("the g_num of add1:", g_num)


def add2(num):
    global g_num
    for i in range(num):
        mutex.acquire()
        g_num += 1
        mutex.release()
    print("the g_num of add2:", g_num)


mutex = threading.Lock()


def main():
    t1 = threading.Thread(target=add1, args=(1000000,))
    t2 = threading.Thread(target=add2, args=(1000000,))
    t1.start()
    t2.start()

    time.sleep(2)
    print("the g_num of main:", g_num)


if __name__ == '__main__':
    main()
使用互斥鎖解決資源競爭的問題

執行結果

the g_num of add2: 1901141
the g_num of add1: 2000000
the g_num of main: 2000000

可以看出,使用互斥鎖可以解決資源競爭的問題。

 

死鎖問題

使用互斥鎖特別是多個互斥鎖的時候,特別容易產生死鎖,就是你在等我的資源,我在等你的資源;

 

本章內容總結

執行緒的生命週期

  • 從程式開始執行到結束,一直都有一條主執行緒
  • 如果主執行緒先死了,那麼正在執行的子執行緒也會死。
  • 子執行緒開始建立是在呼叫t.start()時,而不是建立Thread的例項化物件時。
  • 子執行緒的開始執行是在呼叫t.start()時;
  • 子執行緒的死亡時間是在子執行緒呼叫的函式執行完成後;
  • 執行緒建立完後,執行緒的執行順序是不確定的;
  • 如果想要讓某個執行緒先執行,可以採用time.sleep的方法。

 

建立多執行緒的兩種方式

通過Thread(target=xxx)建立多執行緒

  1. 匯入threading模組;
  2. 編寫多工所需要的的函式;
  3. 建立threading.Thread類的例項物件並傳入函式引用;
  4. 呼叫例項物件的start方法,建立子執行緒。

通過繼承Thread類來建立程序

  1. 匯入threading模組;
  2. 定義一個類,類裡面繼承threading.Thread類,裡面定義一個run方法;
  3. 然後建立這個類的例項物件;
  4. 呼叫例項物件的start方法,就建立了一個執行緒。

 

多執行緒理解

  • 建立多執行緒可以理解為建立執行緒做準備;
  • start() 則是準備好後直接建立並執行執行緒;
  • 主執行緒要等子執行緒結束後在結束是為了清理子執行緒中可能產生的垃圾;

 

多執行緒共享全域性變數

  • 子執行緒和子執行緒之間共享全域性變數;
  • 給子執行緒傳參可以使用  threading.Thread(target=sing, args=(g_num,)) 進行傳參;
  • 多執行緒之間可能存在資源競爭的問題;
  • 可以使用互斥鎖解決資源競爭的問題;

&n