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,)) 進行傳參;
  • 多線程之間可能存在資源競爭的問題;
  • 可以使用互斥鎖解決資源競爭的問題;

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM