Python 多執行緒多程式

小菠蘿測試筆記發表於2021-03-26

前提

  • 我是參考 Github Python 100 天的文章寫的,再結合自己的小練習,總結
  • 最近在面大廠,發現許多大廠都會問 Python 的多執行緒、多程式,所以我覺得很有必要總結學習下

 

什麼是程式

作業系統中執行的一個程式,類似微信、QQ,每個程式都是一個程式
在這裡插入圖片描述

概念

  • 它是 CPU 最小資源分配單元
  • 作業系統會給程式分配記憶體空間,每個程式都會有自己的地址空間、資料棧以及其他用於跟蹤程式執行的輔助資料
  • 作業系統管理所有程式的執行,為它們合理的分配資源

 

fork、spawn

  • 程式可以通過 fork、spawn 的方式來建立新的程式來執行其他任務
  • 不過新的程式有自己獨立的記憶體空間、資料棧
  • 因此不同程式間需要通過通訊機制(IPC)來實現資料共享
 

常見通訊機制

  • 管道
  • 訊號
  • 套接字
  • 共享記憶體區

 

什麼是執行緒

  • 程式中可以擁有多個併發的執行線索
  • 它是 CPU 最小排程的執行單元

 

特點

  • 同一個程式下的執行緒共享相同的上下文
  • 相對於程式來說,執行緒間的資訊共享和通訊更加容易

 

單核 CPU 系統注意事項

  • 真正的併發是不可能的
  • 因為在某個時刻,CPU 只能執行唯一的一個執行緒
  • 多個執行緒共享了 CPU 的執行時間

 

多執行緒的好處

  • 提升程式的效能和改善使用者體驗
  • 今天日常使用的軟體幾乎都用到了多執行緒

 

多執行緒的壞處

  • 站在其他程式的角度,多執行緒的程式對其他程式並不友好,因為它佔用了更多的 CPU 執行時間,導致其他程式無法獲得足夠的 CPU 執行時間
  • 編寫和除錯多執行緒的程式對開發者要求較高

 

Python 實現併發程式設計的方式

  • 多程式
  • 多執行緒
  • 多程式+多執行緒

 

Python 中的多程式

Linux 下的 fork 函式

  • Linux 作業系統上提供了 fork() 系統呼叫來建立程式
  • 呼叫 fork() 函式的是父程式
  • 建立的是子程式
  • 子程式是父程式的拷貝
  • 但子程式有自己的 PID
  •  fork() 函式非常特殊,它會返回兩次,父程式中呼叫 fork() 會返回子程式的 PID,子程式中呼叫 fork() 得到的都是0

 

Python 提供的 fork 函式

os 模組提供了 fork() 

 

Window 下沒有fork()的呼叫

  • 實現跨平臺的多程式變成,可以使用 multiprocessing 模組的 Process 類來建立子程式
  • 還提供了更高階的封裝,例如批量啟動程式的程式池 pool、用於程式間同喜你的佇列 Queue 和管道 Pipe

 

使用多程式和不使用多程式的區別(寫程式碼)

不使用多程式

from random import randint
from time import time, sleep

def download_task(filename):
    print('開始下載%s...' % filename)
    time_to_download = randint(5, 10)
    sleep(time_to_download)
    print('%s下載完成! 耗費了%d秒' % (filename, time_to_download))


def main():
    start = time()
    download_task('Python從入門到住院.pdf')
    download_task('Peking Hot.avi')
    end = time()
    print('總共耗費了%.2f秒.' % (end - start))

if __name__ == '__main__':
    main()

 

執行結果

開始下載Python從入門到住院.pdf...
Python從入門到住院.pdf下載完成! 耗費了10秒
開始下載Peking Hot.avi...
Peking Hot.avi下載完成! 耗費了9秒
總共耗費了19.02秒.

可以看到需要先等第一個檔案下載完才能下載第二個檔案,效率很低

 

使用多程式

from random import randint
from time import time, sleep
from multiprocessing import Process

def download_task(filename):
    print('開始下載%s...' % filename)
    time_to_download = randint(5, 10)
    sleep(time_to_download)
    print('%s下載完成! 耗費了%d秒' % (filename, time_to_download))

def main2():
    start = time()
    p1 = Process(target=download_task,args=("Python從入門到住院.pdf",))
    p1.start()
    p2 = Process(target=download_task, args=("Peking Hot.avi",))
    p2.start()
    p1.join()
    p2.join()
    end = time()
    print('總共耗費了%.2f秒.' % (end - start))


if __name__ == '__main__':
    main2()

 

執行結果

開始下載Python從入門到住院.pdf...
開始下載Peking Hot.avi...
Python從入門到住院.pdf下載完成! 耗費了6秒
Peking Hot.avi下載完成! 耗費了10秒
總共耗費了10.17秒.

兩個任務同時執行,總耗時不再是兩個任務的時間總和

 

知識點

  • Process:通過 Process 類建立程式物件
  • target:通過 target 引數傳入一個函式名來表示程式啟動後要執行的程式碼
  • args:是一個元組,代表傳遞給函式的引數列表
  • start:Process 的 start() 方法來啟動程式
  • join:Process 的 join() 方法表示等待程式執行結束,才會往下執行

 

Python 中的多執行緒

前言

推薦 threading 模組來實現多執行緒程式設計,它提供了更好的物件導向封裝

 

多執行緒的實現方式

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
__title__  = 
__Time__   = 2021/3/19 18:17
__Author__ = 小菠蘿測試筆記
__Blog__   = https://www.cnblogs.com/poloyy/
"""

from random import randint
from threading import Thread
from time import time, sleep

def download_task(filename):
    print('開始下載%s...' % filename)
    time_to_download = randint(5, 10)
    sleep(time_to_download)
    print('%s下載完成! 耗費了%d秒' % (filename, time_to_download))


def main3():
    start = time()
    p1 = Thread(target=download_task,args=("Python從入門到住院.pdf",))
    p1.start()
    p2 = Process(target=download_task, args=("Peking Hot.avi",))
    p2.start()
    p1.join()
    p2.join()
    end = time()
    print('總共耗費了%.2f秒.' % (end - start))


if __name__ == '__main__':
    main3()

 

執行結果

開始下載Python從入門到住院.pdf...
開始下載Peking Hot.avi...
Peking Hot.avi下載完成! 耗費了6秒
Python從入門到住院.pdf下載完成! 耗費了8秒
總共耗費了8.01秒.

 一樣執行效率高很多

 

自定義執行緒類

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
__title__  = 
__Time__   = 2021/3/19 18:17
__Author__ = 小菠蘿測試筆記
__Blog__   = https://www.cnblogs.com/poloyy/
"""

from random import randint
from threading import Thread
from time import time, sleep

class downLoadTask(Thread):
    def __init__(self,filename):
        super().__init__()
        self.filename = filename

    def run(self) -> None:
        print('開始下載%s...' % self.filename)
        time_to_download = randint(5, 10)
        sleep(time_to_download)
        print('%s下載完成! 耗費了%d秒' % (self.filename, time_to_download))

def main3():
    start = time()
    p1 = downLoadTask("Python從入門到住院.pdf")
    p2 = downLoadTask("Peking Hot.avi")
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    end = time()
    print('總共耗費了%.2f秒.' % (end - start))

if __name__ == '__main__':
    main3()

 

執行結果

開始下載Python從入門到住院.pdf...
開始下載Peking Hot.avi...
Peking Hot.avi下載完成! 耗費了6秒
Python從入門到住院.pdf下載完成! 耗費了9秒
總共耗費了9.00秒.

也是一樣的高效執行

 

重點知識:start 和 run 方法的區別

比較點 start run
作用 啟動執行緒,獲取 CPU 時間片 執行執行緒指定的程式碼塊
執行緒狀態 可執行狀態 執行狀態
呼叫次數 一個執行緒只能呼叫一次 可以重複呼叫
執行執行緒 建立了一個子執行緒,執行緒名是自己命名的 在主執行緒中呼叫了一個普通函式
注意點 想用多執行緒,必須呼叫 start()

 

Python 中的協程

什麼是協程

Python 中,單執行緒+非同步 I/O 的程式設計模型

 

協程的優勢

  • 極高的執行效率
  • 子程式切換不是執行緒切換,而是由程式本身控制,沒有執行緒切換的開銷
  • 不需要多執行緒的所機制,只有一個執行緒,所以不存在同時寫變數衝突,在協程中控制共享資源不用加鎖,只需要判斷狀態就好了,所以執行效率比多執行緒高很多

 

重點

要充分利用 CPU 的多核特性,應該使用多程式+協程的方式

 

待更新

相關文章