進(jìn)程與線程
進(jìn)程(Process)和線程(Thread)都是操作系統(tǒng)中的基本概念,它們之間有一些優(yōu)劣和差異。
1. 進(jìn)程
進(jìn)程是程序執(zhí)行時(shí)的一個(gè)實(shí)例,是系統(tǒng)進(jìn)行資源分配的基本單位。
所有與該進(jìn)程有關(guān)的資源卡儒,都被記錄在進(jìn)程控制塊(PCB)中。以表示該進(jìn)程擁有這些資源或正在使用它們俐巴。
另外骨望,進(jìn)程也是搶占處理機(jī)的調(diào)度單位,它擁有一個(gè)完整的虛擬地址空間欣舵。
當(dāng)進(jìn)程發(fā)生調(diào)度時(shí)擎鸠,不同的進(jìn)程擁有不同的虛擬地址空間,而同一進(jìn)程內(nèi)的不同線程共享同一地址空間缘圈。
進(jìn)程可以通過fork或spawn的方式來創(chuàng)建新的進(jìn)程來執(zhí)行其他的任務(wù)劣光,不過新的進(jìn)程也有自己獨(dú)立的內(nèi)存空間,因此必須通過進(jìn)程間通信機(jī)制(IPC糟把,Inter-Process Communication)來實(shí)現(xiàn)數(shù)據(jù)共享绢涡,具體的方式包括管道、信號(hào)遣疯、套接字雄可、共享內(nèi)存區(qū)等。
2. 線程
線程,是進(jìn)程中的一個(gè)實(shí)體数苫,是被系統(tǒng)獨(dú)立調(diào)度和分派的基本單位聪舒。
與進(jìn)程不同,線程與資源分配無關(guān)虐急,線程自己不擁有系統(tǒng)資源箱残,它屬于某一個(gè)進(jìn)程,并與進(jìn)程內(nèi)的其他線程一起共享進(jìn)程的資源止吁。
由于線程在同一個(gè)進(jìn)程下被辑,它們可以共享相同的上下文,因此相對(duì)于進(jìn)程而言赏殃,線程間的信息共享和通信更加容易敷待。
線程只由相關(guān)堆棧(系統(tǒng)棧或用戶棧)寄存器和線程控制表TCB組成仁热。
3. 進(jìn)程與線程的關(guān)系
通常在一個(gè)進(jìn)程中可以包含若干個(gè)線程,它們可以利用進(jìn)程所擁有的資源勾哩。
但是抗蠢,一個(gè)線程只屬于一個(gè)進(jìn)程。
進(jìn)程間相互獨(dú)立思劳,同一進(jìn)程的各線程間共享迅矛。某進(jìn)程內(nèi)的線程在其它進(jìn)程不可見。
而且需要注意的是潜叛,線程不是一個(gè)可執(zhí)行的實(shí)體秽褒。
4. 進(jìn)程和線程的比較
進(jìn)行和線程之間的差異可以從下面幾個(gè)方面來闡述:
調(diào)度 :
在引入線程的操作系統(tǒng)中,線程是調(diào)度和分配的基本單位 威兜,進(jìn)程是資源擁有的基本單位 销斟。把傳統(tǒng)進(jìn)程的兩個(gè)屬性分開,線程便能輕裝運(yùn)行椒舵,從而可顯著地提高系統(tǒng)的并發(fā)程度蚂踊。在同一進(jìn)程中,線程的切換不會(huì)引起進(jìn)程的切換笔宿;在由一個(gè)進(jìn)程中的線程切換到另一個(gè)進(jìn)程中的線程時(shí)犁钟,才會(huì)引起進(jìn)程的切換。并發(fā)性 :
在引入線程的操作系統(tǒng)中泼橘,不僅進(jìn)程之間可以并發(fā)執(zhí)行涝动,而且在一個(gè)進(jìn)程中的多個(gè)線程之間亦可并發(fā)執(zhí)行,因而使操作系統(tǒng)具有更好的并發(fā)性炬灭,從而能更有效地使用系統(tǒng)資源和提高系統(tǒng)吞吐量醋粟。擁有資源 :
不論是傳統(tǒng)的操作系統(tǒng),還是設(shè)有線程的操作系統(tǒng),進(jìn)程都是擁有資源的一個(gè)獨(dú)立 單位昔穴,它可以擁有自己的資源镰官。一般地說,線程自己不擁有系統(tǒng)資源(只有一些必不可少的資源吗货,但它可以訪問其隸屬進(jìn)程的資源泳唠。系統(tǒng)開銷:
由于在創(chuàng)建或撤消進(jìn)程時(shí),系統(tǒng)都要為之分配或回收資源宙搬,因此笨腥,操作系統(tǒng)所付出的開銷將顯著地大于在創(chuàng)建或撤消線程時(shí)的開銷。進(jìn)程切換的開銷也遠(yuǎn)大于線程切換的開銷勇垛。通信:
進(jìn)程間通信IPC脖母,線程間可以直接讀寫進(jìn)程數(shù)據(jù)段(如全局變量)來進(jìn)行通信——需要進(jìn)程同步和互斥手段的輔助,以保證數(shù)據(jù)的一致性闲孤,因此共享簡(jiǎn)單谆级。但是線程的數(shù)據(jù)同步要比進(jìn)程略復(fù)雜。
python 中的多進(jìn)程 multiprocessing
Linux操作系統(tǒng)上提供了fork()系統(tǒng)調(diào)用來創(chuàng)建進(jìn)程讼积,調(diào)用fork()函數(shù)的是父進(jìn)程肥照,創(chuàng)建出的是子進(jìn)程,子進(jìn)程是父進(jìn)程的一個(gè)拷貝勤众,但是子進(jìn)程擁有自己的PID舆绎。fork()函數(shù)非常特殊它會(huì)返回兩次,父進(jìn)程中可以通過fork()函數(shù)的返回值得到子進(jìn)程的PID们颜,而子進(jìn)程中的返回值永遠(yuǎn)都是0吕朵。
Python的os模塊提供了fork()函數(shù)。由于Windows系統(tǒng)沒有fork()調(diào)用窥突,因此要實(shí)現(xiàn)跨平臺(tái)的多進(jìn)程編程努溃,可以使用multiprocessing模塊的Process類來創(chuàng)建子進(jìn)程,而且該模塊還提供了更高級(jí)的封裝波岛,例如批量啟動(dòng)進(jìn)程的進(jìn)程池(Pool)茅坛、用于進(jìn)程間通信的隊(duì)列(Queue)和管道(Pipe)等。
# 非多進(jìn)程下載
import random
import time
def download(file):
download_time= random.randint(1,10)
time.sleep(download_time)
print('%s下載完成! 耗費(fèi)了%d秒' % (file, download_time))
def main():
# 非多進(jìn)程则拷,按代碼順序執(zhí)行download函數(shù)
start=time.time()
download('1hello')
download('2python')
end=time.time()
print('總共耗費(fèi)了%.2f秒.' % (end - start))
main()
1hello下載完成! 耗費(fèi)了8秒
2python下載完成! 耗費(fèi)了7秒
總共耗費(fèi)了15.01秒.
從上面的例子可以看出贡蓖,如果程序中的代碼只能按順序一點(diǎn)點(diǎn)的往下執(zhí)行,那么即使執(zhí)行兩個(gè)毫不相關(guān)的下載任務(wù)煌茬,也需要先等待一個(gè)文件下載完成后才能開始下一個(gè)下載任務(wù)斥铺,很顯然這并不合理也沒有效率。
接下來我們使用多進(jìn)程的方式將兩個(gè)下載任務(wù)放到不同的進(jìn)程中坛善,代碼如下所示晾蜘。
# 多進(jìn)程下載
import random
import time
from multiprocessing import Process
from os import getpid
def download(file):
print('啟動(dòng)下載進(jìn)程邻眷,進(jìn)程號(hào)[%d].' % getpid())
download_time= random.randint(1,10)
time.sleep(download_time)
print('%s下載完成! 耗費(fèi)了%d秒' % (file, download_time))
def main1():
start=time.time()
pid1= Process(target = download , args=('1hello',))
pid1.start()
pid2= Process(target = download , args=('2python',))
pid2.start()
pid1.join()
pid2.join()
end=time.time()
print('總共耗費(fèi)了%.2f秒.' % (end - start))
if __name__ == '__main__':
main1()
啟動(dòng)下載進(jìn)程1hello剔交,進(jìn)程號(hào)[10304].
1hello下載完成! 耗費(fèi)了2秒
2python下載完成! 耗費(fèi)了9秒
總共耗費(fèi)了9.55秒.
在上面的代碼中肆饶,我們通過Process類創(chuàng)建了進(jìn)程對(duì)象,
通過target參數(shù)我們傳入一個(gè)函數(shù)來表示進(jìn)程啟動(dòng)后要執(zhí)行的代碼岖常,
后面的args是一個(gè)元組驯镊,它代表了傳遞給函數(shù)的參數(shù)。
Process對(duì)象的start方法用來啟動(dòng)進(jìn)程竭鞍,而join方法表示等待進(jìn)程執(zhí)行結(jié)束板惑。
運(yùn)行上面的代碼可以明顯發(fā)現(xiàn)兩個(gè)下載任務(wù)“同時(shí)”啟動(dòng)了,而且程序的執(zhí)行時(shí)間將大大縮短偎快,不再是兩個(gè)任務(wù)的時(shí)間總和冯乘。下面是程序的一次執(zhí)行結(jié)果。
由圖可知裆馒,運(yùn)行時(shí)系統(tǒng)里面存在此進(jìn)程
multiprocessing.Process(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)
- target 是函數(shù)名字,需要調(diào)用的函數(shù)
- args 函數(shù)需要的位置參數(shù)惋戏,以 tuple 的形式傳入领追。args=(1,2,'justin',)
- kwargs 函數(shù)需要的命名參數(shù),以 dict 的形式傳入响逢。kwargs={'name':'jack','age':18}
- name 子進(jìn)程的名稱,默認(rèn)為None
- group 參數(shù)未使用棕孙,默認(rèn)為None
方法:
- star() 方法啟動(dòng)進(jìn)程
- join() 方法實(shí)現(xiàn)進(jìn)程間的同步舔亭,等待所有進(jìn)程退出。
python 中的多線程threading
目前的多線程開發(fā)我們推薦使用threading模塊蟀俊,該模塊對(duì)多線程編程提供了更好的面向?qū)ο蟮姆庋b钦铺。我們把剛才下載文件的例子用多線程的方式來實(shí)現(xiàn)一遍。
# 多線程下載
from threading import Thread
import time
import random
def download(file):
print('開始下載[%s].' % file)
download_time= random.randint(1,10)
time.sleep(download_time)
print('%s下載完成! 耗費(fèi)了%d秒' % (file, download_time))
def main():
start=time.time()
t1= Thread(target = download , args=('1hello',))
t1.start()
t2=Thread(target = download , args=('2python',))
t2.start()
t1.join()
t2.join()
end=time.time()
print('總共耗費(fèi)了%.3f秒' % (end - start))
if __name__ == '__main__':
main()
開始下載[1hello].
開始下載[2python].
2python下載完成! 耗費(fèi)了4秒
1hello下載完成! 耗費(fèi)了6秒
總共耗費(fèi)了6.006秒
1. 線程鎖Lock
因?yàn)槎鄠€(gè)線程可以共享進(jìn)程的內(nèi)存空間肢预,因此要實(shí)現(xiàn)多個(gè)線程間的通信相對(duì)簡(jiǎn)單矛洞,大家能想到的最直接的辦法就是設(shè)置一個(gè)全局變量,多個(gè)線程共享這個(gè)全局變量即可烫映。
但是當(dāng)多個(gè)線程共享同一個(gè)變量(我們通常稱之為“資源”)的時(shí)候沼本,很有可能產(chǎn)生不可控的結(jié)果從而導(dǎo)致程序失效甚至崩潰。
如果一個(gè)資源被多個(gè)線程競(jìng)爭(zhēng)使用锭沟,那么我們通常稱之為“臨界資源”抽兆,對(duì)“臨界資源”的訪問需要加上保護(hù),否則資源會(huì)處于“混亂”的狀態(tài)族淮。
下面的例子演示了100個(gè)線程向同一個(gè)銀行賬戶轉(zhuǎn)賬(轉(zhuǎn)入1元錢)的場(chǎng)景辫红,在這個(gè)例子中凭涂,銀行賬戶就是一個(gè)臨界資源,在沒有保護(hù)的情況下我們很有可能會(huì)得到錯(cuò)誤的結(jié)果贴妻。
from time import sleep
from threading import Thread
class Account():
"""
類:賬戶管理
"""
def __init__(self):
self._balance=0
def deposit(self,money):
"""
計(jì)算存款后的余額
"""
new_balance = self._balance + money
sleep(0.01)
self._balance = new_balance
@property
def balance(self):
return self._balance
class AddMoney(Thread):
"""
繼承線程類: 存錢操作
"""
def __init__(self, account, money):
super().__init__()
self._account = account
self._money = money
def run(self):
self._account.deposit(self._money)
def main():
account = Account()
thread_pool = []
# 啟動(dòng)100個(gè)線程切油,并存1元錢
for _ in range(100):
t = AddMoney(account,1)
thread_pool.append(t)
t.start()
for t in thread_pool:
t.join()
print('賬戶余額為:%d 元'% account.balance)
if __name__=='__main__':
main()
賬戶余額為:3 元
運(yùn)行上面程序會(huì)發(fā)現(xiàn)100個(gè)線程存錢后,賬戶里面的錢是小于100元的名惩。
多個(gè)線程同時(shí)向賬戶中存錢時(shí)澎胡,會(huì)一起執(zhí)行到new_balance = self._balance + money
這行代碼,多個(gè)線程得到的賬戶余額都是初始狀態(tài)下的0绢片,所以都是0上面做了+1的操作滤馍,因此得到了錯(cuò)誤的結(jié)果。
在這種情況下底循,“鎖”就可以派上用場(chǎng)了巢株。我們可以通過“鎖”來保護(hù)“臨界資源”,只有獲得“鎖”的線程才能訪問“臨界資源”熙涤,而其他沒有得到“鎖”的線程只能被阻塞起來阁苞,直到獲得“鎖”的線程釋放了“鎖”,其他線程才有機(jī)會(huì)獲得“鎖”祠挫,進(jìn)而訪問被保護(hù)的“臨界資源”那槽。下面的代碼演示了如何使用“鎖”來保護(hù)對(duì)銀行賬戶的操作,從而獲得正確的結(jié)果等舔。
from threading import Lock
class AccountWithLock(object):
def __init__(self):
self._balance = 0
self._lock = Lock()
def deposit(self, money):
# 先獲取鎖才能執(zhí)行后續(xù)的代碼
self._lock.acquire()
try:
new_balance = self._balance + money
sleep(0.01)
self._balance = new_balance
finally:
# 在finally中執(zhí)行釋放鎖的操作保證正常異常鎖都能釋放
self._lock.release()
@property
def balance(self):
return self._balance
def mainLock():
account = AccountWithLock()
thread_pool = []
# 啟動(dòng)100個(gè)線程骚灸,并存1元錢
for _ in range(100):
t = AddMoney(account,1)
thread_pool.append(t)
t.start()
for t in thread_pool:
t.join()
print('賬戶余額為:%d 元'% account.balance)
if __name__=='__main__':
mainLock()
賬戶余額為:100 元
2. GIL 全局解釋器鎖 (面試常考)
比較遺憾的一件事情是Python的多線程并不能發(fā)揮CPU的多核特性慌植,這一點(diǎn)只要啟動(dòng)幾個(gè)執(zhí)行死循環(huán)的線程就可以得到證實(shí)了甚牲。
之所以如此,是因?yàn)镻ython的解釋器有一個(gè)“全局解釋器鎖”(GIL)的東西蝶柿,任何線程執(zhí)行前必須先獲得GIL鎖丈钙,然后每執(zhí)行100條字節(jié)碼,解釋器就自動(dòng)釋放GIL鎖交汤,讓別的線程有機(jī)會(huì)執(zhí)行雏赦,這是一個(gè)歷史遺留問題,但是即便如此芙扎,就如我們之前舉的例子星岗,使用多線程在提升執(zhí)行效率和改善用戶體驗(yàn)方面仍然是有積極意義的。
多進(jìn)程還是多線程選擇纵顾?
無論是多進(jìn)程還是多線程伍茄,只要數(shù)量一多,效率肯定上不去施逾,為什么呢敷矫?
我們打個(gè)比方例获,假設(shè)你不幸正在準(zhǔn)備中考,每天晚上需要做語文曹仗、數(shù)學(xué)榨汤、英語、物理怎茫、化學(xué)這5科的作業(yè)收壕,每項(xiàng)作業(yè)耗時(shí)1小時(shí)。如果你先花1小時(shí)做語文作業(yè)轨蛤,做完了蜜宪,再花1小時(shí)做數(shù)學(xué)作業(yè),這樣祥山,依次全部做完圃验,一共花5小時(shí),這種方式稱為單任務(wù)模型缝呕。如果你打算切換到多任務(wù)模型澳窑,可以先做1分鐘語文,再切換到數(shù)學(xué)作業(yè)供常,做1分鐘摊聋,再切換到英語,以此類推栈暇,只要切換速度足夠快麻裁,這種方式就和單核CPU執(zhí)行多任務(wù)是一樣的了,以旁觀者的角度來看源祈,你就正在同時(shí)寫5科作業(yè)悲立。
但是,切換作業(yè)是有代價(jià)的新博,比如從語文切到數(shù)學(xué),要先收拾桌子上的語文書本脚草、鋼筆(這叫保存現(xiàn)場(chǎng))赫悄,然后,打開數(shù)學(xué)課本馏慨、找出圓規(guī)直尺(這叫準(zhǔn)備新環(huán)境)埂淮,才能開始做數(shù)學(xué)作業(yè)。操作系統(tǒng)在切換進(jìn)程或者線程時(shí)也是一樣的写隶,它需要先保存當(dāng)前執(zhí)行的現(xiàn)場(chǎng)環(huán)境(CPU寄存器狀態(tài)、內(nèi)存頁等)痪蝇,然后,把新任務(wù)的執(zhí)行環(huán)境準(zhǔn)備好(恢復(fù)上次的寄存器狀態(tài)躏啰,切換內(nèi)存頁等)趁矾,才能開始執(zhí)行。這個(gè)切換過程雖然很快毫捣,但是也需要耗費(fèi)時(shí)間帝际。如果有幾千個(gè)任務(wù)同時(shí)進(jìn)行蔓同,操作系統(tǒng)可能就主要忙著切換任務(wù),根本沒有多少時(shí)間去執(zhí)行任務(wù)了蹲诀,這種情況最常見的就是硬盤狂響斑粱,點(diǎn)窗口無反應(yīng)侧甫,系統(tǒng)處于假死狀態(tài)。所以咒锻,多任務(wù)一旦多到一個(gè)限度守屉,反而會(huì)使得系統(tǒng)性能急劇下降,最終導(dǎo)致所有任務(wù)都做不好拇泛。
可以把任務(wù)分為計(jì)算密集型和I/O密集型。
計(jì)算密集型任務(wù)的特點(diǎn)是要進(jìn)行大量的計(jì)算恭取,消耗CPU資源熄守,比如對(duì)視頻進(jìn)行編碼解碼或者格式轉(zhuǎn)換等等,這種任務(wù)全靠CPU的運(yùn)算能力攒发,雖然也可以用多任務(wù)完成晋南,但是任務(wù)越多,花在任務(wù)切換的時(shí)間就越多偶妖,CPU執(zhí)行任務(wù)的效率就越低。計(jì)算密集型任務(wù)由于主要消耗CPU資源檀葛,這類任務(wù)用Python這樣的腳本語言去執(zhí)行效率通常很低腹缩,最能勝任這類任務(wù)的是C語言,我們之前提到了Python中有嵌入C/C++代碼的機(jī)制润讥。此類任務(wù)一般適合多進(jìn)程架構(gòu)盘寡。
除了計(jì)算密集型任務(wù),其他的涉及到網(wǎng)絡(luò)脆粥、存儲(chǔ)介質(zhì)I/O的任務(wù)都可以視為I/O密集型任務(wù)影涉,這類任務(wù)的特點(diǎn)是CPU消耗很少,任務(wù)的大部分時(shí)間都在等待I/O操作完成(因?yàn)镮/O的速度遠(yuǎn)遠(yuǎn)低于CPU和內(nèi)存的速度)匣缘。對(duì)于I/O密集型任務(wù)鲜棠,如果啟動(dòng)多任務(wù),就可以減少I/O等待時(shí)間從而讓CPU高效率的運(yùn)轉(zhuǎn)柑爸。此類任務(wù)一般適合多線程架構(gòu)盒音。
多進(jìn)程模式最大的優(yōu)點(diǎn)就是穩(wěn)定性高,因?yàn)橐粋€(gè)子進(jìn)程崩潰了,不會(huì)影響主進(jìn)程和其他子進(jìn)程用爪。(當(dāng)然主進(jìn)程掛了所有進(jìn)程就全掛了,但是Master進(jìn)程只負(fù)責(zé)分配任務(wù)诸衔,掛掉的概率低)著名的Apache最早就是采用多進(jìn)程模式。
多進(jìn)程模式的缺點(diǎn)是創(chuàng)建進(jìn)程的代價(jià)大就缆,在Unix/Linux系統(tǒng)下谒亦,用fork調(diào)用還行,在Windows下創(chuàng)建進(jìn)程開銷巨大切揭。另外锁摔,操作系統(tǒng)能同時(shí)運(yùn)行的進(jìn)程數(shù)也是有限的,在內(nèi)存和CPU的限制下谐腰,如果有幾千個(gè)進(jìn)程同時(shí)運(yùn)行,操作系統(tǒng)連調(diào)度都會(huì)成問題励背。
多線程模式通常比多進(jìn)程快一點(diǎn)桦踊,但是也快不到哪去,而且竟闪,多線程模式致命的缺點(diǎn)就是任何一個(gè)線程掛掉都可能直接造成整個(gè)進(jìn)程崩潰杖狼,因?yàn)樗芯€程共享進(jìn)程的內(nèi)存。在Windows上理朋,如果一個(gè)線程執(zhí)行的代碼出了問題绿聘,你經(jīng)常可以看到這樣的提示:“該程序執(zhí)行了非法操作兽愤,即將關(guān)閉”,其實(shí)往往是某個(gè)線程出了問題逐沙,但是操作系統(tǒng)會(huì)強(qiáng)制結(jié)束整個(gè)進(jìn)程吩案。
在Windows下帝簇,多線程的效率比多進(jìn)程要高崎岂,所以微軟的IIS服務(wù)器默認(rèn)采用多線程模式冲甘。由于多線程存在穩(wěn)定性的問題,IIS的穩(wěn)定性就不如Apache陶夜。為了緩解這個(gè)問題条辟,IIS和Apache現(xiàn)在又有多進(jìn)程+多線程的混合模式宏胯,真是把問題越搞越復(fù)雜肩袍。
異步IO
考慮到CPU和IO之間巨大的速度差異魂爪,一個(gè)任務(wù)在執(zhí)行的過程中大部分時(shí)間都在等待IO操作艰管,單進(jìn)程單線程模型會(huì)導(dǎo)致別的任務(wù)無法并行執(zhí)行,因此粗井,我們才需要多進(jìn)程模型或者多線程模型來支持多任務(wù)并發(fā)執(zhí)行。
現(xiàn)代操作系統(tǒng)對(duì)IO操作已經(jīng)做了巨大的改進(jìn),最大的特點(diǎn)就是支持異步IO餐济。如果充分利用操作系統(tǒng)提供的異步IO支持絮姆,就可以用單進(jìn)程單線程模型來執(zhí)行多任務(wù),這種全新的模型稱為事件驅(qū)動(dòng)模型篙悯,Nginx就是支持異步IO的Web服務(wù)器鸽照,它在單核CPU上采用單進(jìn)程模型就可以高效地支持多任務(wù)。在多核CPU上定血,可以運(yùn)行多個(gè)進(jìn)程(數(shù)量與CPU核心數(shù)相同)诞外,充分利用多核CPU。由于系統(tǒng)總的進(jìn)程數(shù)量十分有限茫虽,因此操作系統(tǒng)調(diào)度非常高效既们。用異步IO編程模型來實(shí)現(xiàn)多任務(wù)是一個(gè)主要的趨勢(shì)。
對(duì)應(yīng)到Python語言悼枢,單線程的異步編程模型稱為協(xié)程脾拆,有了協(xié)程的支持,就可以基于事件驅(qū)動(dòng)編寫高效的多任務(wù)程序绰上。我們會(huì)在后面討論如何編寫協(xié)程渠驼。
本文部分內(nèi)容來自于廖雪峰官方網(wǎng)站的《Python教程》
微信關(guān)注.png