Python進程、線程、協(xié)程

進程

進程是指一個程序在給定數據集合上的一次執(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)
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容