深入理解 Python 虚拟机:进程、线程和协程

news2024/11/8 17:09:42

深入理解 Python 虚拟机:进程、线程和协程

在本篇文章当中深入分析在 Python 当中 进程、线程和协程的区别,这三个概念会让人非常迷惑。如果没有深入了解这三者的实现原理,只是看一些文字说明,也很难理解。在本篇文章当中我们将通过分析部分源代码来详细分析一下这三者根本的区别是什么,重点是协程的应用场景和在 Python 当中是如何使用协程的,至于协程的实现原理在前面的文章当中已经详细讨论过了 深入理解 Python 虚拟机:协程初探——不过是生成器而已 和 深入理解 Python 虚拟机:生成器停止背后的魔法。

进程和线程

进程是一个非常古老的概念,根据 wiki 的描述,进程是一个正在执行的计算机程序,这里说的计算机程序是指的是能够直接被操作系统加载执行的程序,比如你通过编译器编译之后的 c/c++ 程序。

举个例子,你在 shell 当中敲出的 ./a.out 在按下回车之后,a.out 就会被执行起来,这个被操作系统执行的程序就是一个进程。在一个进程内部会有很多的资源,比如打开的文件,申请的内存,接收到的信号等等,这些信息都是由内核来维护。关于进程有一个非常重要的概念,就是进程的内存地址空间,一个进程当中主要有代码、数据、堆和执行栈:

这里我们不过多的去分析这一点,现在就需要知道在一个进程当中主要有这 4 个东西,而且在内核当中会有数据结构去保存他们。程序被操作系统加载之后可以被操作系统放到 CPU 上运行。我们可以同时启动多个进程,让操作系统去调度,而且随着体系结构的发展,现在的机器上都是多核机器,同时启动多个进程可以让他们同时执行。

在编程时我们会有一个需求,我们希望并行的去执行程序,而且他们可以修改共有的内存,当一个进程修改之后能够被另外一个进程看到,从这个角度来说他们就需要有同一个地址空间,这样就可以实现这一点了,而且这种方式有一个好处就是节省内存资源,比如只需要保存一份内存的地址空间了。

上面谈到的实现进程的方式实际上被称作轻量级进程,也被叫做线程。具体来说就是可以在一个进程内部启动多个线程,这些线程之前有这相同的内存地址空间,这些线程能够同时被操作系统调度到不同的核心上同时执行。我们现在在 linux 上使用的线程是NPTL (Native POSIX Threads Library),从 glibc2.3.2 开始支持,而且要求 linux 2.6 之后的特性。在前面的内容我们谈到了,在同一个进程内部的线程是可以共享一些进程拥有的数据的,比如:

  • 进程号。
  • 父进程号。
  • 进程组号和会话号。
  • 控制终端。
  • 打开的文件描述符表。
  • 当前工作目录。
  • 虚拟地址空间。

线程也有自己的私有数据,比如:

  • 程序执行栈空间。
  • 寄存器状态。
  • 线程的线程号。

在 linux 当中创建线程和进程的系统调用分别为 clonefork,如果为了创建线程的话我们可以不使用这么低层级的 API,我们可以通过 NPTL 提供的 pthread_create 方法创建线程执行相应的方法。

#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); // 创建线程并且执行函数 func 

  // wait unit thread t finished
  pthread_join(t, NULL); // 主线程等待线程 t 执行完成然后主线程才继续往下执行

  printf("thread t has finished\n");
  return 0;
}

编译上述程序:

clang helloworld.c -o helloworld.out -lpthread
或者
gcc helloworld.c -o helloworld.out -lpthread

在上面的代码当中主线程(可以认为是执行主函数的线程)首先定义一个线程,然后创建线程并且执行函数 func ,当创建完成之后,主线程使用 pthread_join 阻塞自己,直到等待线程 t 执行完成之后主线程才会继续往下执行。

我们现在仔细分析一下 pthread_create 的函数签名,并且对他的参数进行详细分析:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);
  • 参数 thread 是一个类型为 pthread_t 的指针对象,将这个对象会在 pthread_create 内部会被赋值为存放线程 id 的地址,在后文当中我们将使用一个例子仔细的介绍这个参数的含义。
  • 参数 attr 是一个类型为 pthread_attr_t 的指针对象,我们可以在这个对象当中设置线程的各种属性,比如说线程取消的状态和类别,线程使用的栈的大小以及栈的初始位置等等,在后文当中我们将详细介绍这个属性的使用方法,当这个属性为 NULL 的时候,使用默认的属性值。
  • 参数 start_routine 是一个返回类型为 void,参数类型为 void 的函数指针,指向线程需要执行的函数,线程执行完成这个函数之后线程就会退出。
  • 参数 arg ,传递给函数 start_routine 的一个参数,在上一条当中我们提到了 start_routine 有一个参数,是一个 void 类型的指针,这个参数也是一个 void 类型的指针,在后文当中我们使用一个例子说明这个参数的使用方法。

在 Python 当中可以通过 threading 来创建一个线程:

import threading

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


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

现在有一个问题是,在 Python 当中真的是使用 pthread_create 来创建线程的吗(在 Linux 当中)?Python 当中的线程和我们常说的线程是一致的吗?

我们现在来分析一下 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, ()),这行代码的含义是启动一个新的线程去执行 self._bootstrap ,在 self._bootstrap 当中会调用 _bootstrap_inner,在 _bootstrap_inner 当中会调用 Thread 的 run 方法,而在run方法当中最终调用了我们传递给 Thread 类的函数。

    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()

现在的问题是 _start_new_thread 是如何实现的?这个方法是 CPython 内部使用 C 语言实现的方法,在这里我们不再将全部的细节进行分析,只讨论大致的流程。

在执行 _start_new_thread 时,最终会调用PyThread_start_new_thread 这个方法,第一个参数是一个函数,这个函数为 t_bootstrap,在PyThread_start_new_thread 当中会使用 pthread_create 创建一个新的线程执行 t_bootstrap 函数,在函数 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
                             ); // 创建新线程执行函数 func,也就是传递过来的函数 t_bootstrap(函数内容见下方)
    // 在执行完上面的代码之后线程就会立即执行了不需要像 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 层面传递过来的 _bootstrap 
    // PyEval_CallObjectWithKeywords 就是调用 Python 层面的函数
    // 下面这行代码就是在创建线程后执行的 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();
}

从上面的整个创建线程的流程来看,当我们在 Python 层面创建一个线程之后,最终会调用 pthread_create 函数,真正创建一个线程(我们在前面已经讨论过这种线程能够被操作系统调度在 CPU 上运行,如果是多核机器的话,这两个线程可以在同一个时刻运行)去执行相应的 Python 代码,也就是说当我们使用 threading 模块创建一个线程的时候,最终确实使用了 pthread_create 创建了一个线程。

协程

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

根据 wiki 的描述,协程是一个允许停下来和恢复执行的程序。在 Python 当中协程是基于生成器实现的(如果想具体了解生成器和协程的实现原理,可以参考这两篇文章 深入理解 Python 虚拟机:协程初探——不过是生成器而已 和 深入理解 Python 虚拟机:生成器停止背后的魔法),因为生成器是满足这个要求的,他可以让程序执行到函数的某一部分停下来,然后还能够继续恢复执行。

在继续分析协程之前我们来讨论一下协程的应用场景。现在假如需要处理很多网络请求,一个线程处理一个请求,当处理一个请求的时候我们需要等待客户端的响应,线程在等待客户端响应的时候是处于阻塞状态不需要使用 CPU,假设 CPU 的使用率为 0.0001%,那么我们大概需要 1000000 个线程才能够将 CPU 的使用率达到 100%,而通常我们在内核创建一个线程大概需要 2MB 的内存,4GB 内存大概能够创建 2048 个线程,这远远达不到我们需要创建的线程个数。而我们可以通过创建协程来达到这一点要求,因为协程需要的内存比线程小的多,而且协程是在用户态实现的,不同的编程语言可以根据语言本身的情况进行实现。而我们在前面说明了一个线程可以被挂起,挂起之后也可以被继续执行,我们可以利用这一点,当协程发送一个网络请求之后就被挂起,这个时候切换到其他协程继续执行,这样就可以让一个线程充分利用 CPU 的资源。对应的伪代码如下:

def recv(socket):
  while True:
    try:
      data = socket.recv() # 接收到数据了
    	return data
    except BlockingIOError:
      yield # 让出 CPU 的执行权,也就是将协程暂停,让其他协程运行起来

在 Python 当中和协程非常相关的另外一个概念就是事件循环 (Eventloop),我们将需要运行的协程都加入到事件循环当中,当有协程让出 CPU 的执行权的之后,整个程序的流程就退回到了事件循环上,此时事件循环再运行另外一个协程,这样就能够充分利用 CPU 的性能了。事件循环的执行流程大致如下所示:

def event_loop():
  coroutines = [...]
  while coroutines.is_not_empty():
    coroutine = get_a_coroutine(coroutines)
    res = coroutine.run() # 当程序从这里返回的时候要么是协程停下来了,要么是协程执行完成了
    if coroutine.is_not_finished():
      append(coroutines)

线程和进程的概念相对来说比较容易理解,协程比较困难,协程是用户态实现的,它是由编程语言自己来进行调度,而不是由操作系统进行调度的,这是他和线程和进程最大的区别,而且协程相比起线程和进程来说需要的内存资源更少(如果想具体了解生成器和协程的实现原理,可以参考这两篇文章 深入理解 Python 虚拟机:协程初探——不过是生成器而已 和 深入理解 Python 虚拟机:生成器停止背后的魔法)。

对于我们在实际编程当中来说,只有当你的程序由很多 IO 密集型的程序的时候才需要考虑使用协程,比如服务器开发。这是因为只有在这种场景下才能够发挥协程的性能,如果你的程序是计算密集型的程序就不需要使用协程了,因为协程相对于线程来说还会有协程切换的开销。

总结

在本篇文章当中主要讨论了进程、线程和协程的区别,以及在 Linux 当中创建线程的 API,以及 CPython 当中创建线程的流程,最后讨论了一下协程的使用场景,为什么需要使用协程以及在 Python 当中是如何使用协程的。只有当你的程序是有比较多的 IO 操作的时候,你才需要考虑使用协程,因为协程提升的是 CPU 的利用率,如果你的程序本来 CPU 利用率就很高了,比如有很多的数学计算,你就不需要使用协程了,这样做就可以避免额外的切换开销了。


本篇文章是深入理解 python 虚拟机系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython

更多精彩内容合集可访问项目:https://github.com/Chang-LeHung/CSCore

关注公众号:一无是处的研究僧,了解更多计算机(Java、Python、计算机系统基础、算法与数据结构)知识。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1114497.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

智慧图书馆视频监控系统方案——助力图书信息化管理

图书馆的藏书一般都是较为宝贵和珍重的&#xff0c;但图书馆的读者较多且复杂&#xff0c;为保护十分珍贵的图书资源&#xff0c;防止图书馆图书的丢失和损坏&#xff0c;TSINGSEE青犀智能视频监控系统应运而生。 1、视频监控系统 安装高清摄像头覆盖图书馆内的关键区域&#…

PMP考试中的常见翻译问题

1、题目中出现的“启动会议”或“启动大会”开工会议&#xff08;kick-off meeting) 2、题目中出现的“回报期” 回收期&#xff08;项目选择的经济模型&#xff09; 3、题目中出现的“增强” 提高&#xff08;风险应对策略&#xff09; 4、题目中出现的“缓解” 减轻&#…

电源模块直流稳压电源不知道如何调试?纳米软件为您科普

调试是一个查错和排错的过程。电源模块调试就是在正常使用之前先通过调试检测其是否存在一些故障&#xff0c;确保在后期使用中整个电路可以稳定运行。直流稳压电源是常用的一种电源供应装置&#xff0c;今天纳米软件将介绍直流稳压电源调试方法。 直流稳压电源调试步骤 稳定直…

29 “select *“ 或者 “select field1, field2“ 的实现

前言 这里我们来探究一下 “select *” 或者 “select 字段列表“ 的相关实现 当然 这一部分在 “mysql union” 里面有具体的体现, 只是 可能没有那么细致 这里 来概览一下 这里的整个流程 select * from tz_test; setup_wild 的地方是处理 “select *” 的地方 外层迭代…

从北京到南京:偶数在能源行业的数据迁移实践

能源行业的数字化转型 当前&#xff0c;大数据技术在以电力为代表的能源行业不断推进&#xff0c;同时&#xff0c;分布式能源、储能、电网技术不断改进&#xff0c;电力行业的数字化转型充满了机遇和挑战。 一方面&#xff0c;电力行业本身自动化程度高、信息化基础好、系统…

去水印app有哪些?这三款良心推荐

如今图片的使用越来越普遍&#xff0c;因此我们经常需要对图片进行编辑和修改。但有些图片可能带有水印&#xff0c;这会影响图片的美观和应用效果。你知道有哪些推荐的图片去水印app吗&#xff1f;以下是介绍的三款可以去水印app&#xff0c;让你的素材更加专业和美观&#xf…

Python高频面试题——如何在字符串中删除指定字符,掌握strip()、replace()和re.sub ()正确使用方法!

关于python删除字符串是面试python测试开发工程师的一个经典问题。问题很简单&#xff0c;但是一下子就能测试出来被面试者是否能够熟练的进行python相关编码工作&#xff01; 对于有些临时抱佛脚的同学来讲&#xff0c;一看删除&#xff0c;很自然就说用remove 、del相关方法…

优化销售策略,突破企业全面预算管理难题

传统的企业年度销售计划往往会消耗企业内部人员很多精力和时间&#xff0c;比如需要收集数据、处理电子表格、确定项目优先级、预测未来发展以及为次年的费用制定预算等。然而随着这些繁琐的工作不断进行&#xff0c;其中的准确性和价值也受到了一定的怀疑。虽然销售计划仍按着…

经纬恒润推出全新一代智能电动座椅模块

随着智能驾驶、智能座舱的广泛应用&#xff0c;人们对于汽车的定位不再局限于代步工具&#xff0c;对于汽车座舱这个私密空间也有了不一样的期待。更安全、更舒适、更智能化的体验将成为未来智能座椅的发展方向&#xff0c;而传统的座椅控制系统已无法满足人们新的需求。 为了…

python3 win环境部署

python3 win环境部署 1.安装包 https://www.python.org/ftp/python/3.12.0/python-3.12.0-amd64.exe安装成功后 ctrlr 输入 cmd 执行 python 检验是否安装成功 pip 镜像源更新 查看pip.ini 文件路径,一般在 python 安装目录下添加 pip.ini 文件即可 pip -v config list​ 创建…

JAVA---RMI详解1

一、RMI简介 The Java Remote Method Invocation (RMI)允许运行在一台虚拟机上的对象调用运行在另一台虚拟机上的对象中的方法。RMI使用stubs and skeletons &#xff08;存根和骨架&#xff09;架构来和远程对象&#xff08;Remote Object&#xff09;沟通 二、相关术语介绍 1…

人脸写真FaceChain风格写真的试玩(二)

接着上一篇【人脸写真FaceChain的简单部署记录&#xff08;一&#xff09;】来试玩一下。 1 无限风格写真 参考&#xff1a;让你拥有专属且万能的AI摄影师AI修图师——FaceChain迎来最大版本更新 1.1 人物形象训练 这里的步骤比较简单&#xff0c;就是选择照片&#xff0c;然…

CentOS7.9离线安装Docker环境

1. 下载合适的Docker安装包 Docker安装包下载地址&#xff1a;Index of linux/static/stable/x86_64/https://download.docker.com/linux/static/stable/x86_64/ 进入地址页面&#xff0c;如下图&#xff1a; 我下载的是&#xff1a;docker-23.0.1.tgz 版本 2. 将下载好的Do…

linux-守护进程daemon

linux-守护进程daemon 代码实现 main.c运行结果 代码实现 main.c //pName&#xff1a;程序名 //facility&#xff1a; 守护进程&#xff0c;输出日志类型 302页 #include<signal.h> #include<syslog.h> #include<fcntl.h> static int daemon_proc 0; #defin…

AQS理解

AQS是JAVA中的一组抽象类&#xff0c;就是为了解决多线程并发竞争共享资源而引发的线程安全问题&#xff0c;细致点说AQS就是具备一套线程阻塞等待以及被唤醒的时候锁分配的机制&#xff0c;这个机制是由队列来实现的&#xff0c;暂时获取不到所的线程加入到队列里面&#xff0…

爬虫/scrapy基础

如果文章对你有帮助&#xff0c;欢迎关注、点赞、收藏一键三连支持以下哦&#xff01; 想要一起交流学习的小伙伴可以加zkaq222&#xff08;备注CSDN&#xff0c;不备注通不过哦&#xff09;进入学习&#xff0c;共同学习进步 目录 0x01 安装和简介 0x02 文件作用 0x04 保存…

【软考-中级】系统集成项目管理工程师-合同管理历年案例

持续更新。。。。。。。。。。。。。。。 目录 2018 下 试题一(17分)系列文章 2018 下 试题一(17分) 阅读下列说明&#xff0c;回答问题 1至问题 3&#xff0c;将解答填入答题纸的对应栏内     某大型央企 A 公司计划开展云数据中心建设项目&#xff0c;并将公司主要业务应…

OpenGL —— 2.8、漫游之摄像机飞行移动(附源码,glfw+glad)

源码效果 C源码 纹理图片 需下载stb_image.h这个解码图片的库&#xff0c;该库只有一个头文件。 具体代码&#xff1a; vertexShader.glsl #version 330 corelayout(location 0) in vec3 aPos; layout(location 1) in vec2 aUV;out vec2 outUV;uniform mat4 _modelMatrix; …

智慧公厕蜕变多功能城市智慧驿站公厕的创新

随着城市发展的不断推进&#xff0c;对公共设施的便利性和智能化要求也日益提高。为满足市民对高品质、便捷、舒适的公共厕所的需求&#xff0c;智慧公厕行业的领航厂家广州中期科技有限公司&#xff0c;全新推出了一体化智慧公厕驿站。凭借着“高科技碳中和物联网创意设计新经…

widnows安装audiocraft

audiocraft介绍 AudioCraft是一个PyTorch库&#xff0c;用于音频生成的深度学习研究。我们可以上传一段音乐&#xff0c;AI会根据音乐以及提示词&#xff0c;生成一段新的音乐。下面来看看具体安装。 首先查看cuda版本 如何安装cuda&#xff0c;可以自行搜索 获取程序 可…