進程的概念
進程:一個正在執行的程序
- 計算機程序是存儲在磁盤上的可執行二進制(或其他類型)文件,只有把它們加載到內存中,並被操作系統調用,它們才會擁有其自己的生命周期。
- 進程是表示的一個正在執行的程序。
- 每個進程都擁有自己的地址空間、內存、數據棧以及其他用於跟蹤執行的輔助數據。
- 操作系統負責其上所有進程的執行,並為這些進程合理地分配執行時間。
- 進程之間是獨立的,不能共享彼此的數據。
並行與並發
並行:同時進行多個任務
並發:短時間內,運行多個任務
並發:單CPU,多進程並發
無論是並行還是並發,在用戶看來都是'同時'運行的,不管是進程還是線程,都只是一個任務而已,真實干活的是cpu,cpu來做這些任務,而一個cpu同一時刻只能執行一個任務
一 並發:是偽並行,即看起來是同時運行。單個cpu+多道技術就可以實現並發,(並行也屬於並發)
並行:多CPU(同時運行,只有具有多個cpu才能實現並行)
並行滿足的條件是總進程數量不多於 CPU核心數量!因此,現在PC運行的程序大部分都是輪詢調度產生的並行假象
舉例:比如PC有四個核,六個任務,這樣同一時間有四個任務被執行,假設分別被分配給了cpu1,cpu2,cpu3,cpu4,
一旦任務2遇到I/O就被迫中斷執行,此時任務5就拿到cpu1的時間片去執行,這就是單核下的多道技術
而一旦任務2的I/O結束了,操作系統會重新調用它(需知進程的調度、分配給哪個cpu運行,由操作系統說了算),可能被分配給四個cpu中的任意一個去執行
所以,現代計算機經常會在同一時間做很多件事,一個用戶的PC(無論是單cpu還是多cpu),都可以同時運行多個任務(一個任務可以理解為一個進程)。
多道技術:內存中同時存入多道(多個)程序,cpu從一個進程快速切換到另外一個,使每個進程各自運行幾十或幾百毫秒,這樣,雖然在某一個瞬間,一個cpu只能執行一個任務,但在1秒內,cpu卻可以運行多個進程,這就給人產生了並行的錯覺,即偽並發,以此來區分多處理器操作系統的真正硬件並行(多個cpu共享同一個物理內存)
例子:在python中執行耗時操作
結果:
用進程來分擔耗時任務后:
python進程使用流程
線程的概念
- 線程被稱作輕量級進程。
- 與進程類似,不過它們是在同一個進程下執行的。
- 並且它們會共享相同的上下文。
- 當其他線程運行時,它可以被搶占(中斷)和臨時掛起(也稱為睡眠)
- 線程的輪訓調度機制類似於進程的輪詢調度,只不過這個調度不是由操作系統來負責,而是由Python解釋器來負責。
GIL鎖
Python在設計的時候,還沒有多核處理器的概念。因此,為了設計方便與線程安全,直接設計了一個鎖。這個鎖要求,任何進程中,一次只能有一個線程在執行。
因此,並不能為多個線程分配多個CPU。所以Python中的線程只能實現並發,而不能實現真正的並行。但是Python3中的GIL鎖有一個很棒的設計,在遇到阻塞(不是耗時)的時候,會自動切換線程。
Scrapy、Django、Flask、Web2py ...等框架 都是使用多線程來完成
GIL鎖帶給我們的新認知,遇到阻塞就自動切換。因此我們可以利用這種機制來有效的避開阻塞 ,充分利用CPU
例子:使用線程來避開阻塞任務
python線程使用流程
計算密集型和IO密集型
IO密集型遇到等待時,不消耗CPU,CPU會調度其他程序執行,使用多線程可以有效的進行並發(爬蟲的請求是IO密集型),線程在系統中所占資源較少。
計算密集型:CPU會一直計算,此時使用多線程,反而會耗時更多,此時使用多進程(CPU越多,性能越好)。
使用多進程/多線程實現並發服務器:
import socket from multiprocessing import Process from threading import Thread def read(conn,addr): while True: data = conn.recv(1024) if data: print('客戶端{}的消息:{}'.format(addr,data.decode())) conn.send(data) else: conn.close() print('客戶端{}斷開連接'.format(addr)) break if __name__ == '__main__': server = socket.socket() server.bind(('', 9998)) # 注意這些代碼要寫在if...main...判斷下面,因為在Windows中,使用進程的模式類似導入,在判斷外面的代碼會在子進程執行 server.listen(5) while True: print('等待客戶端連接') conn,addr = server.accept() # 有客戶端連接,就往下進行 print('客戶端{}已連接'.format(addr)) # p = Process(target=read,args=(conn,addr)) # 使用多進程,每來一個客戶端連接,就分出一個進程去和它一對一處理 # p.start() t = Thread(target=read,args=(conn,addr)) # 使用多線程,每來一個客戶端連接,就分出一個線程去和它一對一處理 t.start()
補充
fork介紹
Unix/Linux操作系統提供了一個fork()
系統調用,它非常特殊。普通的函數調用,調用一次,返回一次,但是fork()
調用一次,返回兩次,因為操作系統自動把當前進程(稱為父進程)復制了一份(稱為子進程),然后,分別在父進程和子進程內返回。
子進程永遠返回0
,而父進程返回子進程的ID。這樣做的理由是,一個父進程可以fork出很多子進程,所以,父進程要記下每個子進程的ID,而子進程只需要調用getppid()
就可以拿到父進程的ID。
import os if __name__ == '__main__': print('進程 (%s) start...' % os.getpid()) pid = os.fork() # time.sleep(10) if pid == 0: # 若fork()返回0為子進程 print("子進程{},父進程{}".format(os.getpid(), os.getppid())) #getpid()獲取當前進程的pid,getppid()獲取父進程的pid else: # 父進程fork返回子進程的id print("父進程{},子進程{}".format(os.getpid(), pid)) >> 進程 (3130) start... 父進程3130,子進程3131 子進程3131,父進程3130
multiprocessing模塊介紹
python中的多線程無法利用CPU資源,在python中大部分計算密集型任務使用多進程。如果想要充分地使用多核CPU的資源(os.cpu_count()查看)
python中提供了非常好的多進程包multiprocessing。
multiprocessing模塊用來開啟子進程,並在子進程中執行功能(函數),該模塊與多線程模塊threading的編程接口類似。
multiprocessing的功能眾多:支持子進程、通信和共享數據、執行不同形式的同步,提供了Process、Queue、Pipe、Lock等組件。
計算密集型任務:如金融分析,科學計算
multiprocessing.current_process() :在任何一個進程中搞清楚自己是誰,在線程中也有類似方法
Process類的介紹
1.創建進程的類
Process([group [, target [, name [, args [, kwargs]]]]]),由該類實例化得到的對象,表示一個子進程中的任務(尚未啟動) 強調: 1. 需要使用關鍵字的方式來指定參數 2. args指定的為傳給target函數的位置參數,是一個元組形式,必須有逗號
2.參數介紹
group參數未使用,值始終為None target表示調用對象,即子進程要執行的任務 args表示調用對象的位置參數元組,args=(1,2,'al',) kwargs表示調用對象的字典,kwargs={'name':'al','age':18} name為子進程的名稱
3.方法介紹
p.start():啟動進程,並調用該子進程中的p.run()
p.run():進程啟動時運行的方法,正是它去調用target指定的函數,我們自定義類的類中一定要實現該方法
p.terminate():強制終止進程p,不會進行任何清理操作,如果p創建了子進程,該子進程就成了僵屍進程,使用該方法需要特別小心這種情況。
如果p還保存了一個鎖那么也將不會被釋放,進而導致死鎖,線程中無此方法
p.is_alive():如果p仍然運行,返回True
p.join([timeout]):主線程等待p終止(強調:是主線程處於等的狀態,而p是處於運行的狀態)再往下運行。timeout是可選的超時時間,
需要強調的是,p.join只能join住start開啟的進程,而不能join住run開啟的進程
4.屬性介紹
p.daemon:默認值為False,如果設為True,代表p為后台運行的守護進程,當p的父進程終止時,p也隨之終止,並且設定為True后,p不能創建自己的新進程,必須在p.start()之前設置
p.name:進程的名稱
p.pid:進程的pid,線程中是t.ident 注意只有進程或線程已經啟動才會分配pid或ident
p.exitcode:進程在運行時為None、如果為–N,表示被信號N結束(了解即可)
p.authkey:進程的身份驗證鍵,默認是由os.urandom()隨機生成的32字符的字符串。這個鍵的用途是為涉及網絡連接的底層進程間通信提供安全性,
這類連接只有在具有相同的身份驗證鍵時才能成功(了解即可)
以用面向對象的形式使用進程與線程
關鍵點:
- 繼承Process或Thread類
- 重寫__init__方法
- 重寫run方法
from multiprocessing import Process import time import random import os
class pro(Process): #繼承Process類 def __init__(self,name): super().__init__() # 重寫__init__方法 這里是直接調用父類的初始化方法 self.name = name
def run(self): # 重寫run方法 print('%s is working,父進程%s,當前進程%s'%(self.name,os.getppid(),os.getpid())) time.sleep(1) print('%s end'%self.name)
if __name__ =='__main__': p1 = pro('a') p2 = pro('b') p3 = pro('c') p1.start() p2.start() p3.start() print('主進程',os.getpid()) >> 主進程 9116 b is working,父進程9116,當前進程4316 a is working,父進程9116,當前進程6664 c is working,父進程9116,當前進程10132 b end a end c end
總結:
Windows用的導入(所以if...main...判斷外面的代碼,在子進程仍會執行) 方法: p = Process(target=func,name='p1',daemon=True) #實例的時候可以指定方法、名字、是否守護進程 p.current_process() 獲取當前進程對象 p.is_alive() 判斷進程實例是否在運行 是返回True 否返回False p.terminate() 結束進程 線程無此方法 p.name 獲取進程名字 p.join() 等待子進程結束再往下執行 p.daemon = True 把進程設為守護進程 (父進程結束,守護進程也結束) 注意:如果設置了join() 那么terminate和daemon 就不管用啦 p.pid 獲取當前進程的pid 線程是t.ident 注意:只有進程(或線程)啟動之后,操作系統(或python解釋器)才會分配pid(或ident) 默認在子進程當中,會關閉標准輸入 import sys import os sys.stdin = os.fdopen(0) #加上這行代碼,子進程才能使用標准輸入 多線程的方法與屬性 t.setDaemon(True) t.setName('p1') t.getName()