進程
進程是指一個程序在給定數據集合上的一次執(zhí)行過程,是系統(tǒng)進行資源分配和運行調用的獨立單位。
可以簡單地理解為操作系統(tǒng)中正在執(zhí)行的程序。也就說,每個應用程序都有一個自己的進程。
每一個進程啟動時都會最先產生一個唯一線程,即主線程,然后主線程會再創(chuàng)建其他的子線程。
線程
線程是一個基本的CPU執(zhí)行單元。它必須依托于進程存活。一個線程是一個execution context(執(zhí)行上下文),即一個CPU執(zhí)行時所需要的一串指令。
協(xié)程
協(xié)程是一種用戶態(tài)的輕量級線程,協(xié)程的調度完全由用戶控制。
從技術的角度來說,“協(xié)程就是你可以暫停執(zhí)行的函數”。協(xié)程擁有自己的寄存器上下文和棧。
協(xié)程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧,直接操作棧則基本沒有內核切換的開銷。
可以不加鎖的訪問全局變量,所以上下文的切換非常快。
進程和線程的區(qū)別
- 線程必須在某個進程中執(zhí)行。
- 一個進程可包含多個線程,其中有且只有一個主線程。
- 多線程共享同個地址空間、打開的文件以及其他資源。
- 多進程共享物理內存、磁盤、打印機以及其他資源。
- 線程是處理器調度的基本單位,但進程不是
線程的類型
線程的因作用可以劃分為不同的類型。大致可分為:
- 主線程
- 子線程
- 后臺線程(守護線程)
- 前臺線程
GIL(全局解釋性鎖)
其他語言,CPU是多核時是支持多個線程同時執(zhí)行。但在Python中,無論是單核還是多核,同時只能由一個線程在執(zhí)行。其根源是GIL的存在。GIL的全稱是Global Interpreter Lock(全局解釋器鎖),來源是Python設計之初的考慮,為了數據安全所做的決定。某個線程想要執(zhí)行,必須先拿到GIL,我們可以把GIL看作是“通行證”,并且在一個Python進程中,GIL只有一個。拿不到通行證的線程,就不允許進入CPU執(zhí)行。
GIL只在CPython中才有,而在PyPy和Jython中是沒有GIL的,CPython版本的解釋器最常用。
并且由于GIL鎖存在,Python里一個進程永遠只能同時執(zhí)行一個線程(拿到GIL的線程才能執(zhí)行),這就是為什么在多核CPU上,Python 的多線程效率并不高的根本原因。
GIL鎖和線程鎖(互斥鎖)的區(qū)別
1. GIL鎖是解釋層面的鎖,而線程鎖是代碼層面的鎖。
2. 線程沒拿到GIL鎖時,不能進入CPU執(zhí)行,而沒拿到互斥鎖時,不能修改數據
例:
假設只有1個進程,有線程1、線程2要修改共享數據data,并且有互斥鎖。
多線程運行,假設線程1拿到了GIL鎖進入了CPU執(zhí)行,此時線程1獲得了互斥鎖,可以進行數據的修改,但還未進行修改。
線程1在修改data前,進行了IO操作或 ticks計數滿100,讓出了GIL鎖,假設線程2競爭獲得了GIL鎖,可以進入CPU執(zhí)行。
此時線程2執(zhí)行修改共享數據data的代碼,但由于線程1擁有互斥鎖,因而線程2并不能進行修改data數據,這時線程2讓出GIL鎖,GIL鎖再次發(fā)生競爭。
假設線程1獲得了GIL鎖,可以進入CPU執(zhí)行,因為線程1還擁有互斥鎖,所以其可以繼續(xù)對共享數據進行修改,修改完成后釋放互斥鎖。
當線程2得到了GIL鎖以及互斥鎖后,可以進入CPU執(zhí)行,并修改共享數據data。
Python 對并發(fā)編程的支持
多線程:【threading】,利用CPU和IO同時執(zhí)行的原理,讓CPU不會干巴巴等待IO完成
多進程:【multiprocessing】,利用多核CPU的能力,真正的執(zhí)行任務
異步IO:【asyncio】,在單線程利用CPU和IO同時執(zhí)行的原理,實現函數異步執(zhí)行
可以使用【Lock】對資源進行加鎖,防止沖突
使用【Queue】實現不同線程/進程間的通信,實現生產者/消費者模式
使用線程池【ThreadPoolExecutor】/進程池【ProcessPoolExecutor】,簡化線程/進程的任務提交、等待結束、獲取結果
多進程、多線程、多協(xié)程的對比
一個進程開啟的數量有限,這取決于CPU的限制
優(yōu)點:可以利用多核CPU并行運算
缺點:占用資源最多,可以啟動的數量比線程少
適用于:CPU密集型計算,例如:加解密、大數據、機器學習、正則表達式匹配等
一個進程中可以開啟N個線程
優(yōu)點:相比進程,更輕量,占用資源更少
缺點:
- 相比進程:多線程只能并發(fā)執(zhí)行,不能利用多CPU(GIL)
- 相比協(xié)程:啟動數目有限制,占用內存資源,有線程切換開銷
適用于:I/O密集型計算,例如:api接口獲取數據、爬蟲、數據庫或文件頻繁讀寫等
一個線程可以開啟N個協(xié)程,協(xié)程占用內存甚至只需要幾Kb
優(yōu)點:內存占用最小,啟動數目最多
缺點:支持的庫有限制,例如不能使用requtests,而要aiohttp或httpx,并且代碼實現復雜
適用于:I/O密集型計算,需要超多任務執(zhí)行,但有現成庫支持的場景
如何選擇使用合適的技術
1.首先判斷任務類型,判斷任務屬于CPU密集型,還是IO密集型
2.如果任務屬于CPU密集型 ==> 選擇多進程
3.如果任務屬于IO密集型:
- 判斷任務是否需要超多的任務量,并且有現有協(xié)程庫支持,并且可以接受其實現復雜度 ==> 選擇多協(xié)程
- 否則 ==> 選擇多線程
線程池使用的好處
提升性能:減去大量新建、終止線程的開銷,重用了線程資源
適用場景:適合處理突發(fā)性大量請求或需要大量線程完成任務、但實際任務處理時間較短
防御功能:能有效避免系統(tǒng)創(chuàng)建線程過多,而導致系統(tǒng)負荷過大、變慢的問題
代碼優(yōu)勢:使用線程池的語法,比自己創(chuàng)建執(zhí)行線程更簡潔
threading 和 multiprocessing對比

協(xié)程
在單線程內實現并發(fā)
核心原理1:用一個超級循環(huán)(實際上就是while...true循環(huán))
核心原理2:配合IO多路復用原理(IO時CPU可以干其他事情)
信號量、旗語【Semaphore】
是一個同步對象,用于保持0到指定最大值之間的一個計數值,簡而言之,用以控制并發(fā)量
簡單案例
import aiohttp
import asyncio
loop = asyncio.get_event_loop()
# 當放開下面代碼時,每次執(zhí)行10個任務后會停下等待一會,當然,最終程序爬取完成時間會變長
# semaphore = asyncio.Semaphore(10)
async def async_crawl(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
result = await resp.text()
await asyncio.sleep(5)
print(f'請求地址:{url},{len(result)}')
if __name__ == '__main__':
t1 = time.time()
task_list = [loop.create_task(async_crawl(f'https://pic.netbian.com/index_{page}.html')) for page in range(50)]
loop.run_until_complete(asyncio.wait(task_list))
t2 = time.time()
print(t2-t1)