首页 > 系统相关 >进程、线程、协程

进程、线程、协程

时间:2024-11-16 18:44:00浏览次数:3  
标签:协程 函数 任务 线程 内存 进程 执行

进程、线程、协程

文章目录

一、进程的出现

CPU是不知道进程、线程的概念的,CPU只知道做两件事情。

  • 从内存中读取指令;
  • 执行指令,然后继续读取指令。

CPU是从PC(程序计数器)中读取指令的。那么这个PC是什么呢?PC其实存储的是一个内存地址,一个指向内存中下一条要执行指令的地址。

那么PC的初始值是怎么设置的呢?

我们知道CPU要执行的指令来自内存,内存中的指令是从磁盘加载而来的,磁盘中的指令是编译器生成的,编译器又是从哪生成的机器指令呢?答案是我们定义的函数。

在这里插入图片描述

函数被编译后会生成供 CPU 执行的机器指令。那么,如何让 CPU 执行这些指令呢?

显然,我们只需要将编译后的函数的第一条指令的地址放入程序计数器(PC)中。可是,这和进程或线程又有什么关系呢?

为了执行这些机器指令,我们首先需要将它们加载到内存中,并确保设置了正确的程序入口地址。要完成这两个步骤,完全依靠人工是非常繁琐的,因此,聪明的程序员决定编写程序来完成这个过程。

具体而言,机器指令需要被加载到内存中执行,同时需要记录内存的起始地址和长度,确保函数的入口地址被正确设置并写入程序计数器(PC)。为此,我们需要一个结构体来存储这些信息。

type xxx struct{
	strat_addr pointer
	len int
	start_point pointer
}

这个数据结构总得有个名字吧?它用来记录程序在加载到内存中的运行状态,确保程序能够从磁盘被正确加载并运行。于是,程序从磁盘加载到内存并开始执行的状态,就可以称为进程(Process)。

至于 CPU 执行的第一个函数,我们不妨也给它取个响亮的名字。既然它是程序执行的起点,那就叫它 main 函数吧。

最后,这个能把进程从磁盘加载到内存并开始执行的程序也需要个名字。这样,操作系统就诞生了,程序员再也不需要手动去加载程序,一切都自动化了。

二、线程的出现

进程占用了内存中的一块区域,这段区域保存了 CPU 执行的机器指令以及函数运行时的堆栈信息。为了让程序开始执行,我们只需要将 main 函数的第一条指令地址写入程序计数器(PC)中。

既然程序计数器可以指向 main 函数,那么它当然也可以指向其他函数。

没错,当我们将程序计数器指向非 main 函数时,线程 就诞生了。这样,进程中的机器指令就不再局限于单一的入口函数,进程的执行可以通过多个线程并行在不同的内核上执行。

至此,一个进程中的代码可以被多个内核同时执行,从而实现更高效的并行计算。

通过引入线程,多个 CPU 核心可以共享同一个进程的内存空间,并在不同的线程中并行执行不同的任务,这就是现代计算机多核处理的基本原理。

线程处理的任务通常分为两类:

  • 长任务:这类任务通常涉及到磁盘操作,如向磁盘写入数据等。为了提高效率,通常会专门为这些长任务创建独立的线程来执行,避免阻塞主线程。

  • 短任务:短任务比较常见,比如一次网络请求、一次数据库查询等。虽然这些任务本身耗时较短,但如果任务数量庞大,频繁创建和销毁线程会导致性能下降。

创建和销毁线程是需要消耗时间和系统资源的,每个线程还需要自己的栈空间,这可能导致过多的内存消耗。为了解决这个问题,我们引入了线程池,这是一种能够复用线程并有效管理系统资源的机制。

线程池中的核心思想是:通过复用线程来减少频繁创建和销毁线程的开销

线程池有两个主要部分需要处理:

  1. 任务数据:任务需要处理的数据。
  2. 处理函数:负责处理任务的函数。

线程池中的线程会处于阻塞状态,等待从任务队列中获取任务。当生产者向队列中写入任务时,线程池中的某个线程会被唤醒,然后从队列中取出任务,并使用任务数据作为参数调用处理函数。

具体执行流程如下:

while(true) {
    struct task = GetFromQueue();  // 从队列中取出任务
    task->handle(task->data);      // 使用任务中的数据调用处理函数
}

线程池的核心部分:

  1. 任务队列:保存待处理的任务,每个任务通常包含需要处理的数据和一个处理该数据的函数。
  2. 线程池中的线程:这些线程在初始化时就已经创建好,线程处于阻塞状态,等待任务的到来。当有任务加入队列时,线程被唤醒,从队列中取出任务并处理。
  3. 任务处理:线程从队列中取出任务后,调用任务对应的处理函数来执行具体的操作。

三、协程

协程是如何实现的:

从协程的本质出发,它可以看作是一个能够被暂停并恢复执行的函数。

要理解这一点,我们可以类比到篮球比赛的暂停。在比赛过程中,裁判可以随时暂停比赛,并记录下比赛状态(比如球的位置、各个球员的位置等),当比赛重新开始时,裁判只需恢复这些记录的状态,比赛就可以继续进行,就像从未被暂停一样。

这就是协程的核心思想:在执行过程中,协程会保存自己的上下文,并在需要时恢复这些上下文,从而从上次暂停的地方继续执行。

协程与函数的区别

普通函数的执行是线性的,一旦开始执行,直到函数返回时,程序才会继续执行其他任务。而协程不同,它可以在执行的任意时刻通过 yield 暂停,保存当前执行状态(即上下文),然后返回给调用者。当协程需要恢复时,它会从上次暂停的位置继续执行,而不是从头开始。

协程如何实现?

函数的运行状态(上下文)

在普通函数执行时,所有的局部变量、调用栈等信息都存储在栈中(栈帧)。如果我们要暂停函数的执行,就需要保存当前函数的执行状态,这个状态即为函数的上下文。

在这里插入图片描述

从图中我们可以看出,该进程中只有一个线程,栈区中有四个栈帧,main函数调用A函数,A函数调用B函数,B函数调用C函数,当C函数在运行时整个进程的状态就如图所示。

既然函数的运行时状态保存在栈区的栈帧中,那么如果我们想暂停协程的运行就必须保存整个栈帧的数据,那么我们该将整个栈帧中的数据保存在哪里呢?

在协程被暂停时,它的执行状态(即栈帧)需要被保存。栈通常是在栈区分配的,但为了能在多个执行流之间切换,协程的栈帧需要存储在堆区中,这样可以在协程切换时,保持其状态。栈区用来存储当前执行线程的栈帧,而堆区则为协程提供更灵活的存储空间。

在这里插入图片描述

从图中我们可以看到,该程序中开启了两个协程,这两个协程的栈区都是在堆上分配的,这样我们就可以随时中断或者恢复协程的执行了。

因为堆区是用于长时间存储数据的内存区域,我们可以在堆区为每个协程动态分配栈空间。当协程被暂停时,整个栈帧的内容被保存到堆区。当协程恢复时,操作系统不需要再次复制数据,而是直接从堆区加载保存的栈帧,恢复协程的执行状态。

协程与线程的区别

与线程不同,协程的调度和切换完全发生在用户空间,并不需要操作系统干预。协程的切换开销远小于线程,因为它们不需要涉及内核态和用户态之间的切换。协程通常在堆上分配栈空间,因此可以实现大量并发执行流,而不需要为每个执行流创建一个线程。

多个执行流但只有一个线程

当你创建了多个协程时,操作系统并不“看到”这些协程,因为它们本质上运行在单一线程内。协程的切换完全由程序员控制,通过调度机制实现任务的交替执行。因此,即使创建了多个协程,操作系统看到的仍然是一个线程,而协程的调度、切换则由程序内部管理。

为什么要使用协程?

  1. 高效的并发处理

    协程相较于线程有更低的内存和时间开销。通过在用户空间管理并发任务,程序员能够灵活地控制协程的调度和切换,而不依赖操作系统的线程调度。

  2. 没有线程开销

    线程的创建和切换涉及大量的操作系统开销,而协程的切换仅需要保存和恢复执行上下文,切换开销小得多。这意味着协程适用于需要高并发的场景,可以在不增加大量线程开销的情况下,模拟多个执行流。

  3. 灵活的执行流管理

    程序员可以控制何时暂停和恢复协程执行,使得协程能够高效地完成并发任务,而不需要操作系统干预。这使得协程特别适用于事件驱动或异步编程模式。

本篇文章总结于以下文章:

1、https://mp.weixin.qq.com/s?__biz=MzUxODAzNDg4NQ==&mid=2247488003&idx=2&sn=cc32dee02330123d9f4081cefa531271&chksm=f98e56a9cef9dfbf1bb6b3a1e0f23ed07f37a541c9b0092d929049fcb67f1b80896575fa35ba&token=1339272147&lang=zh_CN#rd

2、https://mp.weixin.qq.com/s?__biz=Mzg4OTYzODM4Mw==&mid=2247485705&idx=1&sn=1845875575601b23ed5cea0579c1f77e&source=41#wechat_redirect

标签:协程,函数,任务,线程,内存,进程,执行
From: https://blog.csdn.net/m0_73337964/article/details/143821382

相关文章

  • 多线程进阶
    1.常见的锁策略如果你自己实现一把锁,你认为标准库给你提供的锁不够用,这个时候你就需要关注锁策略,其实synchronized已经非常好用了足够覆盖大多数的使用场景。这里的锁策略不是和java强相关的,其他语言但凡涉及到并发编程,设计到锁都可以谈到这样的锁策略。1.1乐观锁VS悲观锁......
  • 几个有意思的多线程问题 & 有趣现象笔记
    信号量释放的时候线程被带入的问题SemaphoreSlim和多线程使用的时候,.Release()时,应该在新的线程去做Release操作同理,因为Release时会切换到await等待的代码执行,也就是调用SemaphoreSlim.Release的线程被带入到了awaitSemaphoreSlim.WaitAsync()的代码执行,如果是一个......
  • Java基础——多线程
    1.线程是一个程序内部的一条执行流程程序中如果只有一条执行流程,那这个程序就是单线程的程序2.多线程指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)2.1.如何创建多条线程Java通过java.lang.Thread类的对象来代表线程2.1.1.方式一:继承Thread类//1......
  • 【Linux进程篇1】认识冯·诺依曼体系结构(引出进程详解)
    ---------------------------------------------------------------------------------------------------------------------------------每日鸡汤:用这生命中的每一秒,给自己一个不后悔的未来。-------------------------------------------------------------------------......
  • Linux:进程状态
    文章目录前言一、初识fork1.1fork函数的介绍1.2fork出的子进程存在形式1.3写时拷贝二、进程的状态2.1Linux内核源代码2.2理解内核链表(重要)2.3运行状态2.4阻塞状态2.5挂起状态三、Z(zombie)状态,僵尸进程四、孤儿进程总结前言本文将介绍如何利用系统调用......
  • C++ 创建一个线程
            C++11标准库引入了对多线程编程的支持,使得开发者能够以更加标准化的方式创建和管理线程。主要的线程管理方式是通过std::thread类,它可以用来创建、启动和管理线程。下面我将详细介绍如何使用C++标准库创建线程的方法,以及其他一些相关的工具类和概念。1.......
  • 【Linux】:进程信号(信号保存 & 信号处理)
    ✨                         落日一点如红豆,已把相思写满天    ......
  • JUC---多线程下的数据共享(基于ThreadLocal的思考)
    多线程下的数据共享(基于ThreadLocal的思考)起初实在写项目过程中,在完成超时订单自动取消的任务时,使用xxl-job,整个逻辑是需要从订单表中找出过期的订单,然后将其存入订单取消表。存入订单取消表时需要存储用户的信息。我最开始没想那么多,就直接从ThreadLocal中取出用户信息,但......
  • 进程的知识点
    进程的基本概念进程是操作系统中的一个执行单位,代表正在运行的程序实例。每个进程都有自己独立的内存空间和系统资源,独立于其他进程运行。进程的生命周期包括创建、就绪、运行、等待和终止等状态。进程的创建与管理在操作系统中,进程的创建和管理通常通过系统调用实现,如fork(......
  • 【Linux探索学习】第十三弹——进程状态:深入理解操作系统进程状态与Linux操作系统中的
    Linux笔记:https://blog.csdn.net/2301_80220607/category_12805278.html?spm=1001.2014.3001.5482前言:在上篇我们已经讲解了进程的基本内容,也了解了进程在操作系统的重要作用,今天我们正式开始进程的另一个知识点的讲解:进程状态,即一个进程不可能一直处在运行或终止状态中,它......