一. 多任務(wù)編程
多任務(wù)編程指利用計算機的多核特點,同時執(zhí)行多個任務(wù)。通過充分地利用計算機資源吼句,來提高程序的運行效率。如多線程和多進程編程事格。在開始學習多任務(wù)編程前惕艳,區(qū)分如下幾個概念非常重要
1.1 并行和并發(fā)
當多個計算機核心同時處理多個任務(wù)時,多個任務(wù)之間是并行關(guān)系驹愚,因此并行數(shù)量取決于計算機核心數(shù)远搪。計算機同時處理多個任務(wù),內(nèi)核在多個任務(wù)之間不斷切換逢捺,達到好像在同時處理的運行效果谁鳍,此時多個任務(wù)實際為并發(fā)關(guān)系,如IO多路復(fù)用劫瞳。
舉一個生活中的列子:當只有一個咖啡機時倘潜,兩隊人排隊交替使用一個咖啡機,這是并發(fā)志于;而當我們有2臺咖啡機時涮因,兩隊人員就能同時使用各自的咖啡機,互補干擾伺绽。那么回到計算機中:
- 并行(parallel):指在同一時刻养泡,有多條指令在多個處理器上同時執(zhí)行嗜湃。所以無論從微觀還是從宏觀來看,二者都是一起執(zhí)行的澜掩。
- 并發(fā)(concurrency):指在同一時刻只能有一條指令執(zhí)行净蚤,但多個進程指令被快速的輪換執(zhí)行,使得在宏觀上具有多個進程同時執(zhí)行的效果输硝,但在微觀上并不是同時執(zhí)行的今瀑,只是把時間分成若干段,使多個進程快速交替的執(zhí)行点把。
1.2 進程和程序
進程:程序在計算機中運行一次的過程橘荠,是一個動態(tài)的過程描述,占有CPU內(nèi)存等計算機資源郎逃,具有一定的生命周期哥童。同一個程序的不同執(zhí)行過程,分配的計算機資源不同褒翰,屬于不同的進程贮懈。
一個進程的創(chuàng)建流程如下:
- 用戶空間運行一個程序,發(fā)起進程創(chuàng)建
- 操作系統(tǒng)接受用戶請求优训,開啟進程創(chuàng)建
- 操作系統(tǒng)分配系統(tǒng)資源朵你,確認進程狀態(tài)
- 將創(chuàng)建好的進程提供給應(yīng)用層使用
程序:靜態(tài)的可執(zhí)行文件,占有磁盤揣非,不占有計算機的運行資源抡医;磁盤不屬于計算機資源。
二. 進程
2.1 相關(guān)概念
CPU時間片:如果一個進程占有計算機核心早敬,我們稱為該進程在CPU的時間片上忌傻。多個任務(wù)實際對CPU會進行爭奪,一般由操作系統(tǒng)分配CPU時間片搞监。
進程控制塊PCB(Processing Control Block):在操作系統(tǒng)中水孩,進程創(chuàng)建后會自動產(chǎn)生一個空間存放進程信息,稱為進程控制塊琐驴。進程信息包含進程PID俘种、進程占有的內(nèi)存位置、創(chuàng)建時間棍矛、用戶安疗、狀態(tài)等。
進程PID:進程在操作系統(tǒng)中的唯一編號够委,是一個大于0的整數(shù)荐类,由系統(tǒng)自動分配。
進程的特征:進程是操作系統(tǒng)分配“計算機資源”的最小單位茁帽。每個進程都有自己單獨的虛擬內(nèi)存空間玉罐,進程間的執(zhí)行相互獨立屈嗤,互不影響。
ps -aux 查看進程信息
STAT | 描述 |
---|---|
D | 等待態(tài)吊输,且為不可中斷等待 |
S | 等待態(tài)饶号,為可中斷等待 |
T | 等待態(tài),為暫停狀態(tài) |
R | 就緒態(tài)季蚂,運行態(tài) |
Z | 僵尸態(tài) 茫船,進程已經(jīng)結(jié)束,但仍存在一些問題解決 |
+ | 前臺進程 |
< | 高優(yōu)先級 |
N | 低優(yōu)先級 |
I | 有進程鏈接 |
s | 會話組組長 |
進程的狀態(tài)
三態(tài):
- 就緒:進程具備執(zhí)行條件扭屁,等待系統(tǒng)分配CPU
- 執(zhí)行:進程占有CPU時間片算谈,處于運行狀態(tài)
- 等待:進程暫時不具備運行條件,需要阻塞等待
五態(tài)(增加新建態(tài)和終止態(tài)):
- 新建:創(chuàng)建一個新的進程料滥,獲取資源的過程
- 終止:進程結(jié)束然眼,釋放資源的過程
- 進程優(yōu)先級:優(yōu)先級決定了一個進程的執(zhí)行權(quán)限和占有資源的優(yōu)先程度
- top命令 : 動態(tài)地查看進程優(yōu)先級,其中NI的取值范圍為-20~19葵腹,其中-20優(yōu)先級最高
- nice : 運行程序時指定優(yōu)先級高每;
sudo nice -9 ./while.py # 指定以-9優(yōu)先級來運行程序
父子進程:除了初始化進程,每個進程都有一個父進程践宴,也可能有0個或者多個子進程鲸匿。
- pstree:查看進程樹
- ps -ajx:查看父進程PID
進程的相關(guān)函數(shù)
- os.getpid():獲取當前進程的PID
- os.getppid():獲取父進程的PID
- os._exit(status):退出進程,使用status整數(shù)標識進程的退出狀態(tài)浴井,并通過人為賦予相應(yīng)的含義
- sys.exit([status]):默認為0晒骇,如果是整數(shù),則表示退出狀態(tài)磺浙;如果是字符串,則表示在退出時打印內(nèi)容
孤兒進程:父進程先于子進程退出徒坡,此時子進程就會成為孤兒進程撕氧。孤兒進程會被系統(tǒng)指定的進程收養(yǎng),即系統(tǒng)進程會成為該孤兒進程新的父進程喇完,處理孤兒進程的退出狀態(tài)伦泥。因此孤兒進程一定不會成為僵尸進程。
僵尸進程:子進程先于父進程退出锦溪,并且父進程沒有處理子進程退出狀態(tài)不脯,此時子進程成為僵尸進程。僵尸進程已經(jīng)結(jié)束刻诊,但是會滯留部分PCB信息在內(nèi)存防楷,大量的僵尸會消耗系統(tǒng)資源,應(yīng)該盡量避免僵尸進程的產(chǎn)生则涯。
三. 進程的創(chuàng)建
3.1 使用os.fork函數(shù)
- os.fork()創(chuàng)建子進程失敗時返回負數(shù)复局,成功時返回一個非負數(shù)冲簿;
- 在子進程中,返回值為0亿昏;在父進程中峦剔,返回值為子進程的PID;
- 子進程從pid = os.fork()語句之后開始執(zhí)行角钩,但子進程會復(fù)制父進程的全部代碼段吝沫。
import os
import time
a = 1
print(time.ctime(), '程序開始執(zhí)行:')
pid = os.fork() # 創(chuàng)建子進程
if pid < 0:
print('子進程創(chuàng)建失敗!')
elif pid == 0:
# 子進程
time.sleep(2)
print('子進程:a=', a)
a = 1000
print('在子進程中將a的值設(shè)為1000')
else:
print('子進程PID:', pid)
time.sleep(4)
print('父進程:a=',a) # 空間獨立,互不影響
print(time.ctime(), '程序執(zhí)行完畢!')
運行結(jié)果:
3.2 如何避免僵尸進程的產(chǎn)生
當子進程先于父進程退出递礼,如果父進程沒有處理子進程退出狀態(tài)野舶,此時子進程成為僵尸進程。因為為了避免產(chǎn)生僵尸宰衙,首先想到的即讓“父進程先于子進程退出”平道,具體實現(xiàn)如下:
- 父進程創(chuàng)建子進程等待子進程退出;
- 子進程創(chuàng)建二級子進程供炼,然后立馬退出一屋;
- 二級子進程成為孤兒,處理具體事宜袋哼;
import os
from time import sleep, ctime
def func1():
sleep(3)
print(ctime(), 'fun1()')
def func2():
sleep(4)
print(ctime(), 'fun2()')
pid = os.fork()
if pid < 0:
print('create process failed!')
elif pid == 0:
# 創(chuàng)建二級子進程
pid0 = os.fork()
if pid0 < 0:
print('create 2th process failed!')
elif pid0 == 0:
func2()
print(ctime(), '二級子進程退出!')
else:
# 一級子進程在創(chuàng)建二級子進程后立刻退出冀墨,不會造成父進程的阻塞
print(ctime(), '一級子進程退出')
os._exit(0)
else:
print(ctime(), '父進程阻塞等待回收一級子進程')
os.wait()
func1()
print(ctime(), '父進程退出')
運行結(jié)果:
避免僵尸的第二種方法即在父進程中阻塞等待處理子進程的退出,使用我們上例中用到的os.wait即可涛贯。os.wait()返回值為子進程PID以及子進程的退出狀態(tài)(默認為0)诽嘉。
import os
import sys
from time import sleep, ctime
pid = os.fork()
if pid < 0:
print("Create process failed!")
elif pid == 0:
sleep(3)
print(ctime(), "子進程{}即將退出".format(os.getpid()))
sys.exit(2)
else:
print(ctime(), '父進程阻塞等待子進程{}的退出'.format(pid))
# 阻塞等待子進程退出
pid,status = os.wait()
print(ctime(), '父進程已處理子進程{}的退出'.format(pid))
# 退出狀態(tài)2 * 256,便于系統(tǒng)的識別
print(pid,status)
# 打印sys.exit(2)設(shè)置的退出狀態(tài)
print(os.WEXITSTATUS(status))
運行結(jié)果:
和os.wait方法類似,os模塊還提供了一個os.waitpid(pid,option)方法弟翘,pid可以指定處理的子進程PID虫腋,option則可以指定阻塞和非阻塞。具體如下:
-
pid,status = os.waitpid(pid,option)
處理子進程的退出 -
pid
-1:任意子進程退出稀余;>0:指定PID號的子進程退出悦冀; -
option
0:阻塞等待;WNOHANG:非阻塞 -
pid, status = waitpid(-1,0)
等價于pid, status = wait()
import os
import sys
from time import sleep
pid = os.fork()
if pid < 0:
print("Create process failed!")
elif pid == 0:
sleep(3)
print("子進程{}即將退出".format(os.getpid()))
sys.exit(2)
else:
# 等待子進程退出
while True:
sleep(1)
#-1表示任意子進程 os.WNOHANG表示非阻塞
pid,status = os.waitpid(-1,os.WNOHANG)
#status即子進程的退出狀態(tài)sys.exit(2) * 256
print(pid,status)
if os.WEXITSTATUS(status):
print("主進程已處理{}進程的退出".format(pid))
break
print("Do something others!")
運行結(jié)果:
除了使用上述的os.wait / os.waitpid在主進程中處理子進程的退出狀態(tài)外睛琳,更常用的一種方式是利用系統(tǒng)內(nèi)核盒蟆,異步地處理子進程退出問題。實現(xiàn)過程大致如下:
- 在多任務(wù)程序初始化時师骗,(注意历等,需要導(dǎo)入signal標準庫模塊)聲明signal.signal(signal.SIGCHLD, signal.SIG_IGN);
- 內(nèi)核發(fā)現(xiàn)子進程退出時辟癌,會給主進程發(fā)送一個SIGCHLD信號寒屯,signal的第二個參數(shù)是主進程對子進程的處理方式;
- SIG_IGN即主進程忽略子進程的退出愿待,并交由系統(tǒng)處理回收子進程的退出浩螺。(此處不再詳細舉例靴患,后續(xù)介紹進程間通信方式時,再對信號做詳細介紹)
3.2 使用multiprocessing模塊
multiprocessing模塊中要出,需要將要做的事情封裝為函數(shù)鸳君,使用multiprocessing中提供的Process類,在實例化時傳入target函數(shù)患蹂,創(chuàng)建進程對象或颊。p.start啟動進程后,會自動執(zhí)行相關(guān)聯(lián)函數(shù)传于,并在事件完成后回收進程囱挑。此過程用到的主要類、方法如下:
類/方法 | 參數(shù) |
---|---|
Process(name,target,*args,**kwargs) |
創(chuàng)建進程對象: name:給創(chuàng)建的進程對象起一個名字沼溜,default:Process-n平挑;target:綁定的事件函數(shù);args:給“target”函數(shù)'按照位置傳參系草;kwargs:給“target”函數(shù)'按照關(guān)鍵字傳參通熄; |
P.start() |
啟動進程,并自動執(zhí)行進程函數(shù)找都。 |
P.join([timeout]) |
阻塞等待回收響應(yīng)的進程唇辨。timeout可選參數(shù):超時時間 |
'''通常使用multiprocessing創(chuàng)建進程,父進程只用作進程的創(chuàng)建和回收能耻,不做其他工作赏枚。'''
import os, time
import multiprocessing as mp
a = 1
def func():
'''修改子進程空間的內(nèi)容,不會對父進程產(chǎn)生影響'''
global a
a = 100
print('在子進程中a =',a)
print('子進程{}已完成func的執(zhí)行晓猛,即將返回主進程{}'.format(os.getpid(), os.getppid()))
p = mp.Process(target=func) # 創(chuàng)建進程對象
p.start() # 啟動進程
print('在父進程{}中創(chuàng)建了子進程{}'.format(os.getpid(), p.pid))
time.sleep(5) # 延遲5s,使子進程修改了變量a綁定的值
print('父進程中:a = ',a)
p.join() # 回收子進程,有效防止了僵尸進程的產(chǎn)生
運行結(jié)果:
multiprocessing創(chuàng)建進程總結(jié)如下:
multiprocessing創(chuàng)建進程是原來進程的子進程饿幅,創(chuàng)建后父子進程各自執(zhí)行互不影響。子進程同樣是復(fù)制父進程的空間鞍帝,子進程對內(nèi)容的修改诫睬,不會影響父進程空間。join回收子進程帕涌,會有效阻止僵尸進程產(chǎn)生。
進程對象的屬性:
- p.name:進程名稱续徽,默認為Process-1蚓曼,可自定義名稱;
- p.pid:創(chuàng)建進程的PID
- p.daemon:默認值為False钦扭,父進程退出纫版,不會影響子進程的運行;設(shè)置為True時客情,父進程退出其弊,子進程也將退出癞己;daemon 的設(shè)置必須在start前。如果設(shè)置daemon為True梭伐,則不再使用join痹雅,在子進程退出前,會自動做處理糊识;
- p.is_alive() 判斷進程生命周期狀態(tài)绩社,返回True/False
通常使用multiprocessing創(chuàng)建進程,父進程只用作進程的創(chuàng)建和回收赂苗,不做其他工作愉耙。