深入理解Python虛擬機之進程、線程和協(xié)程區(qū)別詳解
深入理解 Python 虛擬機:進程、線程和協(xié)程
進程和線程
進程是一個非常古老的概念,根據(jù) wiki 的描述,進程是一個正在執(zhí)行的計算機程序,這里說的計算機程序是指的是能夠直接被操作系統(tǒng)加載執(zhí)行的程序,比如你通過編譯器編譯之后的 c/c++ 程序。
舉個例子,你在 shell 當中敲出的 ./a.out
在按下回車之后,a.out
就會被執(zhí)行起來,這個被操作系統(tǒng)執(zhí)行的程序就是一個進程。在一個進程內部會有很多的資源,比如打開的文件,申請的內存,接收到的信號等等,這些信息都是由內核來維護。關于進程有一個非常重要的概念,就是進程的內存地址空間,一個進程當中主要有代碼、數(shù)據(jù)、堆和執(zhí)行棧:
這里我們不過多的去分析這一點,現(xiàn)在就需要知道在一個進程當中主要有這 4 個東西,而且在內核當中會有數(shù)據(jù)結構去保存他們。程序被操作系統(tǒng)加載之后可以被操作系統(tǒng)放到 CPU 上運行。我們可以同時啟動多個進程,讓操作系統(tǒng)去調度,而且隨著體系結構的發(fā)展,現(xiàn)在的機器上都是多核機器,同時啟動多個進程可以讓他們同時執(zhí)行。
在編程時我們會有一個需求,我們希望并行的去執(zhí)行程序,而且他們可以修改共有的內存,當一個進程修改之后能夠被另外一個進程看到,從這個角度來說他們就需要有同一個地址空間,這樣就可以實現(xiàn)這一點了,而且這種方式有一個好處就是節(jié)省內存資源,比如只需要保存一份內存的地址空間了。
上面談到的實現(xiàn)進程的方式實際上被稱作輕量級進程,也被叫做線程。具體來說就是可以在一個進程內部啟動多個線程,這些線程之前有這相同的內存地址空間,這些線程能夠同時被操作系統(tǒng)調度到不同的核心上同時執(zhí)行。我們現(xiàn)在在 linux 上使用的線程是NPTL (Native POSIX Threads Library),從 glibc2.3.2 開始支持,而且要求 linux 2.6 之后的特性。在前面的內容我們談到了,在同一個進程內部的線程是可以共享一些進程擁有的數(shù)據(jù)的,比如:
- 進程號。
- 父進程號。
- 進程組號和會話號。
- 控制終端。
- 打開的文件描述符表。
- 當前工作目錄。
- 虛擬地址空間。
線程也有自己的私有數(shù)據(jù),比如:
- 程序執(zhí)行棧空間。
- 寄存器狀態(tài)。
- 線程的線程號。
在 linux 當中創(chuàng)建線程和進程的系統(tǒng)調用分別為 clone
和 fork
,如果為了創(chuàng)建線程的話我們可以不使用這么低層級的 API,我們可以通過 NPTL 提供的 pthread_create
方法創(chuàng)建線程執(zhí)行相應的方法。
#include <stdio.h> #include <pthread.h> void* func(void* arg) { printf("Hello World\n"); return NULL; } int main() { pthread_t t; // 定義一個線程 pthread_create(&t, NULL, func, NULL); // 創(chuàng)建線程并且執(zhí)行函數(shù) func // wait unit thread t finished pthread_join(t, NULL); // 主線程等待線程 t 執(zhí)行完成然后主線程才繼續(xù)往下執(zhí)行 printf("thread t has finished\n"); return 0; }
編譯上述程序:
clang helloworld.c -o helloworld.out -lpthread 或者 gcc helloworld.c -o helloworld.out -lpthread
在上面的代碼當中主線程(可以認為是執(zhí)行主函數(shù)的線程)首先定義一個線程,然后創(chuàng)建線程并且執(zhí)行函數(shù) func ,當創(chuàng)建完成之后,主線程使用 pthread_join 阻塞自己,直到等待線程 t 執(zhí)行完成之后主線程才會繼續(xù)往下執(zhí)行。
我們現(xiàn)在仔細分析一下 pthread_create
的函數(shù)簽名,并且對他的參數(shù)進行詳細分析:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
- 參數(shù) thread 是一個類型為 pthread_t 的指針對象,將這個對象會在 pthread_create 內部會被賦值為存放線程 id 的地址,在后文當中我們將使用一個例子仔細的介紹這個參數(shù)的含義。
- 參數(shù) attr 是一個類型為 pthread_attr_t 的指針對象,我們可以在這個對象當中設置線程的各種屬性,比如說線程取消的狀態(tài)和類別,線程使用的棧的大小以及棧的初始位置等等,在后文當中我們將詳細介紹這個屬性的使用方法,當這個屬性為 NULL 的時候,使用默認的屬性值。
- 參數(shù) start_routine 是一個返回類型為 void,參數(shù)類型為 void 的函數(shù)指針,指向線程需要執(zhí)行的函數(shù),線程執(zhí)行完成這個函數(shù)之后線程就會退出。
- 參數(shù) arg ,傳遞給函數(shù) start_routine 的一個參數(shù),在上一條當中我們提到了 start_routine 有一個參數(shù),是一個 void 類型的指針,這個參數(shù)也是一個 void 類型的指針,在后文當中我們使用一個例子說明這個參數(shù)的使用方法。
在 Python 當中可以通過 threading 來創(chuàng)建一個線程:
import threading def func(): print("Hello World") if __name__ == '__main__': t = threading.Thread(target=func) t.start() t.join()
現(xiàn)在有一個問題是,在 Python 當中真的是使用 pthread_create 來創(chuàng)建線程的嗎(在 Linux 當中)?Python 當中的線程和我們常說的線程是一致的嗎?
我們現(xiàn)在來分析一下 threading 的源代碼,線程的 start (也就是 Thread 類的 start 方法)方法如下:
def start(self): if not self._initialized: raise RuntimeError("thread.__init__() not called") if self._started.is_set(): raise RuntimeError("threads can only be started once") with _active_limbo_lock: _limbo[self] = self try: _start_new_thread(self._bootstrap, ()) except Exception: with _active_limbo_lock: del _limbo[self] raise self._started.wait()
在上面的代碼當中最核心的一行代碼就是 _start_new_thread(self._bootstrap, ())
,這行代碼的含義是啟動一個新的線程去執(zhí)行 self._bootstrap
,在 self._bootstrap
當中會調用 _bootstrap_inner
,在 _bootstrap_inner
當中會調用 Thread 的 run 方法,而在run
方法當中最終調用了我們傳遞給 Thread 類的函數(shù)。
def run(self): try: if self._target is not None: self._target(*self._args, **self._kwargs) finally: # Avoid a refcycle if the thread is running a function with # an argument that has a member that points to the thread. del self._target, self._args, self._kwargs def _bootstrap(self): try: self._bootstrap_inner() except: if self._daemonic and _sys is None: return raise def _bootstrap_inner(self): try: self._set_ident() self._set_tstate_lock() if _HAVE_THREAD_NATIVE_ID: self._set_native_id() self._started.set() with _active_limbo_lock: _active[self._ident] = self del _limbo[self] if _trace_hook: _sys.settrace(_trace_hook) if _profile_hook: _sys.setprofile(_profile_hook) try: self.run() except: self._invoke_excepthook(self) finally: self._delete()
現(xiàn)在的問題是 _start_new_thread 是如何實現(xiàn)的?這個方法是 CPython 內部使用 C 語言實現(xiàn)的方法,在這里我們不再將全部的細節(jié)進行分析,只討論大致的流程。
在執(zhí)行 _start_new_thread 時,最終會調用PyThread_start_new_thread
這個方法,第一個參數(shù)是一個函數(shù),這個函數(shù)為 t_bootstrap
,在PyThread_start_new_thread
當中會使用 pthread_create
創(chuàng)建一個新的線程執(zhí)行 t_bootstrap
函數(shù),在函數(shù) t_bootstrap
當中會調用從 Python 層面當中傳遞過來的 _bootstrap
方法。
long PyThread_start_new_thread(void (*func)(void *), void *arg) { pthread_t th; int status; pthread_attr_t attrs; size_t tss; if (!initialized) PyThread_init_thread(); if (pthread_attr_init(&attrs) != 0) return -1; tss = (_pythread_stacksize != 0) ? _pythread_stacksize : THREAD_STACK_SIZE; if (tss != 0) { if (pthread_attr_setstacksize(&attrs, tss) != 0) { pthread_attr_destroy(&attrs); return -1; } } pthread_attr_setscope(&attrs, PTHREAD_SCOPE_SYSTEM); status = pthread_create(&th, &attrs, (void* (*)(void *))func, (void *)arg ); // 創(chuàng)建新線程執(zhí)行函數(shù) func,也就是傳遞過來的函數(shù) t_bootstrap(函數(shù)內容見下方) // 在執(zhí)行完上面的代碼之后線程就會立即執(zhí)行了不需要像 Python 當中的線程一樣需要調用 start pthread_attr_destroy(&attrs); if (status != 0) return -1; pthread_detach(th); return (long) th; } static void t_bootstrap(void *boot_raw) { struct bootstate *boot = (struct bootstate *) boot_raw; PyThreadState *tstate; PyObject *res; tstate = boot->tstate; tstate->thread_id = PyThread_get_thread_ident(); _PyThreadState_Init(tstate); PyEval_AcquireThread(tstate); nb_threads++; // boot->func 就是從 Python 層面?zhèn)鬟f過來的 _bootstrap // PyEval_CallObjectWithKeywords 就是調用 Python 層面的函數(shù) // 下面這行代碼就是在創(chuàng)建線程后執(zhí)行的 Python 代碼 res = PyEval_CallObjectWithKeywords( boot->func, boot->args, boot->keyw); if (res == NULL) { if (PyErr_ExceptionMatches(PyExc_SystemExit)) PyErr_Clear(); else { PyObject *file; PySys_WriteStderr( "Unhandled exception in thread started by "); file = PySys_GetObject("stderr"); if (file != NULL && file != Py_None) PyFile_WriteObject(boot->func, file, 0); else PyObject_Print(boot->func, stderr, 0); PySys_WriteStderr("\n"); PyErr_PrintEx(0); } } else Py_DECREF(res); Py_DECREF(boot->func); Py_DECREF(boot->args); Py_XDECREF(boot->keyw); PyMem_DEL(boot_raw); nb_threads--; PyThreadState_Clear(tstate); PyThreadState_DeleteCurrent(); PyThread_exit_thread(); }
從上面的整個創(chuàng)建線程的流程來看,當我們在 Python 層面創(chuàng)建一個線程之后,最終會調用 pthread_create
函數(shù),真正創(chuàng)建一個線程(我們在前面已經(jīng)討論過這種線程能夠被操作系統(tǒng)調度在 CPU 上運行,如果是多核機器的話,這兩個線程可以在同一個時刻運行)去執(zhí)行相應的 Python 代碼,也就是說當我們使用 threading 模塊創(chuàng)建一個線程的時候,最終確實使用了 pthread_create
創(chuàng)建了一個線程。
協(xié)程
Coroutines are computer program components that allow execution to be suspended and resumed, generalizing subroutines for cooperative multitasking.
根據(jù) wiki 的描述,協(xié)程是一個允許停下來和恢復執(zhí)行的程序。在 Python 當中協(xié)程是基于生成器實現(xiàn)的(如果想具體了解生成器和協(xié)程的實現(xiàn)原理,因為生成器是滿足這個要求的,他可以讓程序執(zhí)行到函數(shù)的某一部分停下來,然后還能夠繼續(xù)恢復執(zhí)行。
在繼續(xù)分析協(xié)程之前我們來討論一下協(xié)程的應用場景。現(xiàn)在假如需要處理很多網(wǎng)絡請求,一個線程處理一個請求,當處理一個請求的時候我們需要等待客戶端的響應,線程在等待客戶端響應的時候是處于阻塞狀態(tài)不需要使用 CPU,假設 CPU 的使用率為 0.0001%,那么我們大概需要 1000000 個線程才能夠將 CPU 的使用率達到 100%,而通常我們在內核創(chuàng)建一個線程大概需要 2MB 的內存,4GB 內存大概能夠創(chuàng)建 2048 個線程,這遠遠達不到我們需要創(chuàng)建的線程個數(shù)。而我們可以通過創(chuàng)建協(xié)程來達到這一點要求,因為協(xié)程需要的內存比線程小的多,而且協(xié)程是在用戶態(tài)實現(xiàn)的,不同的編程語言可以根據(jù)語言本身的情況進行實現(xiàn)。而我們在前面說明了一個線程可以被掛起,掛起之后也可以被繼續(xù)執(zhí)行,我們可以利用這一點,當協(xié)程發(fā)送一個網(wǎng)絡請求之后就被掛起,這個時候切換到其他協(xié)程繼續(xù)執(zhí)行,這樣就可以讓一個線程充分利用 CPU 的資源。對應的偽代碼如下:
def recv(socket): while True: try: data = socket.recv() # 接收到數(shù)據(jù)了 return data except BlockingIOError: yield # 讓出 CPU 的執(zhí)行權,也就是將協(xié)程暫停,讓其他協(xié)程運行起來
在 Python 當中和協(xié)程非常相關的另外一個概念就是事件循環(huán) (Eventloop),我們將需要運行的協(xié)程都加入到事件循環(huán)當中,當有協(xié)程讓出 CPU 的執(zhí)行權的之后,整個程序的流程就退回到了事件循環(huán)上,此時事件循環(huán)再運行另外一個協(xié)程,這樣就能夠充分利用 CPU 的性能了。事件循環(huán)的執(zhí)行流程大致如下所示:
def event_loop(): coroutines = [...] while coroutines.is_not_empty(): coroutine = get_a_coroutine(coroutines) res = coroutine.run() # 當程序從這里返回的時候要么是協(xié)程停下來了,要么是協(xié)程執(zhí)行完成了 if coroutine.is_not_finished(): append(coroutines)
線程和進程的概念相對來說比較容易理解,協(xié)程比較困難,協(xié)程是用戶態(tài)實現(xiàn)的,它是由編程語言自己來進行調度,而不是由操作系統(tǒng)進行調度的,這是他和線程和進程最大的區(qū)別,而且協(xié)程相比起線程和進程來說需要的內存資源更少。
對于我們在實際編程當中來說,只有當你的程序由很多 IO 密集型的程序的時候才需要考慮使用協(xié)程,比如服務器開發(fā)。這是因為只有在這種場景下才能夠發(fā)揮協(xié)程的性能,如果你的程序是計算密集型的程序就不需要使用協(xié)程了,因為協(xié)程相對于線程來說還會有協(xié)程切換的開銷。
總結
在本篇文章當中主要討論了進程、線程和協(xié)程的區(qū)別,以及在 Linux 當中創(chuàng)建線程的 API,以及 CPython 當中創(chuàng)建線程的流程,最后討論了一下協(xié)程的使用場景,為什么需要使用協(xié)程以及在 Python 當中是如何使用協(xié)程的。只有當你的程序是有比較多的 IO 操作的時候,你才需要考慮使用協(xié)程,因為協(xié)程提升的是 CPU 的利用率,如果你的程序本來 CPU 利用率就很高了,比如有很多的數(shù)學計算,你就不需要使用協(xié)程了,這樣做就可以避免額外的切換開銷了。
本篇文章是深入理解 python 虛擬機系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython
以上就是深入理解Python虛擬機之進程、線程和協(xié)程區(qū)別詳解的詳細內容,更多關于Python進程、線程和協(xié)程的資料請關注腳本之家其它相關文章!
相關文章
在PyCharm中安裝PyTorch、torchvision和OpenCV詳解
這篇文章主要介紹了在PyCharm中安裝PyTorch、torchvision和OpenCV方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-04-04使用python中的in ,not in來檢查元素是不是在列表中的方法
今天小編就為大家分享一篇使用python中的in ,not in來檢查元素是不是在列表中的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-07-07