前提
- 我是參考 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 = Thread(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 的多核特性,應該使用多進程+協程的方式
待更新