欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

深入理解Python虛擬機(jī)之進(jìn)程、線程和協(xié)程區(qū)別詳解

 更新時(shí)間:2023年10月22日 08:34:45   作者:一無是處的研究僧  
在本篇文章當(dāng)中深入分析在 Python 當(dāng)中 進(jìn)程、線程和協(xié)程的區(qū)別,這三個(gè)概念會(huì)讓人非常迷惑,如果沒有深入了解這三者的實(shí)現(xiàn)原理,只是看一些文字說明,也很難理解,在本篇文章當(dāng)中我們將通過分析部分源代碼來詳細(xì)分析一下這三者根本的區(qū)別是什么,需要的朋友可以參考下

深入理解 Python 虛擬機(jī):進(jìn)程、線程和協(xié)程

進(jìn)程和線程

進(jìn)程是一個(gè)非常古老的概念,根據(jù) wiki 的描述,進(jìn)程是一個(gè)正在執(zhí)行的計(jì)算機(jī)程序,這里說的計(jì)算機(jī)程序是指的是能夠直接被操作系統(tǒng)加載執(zhí)行的程序,比如你通過編譯器編譯之后的 c/c++ 程序。

舉個(gè)例子,你在 shell 當(dāng)中敲出的 ./a.out 在按下回車之后,a.out 就會(huì)被執(zhí)行起來,這個(gè)被操作系統(tǒng)執(zhí)行的程序就是一個(gè)進(jìn)程。在一個(gè)進(jìn)程內(nèi)部會(huì)有很多的資源,比如打開的文件,申請(qǐng)的內(nèi)存,接收到的信號(hào)等等,這些信息都是由內(nèi)核來維護(hù)。關(guān)于進(jìn)程有一個(gè)非常重要的概念,就是進(jìn)程的內(nèi)存地址空間,一個(gè)進(jìn)程當(dāng)中主要有代碼、數(shù)據(jù)、堆和執(zhí)行棧:

這里我們不過多的去分析這一點(diǎn),現(xiàn)在就需要知道在一個(gè)進(jìn)程當(dāng)中主要有這 4 個(gè)東西,而且在內(nèi)核當(dāng)中會(huì)有數(shù)據(jù)結(jié)構(gòu)去保存他們。程序被操作系統(tǒng)加載之后可以被操作系統(tǒng)放到 CPU 上運(yùn)行。我們可以同時(shí)啟動(dòng)多個(gè)進(jìn)程,讓操作系統(tǒng)去調(diào)度,而且隨著體系結(jié)構(gòu)的發(fā)展,現(xiàn)在的機(jī)器上都是多核機(jī)器,同時(shí)啟動(dòng)多個(gè)進(jìn)程可以讓他們同時(shí)執(zhí)行。

在編程時(shí)我們會(huì)有一個(gè)需求,我們希望并行的去執(zhí)行程序,而且他們可以修改共有的內(nèi)存,當(dāng)一個(gè)進(jìn)程修改之后能夠被另外一個(gè)進(jìn)程看到,從這個(gè)角度來說他們就需要有同一個(gè)地址空間,這樣就可以實(shí)現(xiàn)這一點(diǎn)了,而且這種方式有一個(gè)好處就是節(jié)省內(nèi)存資源,比如只需要保存一份內(nèi)存的地址空間了。

上面談到的實(shí)現(xiàn)進(jìn)程的方式實(shí)際上被稱作輕量級(jí)進(jìn)程,也被叫做線程。具體來說就是可以在一個(gè)進(jìn)程內(nèi)部啟動(dòng)多個(gè)線程,這些線程之前有這相同的內(nèi)存地址空間,這些線程能夠同時(shí)被操作系統(tǒng)調(diào)度到不同的核心上同時(shí)執(zhí)行。我們現(xiàn)在在 linux 上使用的線程是NPTL (Native POSIX Threads Library),從 glibc2.3.2 開始支持,而且要求 linux 2.6 之后的特性。在前面的內(nèi)容我們談到了,在同一個(gè)進(jìn)程內(nèi)部的線程是可以共享一些進(jìn)程擁有的數(shù)據(jù)的,比如:

  • 進(jìn)程號(hào)。
  • 父進(jìn)程號(hào)。
  • 進(jìn)程組號(hào)和會(huì)話號(hào)。
  • 控制終端。
  • 打開的文件描述符表。
  • 當(dāng)前工作目錄。
  • 虛擬地址空間。

線程也有自己的私有數(shù)據(jù),比如:

  • 程序執(zhí)行??臻g。
  • 寄存器狀態(tài)。
  • 線程的線程號(hào)。

在 linux 當(dāng)中創(chuàng)建線程和進(jìn)程的系統(tǒng)調(diào)用分別為 clonefork,如果為了創(chuàng)建線程的話我們可以不使用這么低層級(jí)的 API,我們可以通過 NPTL 提供的 pthread_create 方法創(chuàng)建線程執(zhí)行相應(yīng)的方法。

#include <stdio.h>
#include <pthread.h>

void* func(void* arg) {
  printf("Hello World\n");
  return NULL;
}

int main() {

  pthread_t t; // 定義一個(gè)線程
  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

在上面的代碼當(dāng)中主線程(可以認(rèn)為是執(zhí)行主函數(shù)的線程)首先定義一個(gè)線程,然后創(chuàng)建線程并且執(zhí)行函數(shù) func ,當(dāng)創(chuàng)建完成之后,主線程使用 pthread_join 阻塞自己,直到等待線程 t 執(zhí)行完成之后主線程才會(huì)繼續(xù)往下執(zhí)行。

我們現(xiàn)在仔細(xì)分析一下 pthread_create 的函數(shù)簽名,并且對(duì)他的參數(shù)進(jìn)行詳細(xì)分析:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);
  • 參數(shù) thread 是一個(gè)類型為 pthread_t 的指針對(duì)象,將這個(gè)對(duì)象會(huì)在 pthread_create 內(nèi)部會(huì)被賦值為存放線程 id 的地址,在后文當(dāng)中我們將使用一個(gè)例子仔細(xì)的介紹這個(gè)參數(shù)的含義。
  • 參數(shù) attr 是一個(gè)類型為 pthread_attr_t 的指針對(duì)象,我們可以在這個(gè)對(duì)象當(dāng)中設(shè)置線程的各種屬性,比如說線程取消的狀態(tài)和類別,線程使用的棧的大小以及棧的初始位置等等,在后文當(dāng)中我們將詳細(xì)介紹這個(gè)屬性的使用方法,當(dāng)這個(gè)屬性為 NULL 的時(shí)候,使用默認(rèn)的屬性值。
  • 參數(shù) start_routine 是一個(gè)返回類型為 void,參數(shù)類型為 void 的函數(shù)指針,指向線程需要執(zhí)行的函數(shù),線程執(zhí)行完成這個(gè)函數(shù)之后線程就會(huì)退出。
  • 參數(shù) arg ,傳遞給函數(shù) start_routine 的一個(gè)參數(shù),在上一條當(dāng)中我們提到了 start_routine 有一個(gè)參數(shù),是一個(gè) void 類型的指針,這個(gè)參數(shù)也是一個(gè) void 類型的指針,在后文當(dāng)中我們使用一個(gè)例子說明這個(gè)參數(shù)的使用方法。

在 Python 當(dāng)中可以通過 threading 來創(chuàng)建一個(gè)線程:

import threading

def func():
	print("Hello World")


if __name__ == '__main__':
	t = threading.Thread(target=func)
	t.start()
	t.join()

現(xiàn)在有一個(gè)問題是,在 Python 當(dāng)中真的是使用 pthread_create 來創(chuàng)建線程的嗎(在 Linux 當(dāng)中)?Python 當(dāng)中的線程和我們常說的線程是一致的嗎?

我們現(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()
 

在上面的代碼當(dāng)中最核心的一行代碼就是 _start_new_thread(self._bootstrap, ()),這行代碼的含義是啟動(dòng)一個(gè)新的線程去執(zhí)行 self._bootstrap ,在 self._bootstrap 當(dāng)中會(huì)調(diào)用 _bootstrap_inner,在 _bootstrap_inner 當(dāng)中會(huì)調(diào)用 Thread 的 run 方法,而在run方法當(dāng)中最終調(diào)用了我們傳遞給 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 是如何實(shí)現(xiàn)的?這個(gè)方法是 CPython 內(nèi)部使用 C 語言實(shí)現(xiàn)的方法,在這里我們不再將全部的細(xì)節(jié)進(jìn)行分析,只討論大致的流程。

在執(zhí)行 _start_new_thread 時(shí),最終會(huì)調(diào)用PyThread_start_new_thread 這個(gè)方法,第一個(gè)參數(shù)是一個(gè)函數(shù),這個(gè)函數(shù)為 t_bootstrap,在PyThread_start_new_thread 當(dāng)中會(huì)使用 pthread_create 創(chuàng)建一個(gè)新的線程執(zhí)行 t_bootstrap 函數(shù),在函數(shù) t_bootstrap 當(dāng)中會(huì)調(diào)用從 Python 層面當(dāng)中傳遞過來的 _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ù)內(nèi)容見下方)
    // 在執(zhí)行完上面的代碼之后線程就會(huì)立即執(zhí)行了不需要像 Python 當(dāng)中的線程一樣需要調(diào)用 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 就是調(diào)用 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();
}

從上面的整個(gè)創(chuàng)建線程的流程來看,當(dāng)我們?cè)?Python 層面創(chuàng)建一個(gè)線程之后,最終會(huì)調(diào)用 pthread_create 函數(shù),真正創(chuàng)建一個(gè)線程(我們?cè)谇懊嬉呀?jīng)討論過這種線程能夠被操作系統(tǒng)調(diào)度在 CPU 上運(yùn)行,如果是多核機(jī)器的話,這兩個(gè)線程可以在同一個(gè)時(shí)刻運(yùn)行)去執(zhí)行相應(yīng)的 Python 代碼,也就是說當(dāng)我們使用 threading 模塊創(chuàng)建一個(gè)線程的時(shí)候,最終確實(shí)使用了 pthread_create 創(chuàng)建了一個(gè)線程。

協(xié)程

Coroutines are computer program components that allow execution to be suspended and resumed, generalizing subroutines for cooperative multitasking.

根據(jù) wiki 的描述,協(xié)程是一個(gè)允許停下來和恢復(fù)執(zhí)行的程序。在 Python 當(dāng)中協(xié)程是基于生成器實(shí)現(xiàn)的(如果想具體了解生成器和協(xié)程的實(shí)現(xiàn)原理,因?yàn)樯善魇菨M足這個(gè)要求的,他可以讓程序執(zhí)行到函數(shù)的某一部分停下來,然后還能夠繼續(xù)恢復(fù)執(zhí)行。

在繼續(xù)分析協(xié)程之前我們來討論一下協(xié)程的應(yīng)用場景。現(xiàn)在假如需要處理很多網(wǎng)絡(luò)請(qǐng)求,一個(gè)線程處理一個(gè)請(qǐng)求,當(dāng)處理一個(gè)請(qǐng)求的時(shí)候我們需要等待客戶端的響應(yīng),線程在等待客戶端響應(yīng)的時(shí)候是處于阻塞狀態(tài)不需要使用 CPU,假設(shè) CPU 的使用率為 0.0001%,那么我們大概需要 1000000 個(gè)線程才能夠?qū)?CPU 的使用率達(dá)到 100%,而通常我們?cè)趦?nèi)核創(chuàng)建一個(gè)線程大概需要 2MB 的內(nèi)存,4GB 內(nèi)存大概能夠創(chuàng)建 2048 個(gè)線程,這遠(yuǎn)遠(yuǎn)達(dá)不到我們需要?jiǎng)?chuàng)建的線程個(gè)數(shù)。而我們可以通過創(chuàng)建協(xié)程來達(dá)到這一點(diǎn)要求,因?yàn)閰f(xié)程需要的內(nèi)存比線程小的多,而且協(xié)程是在用戶態(tài)實(shí)現(xiàn)的,不同的編程語言可以根據(jù)語言本身的情況進(jìn)行實(shí)現(xiàn)。而我們?cè)谇懊嬲f明了一個(gè)線程可以被掛起,掛起之后也可以被繼續(xù)執(zhí)行,我們可以利用這一點(diǎn),當(dāng)協(xié)程發(fā)送一個(gè)網(wǎng)絡(luò)請(qǐng)求之后就被掛起,這個(gè)時(shí)候切換到其他協(xié)程繼續(xù)執(zhí)行,這樣就可以讓一個(gè)線程充分利用 CPU 的資源。對(duì)應(yīng)的偽代碼如下:

def recv(socket):
  while True:
    try:
      data = socket.recv() # 接收到數(shù)據(jù)了
    	return data
    except BlockingIOError:
      yield # 讓出 CPU 的執(zhí)行權(quán),也就是將協(xié)程暫停,讓其他協(xié)程運(yùn)行起來

在 Python 當(dāng)中和協(xié)程非常相關(guān)的另外一個(gè)概念就是事件循環(huán) (Eventloop),我們將需要運(yùn)行的協(xié)程都加入到事件循環(huán)當(dāng)中,當(dāng)有協(xié)程讓出 CPU 的執(zhí)行權(quán)的之后,整個(gè)程序的流程就退回到了事件循環(huán)上,此時(shí)事件循環(huán)再運(yùn)行另外一個(gè)協(xié)程,這樣就能夠充分利用 CPU 的性能了。事件循環(huán)的執(zhí)行流程大致如下所示:

def event_loop():
  coroutines = [...]
  while coroutines.is_not_empty():
    coroutine = get_a_coroutine(coroutines)
    res = coroutine.run() # 當(dāng)程序從這里返回的時(shí)候要么是協(xié)程停下來了,要么是協(xié)程執(zhí)行完成了
    if coroutine.is_not_finished():
      append(coroutines)

線程和進(jìn)程的概念相對(duì)來說比較容易理解,協(xié)程比較困難,協(xié)程是用戶態(tài)實(shí)現(xiàn)的,它是由編程語言自己來進(jìn)行調(diào)度,而不是由操作系統(tǒng)進(jìn)行調(diào)度的,這是他和線程和進(jìn)程最大的區(qū)別,而且協(xié)程相比起線程和進(jìn)程來說需要的內(nèi)存資源更少。

對(duì)于我們?cè)趯?shí)際編程當(dāng)中來說,只有當(dāng)你的程序由很多 IO 密集型的程序的時(shí)候才需要考慮使用協(xié)程,比如服務(wù)器開發(fā)。這是因?yàn)橹挥性谶@種場景下才能夠發(fā)揮協(xié)程的性能,如果你的程序是計(jì)算密集型的程序就不需要使用協(xié)程了,因?yàn)閰f(xié)程相對(duì)于線程來說還會(huì)有協(xié)程切換的開銷。

總結(jié)

在本篇文章當(dāng)中主要討論了進(jìn)程、線程和協(xié)程的區(qū)別,以及在 Linux 當(dāng)中創(chuàng)建線程的 API,以及 CPython 當(dāng)中創(chuàng)建線程的流程,最后討論了一下協(xié)程的使用場景,為什么需要使用協(xié)程以及在 Python 當(dāng)中是如何使用協(xié)程的。只有當(dāng)你的程序是有比較多的 IO 操作的時(shí)候,你才需要考慮使用協(xié)程,因?yàn)閰f(xié)程提升的是 CPU 的利用率,如果你的程序本來 CPU 利用率就很高了,比如有很多的數(shù)學(xué)計(jì)算,你就不需要使用協(xié)程了,這樣做就可以避免額外的切換開銷了。

本篇文章是深入理解 python 虛擬機(jī)系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython

以上就是深入理解Python虛擬機(jī)之進(jìn)程、線程和協(xié)程區(qū)別詳解的詳細(xì)內(nèi)容,更多關(guān)于Python進(jìn)程、線程和協(xié)程的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • python3爬蟲怎樣構(gòu)建請(qǐng)求header

    python3爬蟲怎樣構(gòu)建請(qǐng)求header

    在本篇內(nèi)容里小編給大家分享了關(guān)于python3爬蟲怎樣構(gòu)建請(qǐng)求header的知識(shí)點(diǎn),需要的朋友們學(xué)習(xí)下。
    2018-12-12
  • linux下python中文亂碼解決方案詳解

    linux下python中文亂碼解決方案詳解

    這篇文章主要介紹了linux下python中文亂碼解決方案詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2019-08-08
  • pytorch中常用的損失函數(shù)用法說明

    pytorch中常用的損失函數(shù)用法說明

    這篇文章主要介紹了pytorch中常用的損失函數(shù)用法說明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-05-05
  • 一文帶你了解Python協(xié)程的詳細(xì)解釋以及例子

    一文帶你了解Python協(xié)程的詳細(xì)解釋以及例子

    協(xié)程不是計(jì)算機(jī)提供的,計(jì)算機(jī)只提供:進(jìn)程、線程。協(xié)程是人工創(chuàng)造的一種用戶態(tài)切換的微進(jìn)程,使用一個(gè)線程去來回切換多個(gè)進(jìn)程,本文就來通過一些示例和大家詳細(xì)聊聊Python中的協(xié)程吧
    2023-03-03
  • Python自動(dòng)掃描出微信不是好友名單的方法

    Python自動(dòng)掃描出微信不是好友名單的方法

    很多人想要清楚已經(jīng)被刪除的好友名單。面對(duì)龐大的好友數(shù)量想要清除談何容易,本文主要介紹了Python自動(dòng)掃描出微信不是好友名單的方法,感興趣的可以了解一下
    2021-05-05
  • Python判斷變量是否已經(jīng)定義的方法

    Python判斷變量是否已經(jīng)定義的方法

    這篇文章主要介紹了Python判斷變量是否已經(jīng)定義的方法,非常實(shí)用的方法,需要的朋友可以參考下
    2014-08-08
  • 在PyCharm中安裝PyTorch、torchvision和OpenCV詳解

    在PyCharm中安裝PyTorch、torchvision和OpenCV詳解

    這篇文章主要介紹了在PyCharm中安裝PyTorch、torchvision和OpenCV方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2025-04-04
  • python類參數(shù)self使用示例

    python類參數(shù)self使用示例

    python中__new__和__init__到底是怎么一回事,使用方法看下面的代碼,大家參考使用吧
    2014-02-02
  • python3操作mysql數(shù)據(jù)庫的方法

    python3操作mysql數(shù)據(jù)庫的方法

    這篇文章主要介紹了python3操作mysql數(shù)據(jù)庫的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-06-06
  • 使用python中的in ,not in來檢查元素是不是在列表中的方法

    使用python中的in ,not in來檢查元素是不是在列表中的方法

    今天小編就為大家分享一篇使用python中的in ,not in來檢查元素是不是在列表中的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2018-07-07

最新評(píng)論