首页 > 系统相关 >多线程篇(并发编程 - 进程&线程&协程&纤程&管程)(持续更新迭代)

多线程篇(并发编程 - 进程&线程&协程&纤程&管程)(持续更新迭代)

时间:2024-09-01 09:25:53浏览次数:7  
标签:协程 纤程 process 管程 线程 进程 多线程

目录

一、进程(Progress)

1. 进程

2. 僵尸进程

2.1 什么是僵尸进程

2.2 僵尸进程的危害

2.3 如何避免僵尸进程的产生

3. 参考链接

二、线程(Thread)

1. 线程是什么?

2. 多线程

2.1. 概述

2.2. 多线程的好处

2.3. 多线程的代价

3. 线程模型(三种)

3.1. 一对一模型

3.2. 多对一模型

3.3 多对多模型

3.4 如何选择线程模型

4. 线程调度(两种)

4.1. 协同式线程调度

4.2. 抢占式线程调度

5. 线程和进程区别

6. 总结

三、绿色线程

1. 绿色线程是什么?

2. Java 世界中的绿色线程

四、协程(微线程)

1. 协程是什么?

2. 协程是怎么来的?

3. 协程的好处有哪些?

4. 总结

五、纤程

1. 纤程是什么?

1.1. 说法一

1.2. 说法二

2. 协程与纤程主要的区别

3. 总结

3.1. 说法一

3.2. 说法二

六、管程(Monitors)

1. 管程是什么?

2. 管程的特征

3. enter、leave、c、wait(c)、signal(c)

4. 总结

七、总结


一、进程(Progress)

1. 进程

进程也就是平时所说的程序,比如在操作系统上运行一个谷歌浏览器,那么就代表着谷歌浏览器

就是一个进程。

进程是操作系统中能够独立运行的个体,并且也作为资源分配的基本单位,由指令、数据、堆栈

等结构组成。

安装好一个程序之后,在程序未曾运行之前也仅是一些文件存储在磁盘上,当启动程序时会向操

作系统申请一定的资源,如 CPU、存储空间和 I/O 设备等,OS 为其分配资源后,会真正的出现

在内存中成为一个抽象的概念:进程。

2. 僵尸进程

2.1 什么是僵尸进程

我们知道,除了初始化进程 init (PID=1) 以外,所有的进程都不是完全孤立存在的,它有其父进

程和兄弟进程。所谓僵尸进程,就是当子进程退出时,父进程尚未结束,而父进程又没有对已经

结束的子进程进行回收。此时,这样的子进程就成了僵尸进程。

考虑另外一种情况,当一个进程使用 fork() 产生了一个子进程,当子进程尚未结束时,父进程已

经退出,则此时的子进程就成了孤儿进程,孤儿进程会被 init 进程收养,当孤儿进程退出时,由

init 进程负责对其进行回收。

下面我们就来亲自制造一个僵尸进程(test.c):利用 fork() 分叉出一个子进程,然后让子进程

退出,父进程继续运行,这样子进程便成了僵尸进程。

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>

int main() {
    pid_t pid;
    pid = fork();

    if (pid < 0) {
        printf("fork failed\n");
        return -1;
    } else if (pid == 0) {
        printf("this is son process with PID=%d\n", getpid());
        printf("son process is over\n");
    } else {
        printf("this is father process with PID=%d\n", getpid());
        while (1) {    // 父进程不会结束
            usleep(100);
        }
    }

    return 0;
}

编译运行:

zhengge@wclass-PC:~/Desktop$ gcc -o test test.c
zhengge@wclass-PC:~/Desktop$ ./test
this is father process with PID=5238
this is son process with PID=5239
son process is over

使用 top指令查看僵尸进程:

zhenggge@wclass-PC:~/Desktop$ top

top - 08:17:26 up 15 min,  1 user,  load average: 0.59, 0.35, 0.32
Tasks: 153 total,   9 running, 105 sleeping,   0 stopped,   1 zombie

我们可以看到 1 zombie,这就说明系统进程中存在一个僵尸进程,即我们方才创建的。

还有另外一种方式查看僵尸进程,使用 ps -aux |grep Z:

这种方式就更加明了了,我们可以清楚地看到僵尸进程的 PID ,正如我们预期的那样,这个僵尸

进程的 PID 和 我们 fork 出来的子进程PID 相同,也就是说,这和僵尸进程就是方才的子进程。

2.2 僵尸进程的危害

由于子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候

结束. 那么会不会因为父进程太忙而来不及 wait 子进程,或者说不知道子进程什么时候结束,而

丢失子进程结束时的状态信息呢?

不会。因为 UNIX 提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息,就可以

得到。这种机制就是: 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文

件,占用的内存等。但是仍然为其保留一定的信息(包括进程号 the process ID,退出状态 the

termination status of the process,运行时间 the amount of CPU time taken by the process

等)。直到父进程通过wait/waitpid来取时才释放。

这样就导致了问题,如果进程不调用 wait/waitpid 的话,那么保留的那段信息就不会释放,其进

程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为

没有可用的进程号而导致系统不能产生新的进程。此即为僵尸进程的危害,应当避免。

2.3 如何避免僵尸进程的产生

  • 父进程通过 wait 和 waitpid 等函数等待子进程结束,这会导致父进程挂起。
/* test.c */
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>

int main() {
    int status;
    pid_t pid;
    pid = fork();

    if (pid < 0) {
        printf("fork failed\n");
    } else if (pid == 0) {
        printf("this is son process with PID=%d\n", getpid());
        printf("son process is over\n");
    } else {
        printf("this is father process with PID=%d\n", getpid());
        usleep(100);
        waitpid(pid, &status, 0);    // 等待子进程
        printf("father process is over\n");
    }

    return 0;
}

编译运行:

zhengge@wclass-PC:~/Desktop$ gcc -o test test.c
zhengge@wclass-PC:~/Desktop$ ./test
this is father process with PID=5645
this is son process with PID=5646
son process is over
father process is over

查看是否存在僵尸进程:

zhengge@wclass-PC:~/Desktop$ top

top - 08:38:51 up 37 min,  1 user,  load average: 0.03, 0.05, 0.09
Tasks: 149 total,   1 running, 110 sleeping,   0 stopped,   0 zombie
  • 我们知道,当一个进程结束时会将 SIGCHLD 信号发送给其父进程,系统默认的处理方式是忽略此信号。如果父进程很忙,那么可以用 signal函数(原型为:signal(int signum, void(*handler)(int)))为 SIGCHLD 安装 handler ,可以在 handler 中调用 wait 回收。
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>

void handler(int sig) {
    if (sig == SIGCHLD) {
        int status;
        waitpid(-1, &status, WNOHANG);
    }
}

int main() {
    int status;
    pid_t pid;
    pid = fork();

    if (pid < 0) {
        printf("fork failed\n");
    } else if (pid == 0) {
        printf("this is son process with PID=%d\n", getpid());
        printf("son process is over\n");
    } else {
        signal(SIGCHLD, handler);
        printf("this is father process with PID=%d\n", getpid());        
        usleep(100);
        // waitpid(pid, &status, 0);
        printf("father process is over\n");
    }

    return 0;
}

编译运行:

zhengge@wclass-PC:~/Desktop$ gcc -o test test.c
zhengge@wclass-PC:~/Desktop$ ./test
this is father process with PID=5819
this is son process with PID=5820
son process is over
father process is over

查看是否存在僵尸进程:

jincheng@jincheng-PC:~/Desktop$ top

top - 08:47:09 up 45 min,  1 user,  load average: 0.00, 0.03, 0.07
Tasks: 151 total,   1 running, 110 sleeping,   0 stopped,   0 zombie
  • 如果父进程不关心子进程什么时候结束,那么可以用 signal(SIGCHLD,SIG_IGN)通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收,并不再给父进程发送信号。
/* test.c */
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
/*
void handler(int sig) {
    if (sig == SIGCHLD) {
        int status;
        waitpid(-1, &status, WNOHANG);
    }
}
*/

int main() {
    int status;
    pid_t pid;
    pid = fork();

    if (pid < 0) {
        printf("fork failed\n");
    } else if (pid == 0) {
        printf("this is son process with PID=%d\n", getpid());
        printf("son process is over\n");
    } else {
        //signal(SIGCHLD, handler);
        signal(SIGCHLD,SIG_IGN);    // 通知内核,父进程对子进程忽略
        printf("this is father process with PID=%d\n", getpid());        
        usleep(100);
        // waitpid(pid, &status, 0);
        printf("father process is over\n");
    }

    return 0;
}
/*
编译运行:
zhengge@wclass-PC:~/Desktop$ gcc -o test test.c
zhengge@wclass-PC:~/Desktop$ ./test
this is father process with PID=6384
this is son process with PID=6385
son process is over
father process is over

查看是否存在僵尸进程:    
zhengge@wclass-PC:~/Desktop$ top

top - 09:16:17 up  1:14,  1 user,  load average: 0.16, 0.07, 0.06
Tasks: 155 total,   1 running, 114 sleeping,   0 stopped,   0 zombie
*/
  • 还有一些技巧,就是 fork 两次,父进程 fork 一个子进程,然后继续工作,子进程fork 一个孙进程后退出,那么孙进程被 init 接管,孙进程结束后,init 会回收。不过子进程的回收还要自己做。

3. 参考链接

关于僵尸进程_僵尸进程的pid-CSDN博客

二、线程(Thread)

1. 线程是什么?

线程作为程序执行的最小单位,一个进程中可以拥有多条线程,所有线程可以共享进程的内存区

域,线程通常在运行时也需要一组寄存器、内存、栈等资源的支撑。

程序之所以可以运行起来的根本原因就是因为内部一条条的线程在不断的执行对应的代码逻辑。

多核CPU中,一个核心往往在同一时刻只能支持一个内核线程的运行,所以如果你的机器为八核

CPU,那么理论上代表着同一时刻最多支持八条内核线程同时并发执行。

当然,现在也采用了超线程的技术,把一个物理芯片模拟成两个逻辑处理核心,让单个处理器都

能使用线程级并行计算,进而兼容多线程操作系统和软件,减少了 CPU 的闲置时间,提高的

CPU 的运行效率。

比如四核八线程的 CPU,在同一时刻也支持最大八条线程并发执行。

在 OS 中,程序一般不会去直接申请内核线程进行操作,而是去使用内核线程提供的一种名为

LWP 的轻量级进程( Lightweight Process )进行操作,这个也就是平时所谓的线程,也被称为用

户级线程。

得出结论:

进程可分为一到多个线程,因此线程也称为轻量级进程,每个进程最少拥有一个线程,最早接触

的就是主线程

其实,线程就是一个程序内部的一条执行路径,程序中如果只有一条执行路径,那么这个程序就

是单线程的程序

例如:main方法的执行其实就是一条执行路径,叫做主线程!

总结:线程就是应用程序中要做的事情。比如:360软件中的杀毒,扫描木马,清理垃圾等

2. 多线程

2.1. 概述

一个线程如果有多条执行路径,则称为多线程程序。

2.2. 多线程的好处

1. 提高 CPU 的利用率

一般来说,在等待磁盘 IO,网络 IO 或者等待用户输入时,CPU可以同时去处理其他任务

2. 更高效的响应

多线程技术使程序的响应速度更快 ,因为用户界面可以在进行其它工作的同时一直处于活动状态,不会造成无法响应的现象

3. 公平使用 CPU 资源

当前没有进行处理的任务,可以将处理器时间让给其它任务;

占用大量处理时间的任务,也可以定期将处理器时间让给其它任务;

通过对 CPU 时间的划分,使得 CPU 时间片可以在多个线程之间切换,

避免需要长时间处理的线程独占 CPU,导致其它线程长时间等待

2.3. 多线程的代价

1. 更复杂的设计

共享数据的读取,数据的安全性,线程之间的交互,线程的同步等;

2. 上下文环境切换

线程切换,CPU需要保存本地数据、程序指针等内容;

3. 更多的资源消耗

每个线程都需要内存维护自己的本地栈信息,操作系统也需要资源对线程进行管理维护;

3. 线程模型(三种)

3.1. 一对一模型

一对一模型是指一条用户线程对应着内核中的一条线程,而 Java 中采用的就是这种模型,

如下:

Java 线程一对一模型 一对一模型是真正意义上的并行执行,因为这种模型下,创建一条 Java

的 Thread 线程是真正的在内核中创建并映射了一条内核线程的,执行过程中,一条线程不会因

为另外一条线程的原因而发生阻塞等情况。不过因为是直接映射内核线程的模式,所以数量会存

在上限。并且同一个核心中,多条线程的执行需要频繁的发生上下文切换以及内核态与用户态之

间的切换,所以如果线程数量过多,切换过于频繁会导致线程执行效率下降。

3.2. 多对一模型

顾名思义,多对一模型是指多条用户线程映射同一条内核线程的情况,对于用户线程而言,它们

的执行都由用户态的代码完成切换。

这种模式优点很明显,一方面可以节省内核态到用户态切换的开销,第二方面线程的数量不会受

到内核线程的限制。但是缺点也很明显,因为线程切换的工作是由用户态的代码完成的,所以如

果当一条线程发生阻塞时,与该内核线程对应的其他用户线程也会一起陷入阻塞。

3.3 多对多模型

多对多模型就可以避免上面一对一和多对一模型带来的弊端,也就是多条用户线程映射多条内核

线程,这样即可以避免一对一模型的切换效率问题和数量限制问题,也可以避免多对一的阻塞问

题,如下:

3.4 如何选择线程模型

Solaris 版的 HotSpot 也对应提供了两个平台专有的虚拟机参数,即 -XX:

+UseLWPSynchronization(默认值)和 -XX:+UseBoundThreads来明确指定虚拟机使用哪种

线程模型。

操作系统支持怎样的线程模型,在很大程度上会影响上面的 Java 虚拟机的线程是怎样映射的,

线程模型只对线程的并发规模和操作成本产生影响,对 Java 程序的编码和运行过程来 说,这些

差异都是完全透明的。

4. 线程调度(两种)

4.1. 协同式线程调度

协同式 (Cooperative Threads-Scheduling)线程调度就是每个线程都会在执行完自己的所有指

令后才会让出CPU。

缺点很明显,如果一个线程时间很长或者有问题,则它也不会让出 CPU,导致整个系统阻塞。

4.2. 抢占式线程调度

抢占式(Preemptive Threads-Scheduling)线程调度就是每个线程运行的时间有操作系统决

定,线程本身无法决定。

这种方式很好解决了协调式的问题,不会因为某个线程有问题而导致整个系统阻塞。

抢占式还提供了线程的优先级设置,但这个不可信,只是一个参考。

操作系统自己可以修改线程的优先级,所以即使设置线程的优先级,最后执行的时候也不一定真

的有效。

因此,我们并不能在程序中通过优先级来完全准确判断 一组状态都为 Ready 的线程将会先执行

哪一个。

5. 线程和进程区别

1. 根本区别

进程是操作系统分配资源的最小单位,线程是任务调动和执行的最小单位。

2. 开销方面

每个进程都有独立的代码和数据空间,程序切换会有较大的开销。

线程之间共享代码和数据,每个线程都有自己独立的栈和调度器,线程之间的切换的开销较小。

3. 所处环境

一个操作系统中可以运行多个进程,一个进程中有多个线程同时执行。

4. 内存分配方面

系统在运行时会为每个进程分配内存,系统不会单独为每个线程分配内存。

5. 包含关系

创建进程时系统会自动创建一个主线程由主线程完成,进程中有多线程时,由多线程共同执行完

成。

6. 总结

进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,

线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资

源。

操作系统分配资源时是把资源分配给进程的,但是CPU资源比较特殊,它是被分配到线程的,

因为真正要占用CPU 运行的是线程,所以也说线程是 CPU 分配的基本单位。

在 Java 中,当我们启动 main 函数时其实就启动了一个JVM的进程,而 main 函数所在的线程就

是这个进程中的一个线程,也称主线程。

可以看到,一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己

的程序计数器和栈区域。

程序计数器是一块内存区域,用来记录线程当前要执行的指令地址。

那么为何要将程序计数器设计为线程私有的呢?

首先,线程是占用CPU执行的基本单位,而CPU 一般是使用时间片轮转方式让线程轮询占用

的,所以当前线程CPU 时间片用完后,要让出CPU,等下次轮到自己的时候再执行。

那么如何知道之前程序执行到哪里了呢?

程序计数器就是为了记录该线程让出 CPU 时的执行地址的,待再次分配到时间片时线程就可以

从自己私有的计数器指定地址继续执行。

如果执行的是 native 方法,那么 pc 计数器记录的是 undefined 地址,只有执行的是 Java 代码时

pc 计数器记录的才是下一条指令的地址。每个线程都有自己的栈资源,用于存储该线程的局部

变量,这些局部变量是该线程私有的,其他线程是访问不了的,还用来存放线程的调用栈帧。

是一个进程中最大的一块内存,堆是被进程中的所有线程共享的,是进程创建时分配的,堆里

面主要存放使用 new 操作创建的对象实例。方法区则用来存放

JVM加载的类、常量及静态变量等信息,也是线程共享的。

三、绿色线程

1. 绿色线程是什么?

绿色线程(Green Thread)是一个相对于操作系统线程(Native Thread)的概念。

操作系统线程(Native Thread)的意思就是,程序里面的线程会真正映射到操作系统的线程,

线程的运行和调度都是由操作系统控制的绿色线程(Green Thread)的意思是,程序里面的线

程不会真正映射到操作系统的线程,而是由语言运行平台自身来调度。

当前版本的Python语言的线程就可以映射到操作系统线程。当前版本的Ruby语言的线程就属于

绿色线程,无法映射到操作系统的线程,因此Ruby语言的线程的运行速度比较慢。

难道说,绿色线程要比操作系统线程要慢吗?

当然不是这样。事实上,情况可能正好相反。Ruby是一个特殊的例子。

线程调度器并不是很成熟。

目前,线程的流行实现模型就是绿色线程。比如,stackless python,就引入了更加轻量的绿色

线程概念。在线程并发编程方面,无论是运行速度还是并发负载上,都优于Python。

另一个更著名的例子就是ErLang(爱立信公司开发的一种开源语言)。

ErLang的绿色线程概念非常彻底。ErLang的线程不叫Thread,而是叫做Process。这很容易和

进程混淆起来。这里要注意区分一下。

ErLang Process之间根本就不需要同步。

因为ErLang语言的所有变量都是final的,不允许变量的值发生任何变化。

因此根本就不需要同步。

final变量的另一个好处就是,对象之间不可能出现交叉引用,不可能构成一种环状的关联,对象

之间的关联都是单向的,树状的。

因此,内存垃圾回收的算法效率也非常高。这就让ErLang能够达到Soft Real Time(软实时)的

效果。这对于一门支持内存垃圾回收的语言来说,可不是一件容易的事情

2. Java 世界中的绿色线程

所谓绿色线程更多的是一个逻辑层面的概念,依赖于虚拟机来实现。操作系统对于虚拟机内部如

何进行线程的切换并不清楚,从虚拟机外部来看,或者说站在操作系统的角度看,这些都是不可

见的。

可以把虚拟机看作一个应用程序,程序的代码本身来建立和维护针对不同线程的堆栈,指令计数

器和统计信息等等。这个时候的线程仅仅存在于用户级别的应用程序中,不需要进行系统级的调

用,也不依赖于操作系统为线程提供的具体功能。

绿色线程主要是为了移植方便,但是会增加虚拟机的复杂度。

总的来说,它把线程的实现对操作系统屏蔽,处在用户级别的实现这个层次上。绿色线程模型的

一个特点就是多CPU也只能在某一时刻仅有一个线程运行。

本机线程简单地说就是和操作系统的线程对应,操作系统完全了解虚拟机内部的线程。对于

windows操作系统,一个java虚拟机的线程对应一个本地线程,java线程调度依赖于操作系统线

程。对于solaris,复杂一些,因为后者本身提供了用户级和系统级两个层次的线程库。

依赖于操作系统增加了对于平台的依赖性,但是虚拟机实现相对简单些,而且可以充分利用多

CPU实现多线程同时处理。

四、协程(微线程)

1. 协程是什么?

协程又称微线程,协程不是进程,也不是线程,它就是一个函数,

一个特殊的函数——可以在某个地方挂起,并且可以重新在挂起处继续运行。

所以说,协程与进程、线程相比,不是一个维度的概念。

一个进程可以包含多个线程,一个线程也可以包含多个协程,

也就是说,一个线程内可以有多个那样的特殊函数在运行。

但是有一点,必须明确,一个线程内的多个协程的运行是串行的。

如果有多核 CPU 的话,多个进程或一个进程内的多个线程是可以并行运行的,但是一个线程内

的多个协程却绝对是串行的,无论有多少个CPU(核)。

这个比较好理解,毕竟协程虽然是一个特殊的函数,但仍然是一个函数。

一个线程内可以运行多个函数,但是这些函数都是串行运行的。

当一个协程运行时,其他协程必须挂起。

2. 协程是怎么来的?

一开始大家想要同一时间执行那么三五个程序,大家能一块跑一跑。特别是UI什么的,别一上

计算量比较大的玩意就跟死机一样。

于是就有了并发,从程序员的角度可以看成是多个独立的逻辑流。内部可以是多cpu并行,也可

以是单cpu时间分片,能快速的切换逻辑流,看起来像是大家一块跑的就行。

但是一块跑就有问题了。我计算到一半,刚把多次方程解到最后一步,你突然插进来,我的中间

状态咋办,我用来储存的内存被你覆盖了咋办?所以跑在一个cpu里面的并发都需要处理上下文

切换的问题。进程就是这样抽象出来个一个概念,搭配虚拟内存、进程表之类的东西,用来管理

独立的程序运行、切换。

后来一电脑上有了好几个cpu,好咧,大家都别闲着,一人跑一进程。就是所谓的并行

因为程序的使用涉及大量的计算机资源配置,把这活随意的交给用户程序,非常容易让整个系统

分分钟被搞跪,资源分配也很难做到相对的公平。所以核心的操作需要陷入内核(kernel),切换

到操作系统,让老大帮你来做。

有的时候碰着I/O访问,阻塞了后面所有的计算。空着也是空着,老大就直接把CPU切换到其他

进程,让人家先用着。当然除了I\O阻塞,还有时钟阻塞等等。一开始大家都这样弄,后来发现

不成,太慢了。为啥呀,一切换进程得反复进入内核,置换掉一大堆状态。进程数一高,大部分

系统资源就被进程切换给吃掉了。后来搞出线程的概念,大致意思就是,这个地方阻塞了,但我

还有其他地方的逻辑流可以计算,这些逻辑流是共享一个地址空间的,不用特别麻烦的切换页

表、刷新TLB,只要把寄存器刷新一遍就行,能比切换进程开销少点。

如果连时钟阻塞、 线程切换这些功能我们都不需要了,自己在进程里面写一个逻辑流调度的东

西。那么我们即可以利用到并发优势,又可以避免反复系统调用,还有进程切换造成的开销,分

分钟给你上几千个逻辑流不费力。这就是用户态线程

从上面可以看到,实现一个用户态线程有两个必须要处理的问题:一是碰着阻塞式I\O会导致整

个进程被挂起;二是由于缺乏时钟阻塞,进程需要自己拥有调度线程的能力。如果一种实现使得

每个线程需要自己通过调用某个方法,主动交出控制权。那么我们就称这种用户态线程是协作式

的,即是协程

3. 协程的好处有哪些?

协程是进程和线程的升级版,进程和线程都面临着内核态和用户态的切换问题而耗费许多切换时

间,而协程就是用户自己控制切换的时机,不再需要陷入系统的内核态

协程的执行效率非常高。因为子程序切换不是线程切换,而是由程序自身控制。因此,没有线程

切换的开销,和多线程相比,线程数量越多,相同数量的协程体现出的优势越明显。

不需要多线程的锁机制。由于只有一个线程,也不存在同时写变量的冲突,在协程中控制共享资

源不需要加锁,只需要判断数据的状态,所以执行效率远高于线程。对于多核CPU可以使用多进

程+协程来尽可能高效率地利

用CPU。

4. 总结

协程是一种基于线程之上,但又比线程更加轻量级的存在,这种由程序管理的轻量级线程也被称

为用户空间线程,对于内核而言是不可见

正如同进程中存在多条线程一样,线程中也可以存在多个协程。

协程在运行时也有自己的寄存器、上下文和栈,协程的调度完全由用户控制,协程调度切换时,

会将寄存器上下文和栈保存到分配的私有内存区域中,在切回来的时候,恢复先前保存的寄存器

上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文

的切换非常快。

虽然协程的概念很早就有,但是很长一段时间,主流的编程语言都没有提供对协程的原生支持,

它只出现在一些小众语言中(Simula 和 Modula-2等)。后来出现的纤程啦、coroutine啦都是协

程的具体实现,但是都没有达到现在的热度!

直到高并发成为主流趋势,瞬间涌入的海量请求,让多进程模型下,内存资源“捉襟见肘”;让多

线程模型下,“用户态”“内核态”两头忙,却依然疲于应对!

而协程这种既轻量又灵活的,由用户态进行调度的多任务模型,便获得了广泛的关注。越来越多

的编程语言提供了对协程的支持,而真正让协程大放异彩的,是它在IO多路复用中的应用!

五、纤程

1. 纤程是什么?

1.1. 说法一

纤程(Fiber)是 Windows 操作系统提供的概念。

纤程是一种比线程更轻量级的执行单元,它可以在一个线程中切换执行,不需要操作系统内核的

干预。

纤程可以用来实现异步任务,避免了创建新线程的开销。纤程也叫做协程(coroutine),是一种

用户态的多任务机制。

注意:纤程类似于协程的思想,JAVA语言目前对此还不算成熟,需要类库。

1.2. 说法二

纤程就是用户态的线程,线程中的线程,它是属于用户态里面的线程,

所以也叫线程中的线程,切换和调度不需要经过OS(操作系统)

优势:

属于轻量级“线程” 因为不会惊动OS,占用资源很少,OS启用一个线程(重量级线程)需要差不

多1M内存(基本上可以启动一万个左右,而且每次切换都需要消耗资源),而Fiber(纤程)仅

需要4k,切换简单,可以启动很多个(10w+)

2. 协程与纤程主要的区别

  • 纤程是操作系统级别的实现,而协程是语言级别的实现。纤程被操作系统内核控制,协程对于内核而言不可见。
  • 纤程和线程类似,都拥有自己的栈、寄存器现场等资源,但是纤程更轻量级,一个线程可以包含多个纤程。协程也可以有自己的栈(stackful)或者共享栈(stackless),但是寄存器现场由用户代码保存和恢复。
  • 纤程之间的切换由用户控制,需要显式地调用转换函数。协程之间的切换也由用户控制,但是可以通过生成器异步函数等语法糖来隐式地实现。
  • 纤程只出现在 Windows 上,而协程在很多语言和平台上都有支持。
  • 一个简单的纤程程序,创建两个纤程并在它们之间切换:
#include "pch.h"
#include <iostream>
#include <windows.h>
#include <tchar.h>

 #define FIBER_COUNT 2
 LPVOID g_lpFiber[FIBER_COUNT] = {};
 VOID WINAPI FiberFun(LPVOID pParam) //纤程函数的返回类型为VOID,并不是因为返回值没有意义,而是因为这个函数不应该返回!
 {
     int nFiberIndex = (int)pParam;
     while (true)
     {
         std::cout << "Fiber" << nFiberIndex << std::endl;
         SwitchToFiber(g_lpFiber[1 - nFiberIndex]); //切换到另一个纤程
     }
 }
 int _tmain(int argc, _TCHAR* argv[])
 {
     LPVOID lpMainFiber = ConvertThreadToFiber(NULL); //将当前线程转换为主纤程
     if (lpMainFiber == NULL)
     {
         std::cout << "ConvertThreadToFiber failed" << std::endl;
         return -1;
     }
     for (int i = 0; i < FIBER_COUNT; i++)
     {
         g_lpFiber[i] = CreateFiber(0, FiberFun, (LPVOID)i); //创建子纤程
         if (g_lpFiber[i] == NULL)
         {
             std::cout << "CreateFiber failed" << std::endl;
             return -1;
         }
     }
     SwitchToFiber(g_lpFiber[0]); //切换到第一个子纤程
     for (int i = 0; i < FIBER_COUNT; i++)
     {
         DeleteFiber(g_lpFiber[i]); //删除子纤程
     }
     ConvertFiberToThread(); //将主纤程转换回线程
     return 0;
 }
#include <windows.h>
#include <stdio.h>
#define MAX_FIBERS 3
    
 DWORD dwCounter;
 void WINAPI MyFunc(LPVOID lpParameter)
 {
     DWORD dwIndex;
     dwIndex = *(DWORD *)lpParameter;
     while(TRUE)
     {
         printf("dwCounter=%d,dwIndex=%d\n",dwCounter,dwIndex);
         dwCounter++;
         SwitchToFiber(lpParameter);
     }
 }
 void main()
 {
     LPVOID lpMainAddress;
     LPVOID lpAddress[MAX_FIBERS];
     DWORD dwParameter[MAX_FIBERS];
     int i;
     lpMainAddress=ConvertThreadToFiber(NULL);
     for(i=0;i<MAX_FIBERS;i++)
     {
         dwParameter[i]=i+1;
         lpAddress[i]=CreateFiber(0,(LPFIBER_START_ROUTINE)MyFunc,&dwParameter[i]);
     }
     for(i=0;i<10;i++)
         SwitchToFibers(lpAddress[i%MAX_FIBERS]);
     for(i=0;i<MAX_FIBERS;i++)
         DeleteFibers(lpAddress[i]);
     printf("end\n");
 }

3. 总结

3.1. 说法一

纤程(Fiber)是Microsoft组织为了帮助企业程序的更好移植到Windows系统,而在操做系统中

增加的一个概念,由操作系统内核根据对应的调度算法进行控制,也是一种轻量级的线程。

纤程和协程的概念一致,都是线程的多对一模型,但有些地方会区分开来,但从协程的本质概念

上来谈:纤程、绿色线程、微线程这些概念都属于协程的范围。

纤程和线程的区别在于:使用纤程 线程是在内核模式下实现的,操作系统控制。 而纤程是在用户

模式下实现的,内核对纤程一无所知。

纤程和协程的区别在于:纤程是OS级别的实现,而协程是语言级别的实现,纤程被 OS 内核控

制,协程对于内核而言不可见。

3.2. 说法二

协程是一种在应用层模拟的线程,它可以在不同的执行点之间切换,而不需要操作系统的干预。

协程可以提高程序的性能和并发能力,同时也简化了异步编程的复杂度。

协程是一种轻量级的并发技术,它可以在单个线程内执行多个任务,从而实现高效的并发操作。

与线程相比,协程的优势在于它可以避免线程切换的开销,减少资源占用,同时也更易于编程。

尽管协程的概念早于线程,但协程的实现并不是所有操作系统原生支持的。

目前,很多编程语言都是通过自己的运行时环境来模拟协程,利用线程技术来实现协程的调度。

这些语言中,像 golang 这样的语言在实现上比较成熟,可以支持大量的协程同时执行,这也是

golang 能够处理高并发的原因之一。

在 golang 中,协程的实现是基于线程的,它维护了一个协程队列,由多个线程来负责执行协程

队列中的任务。

当一个协程在执行过程中遇到了阻塞操作,比如等待 IO 数据返回,它会被放入一个阻塞队列

中,等待 IO 数据返回后再继续执行。

在这个过程中,当前线程会去执行队列中的其他协程,从而实现协程之间的切换。

六、管程(Monitors)

1. 管程是什么?

  • 管程是一种程序结构,结构内的多个子程序( 对象或 模块)形成的多个工作线程互斥访问共享资源。这些共享资源一般是 硬件设备或一群 变量。管程实现了在一个时间点,最多只有一个 线程在执行管程的某个子程序。
  • 与那些通过修改数据结构实现互斥访问的并发程序设计相比,管程实现很大程度上简化了程序设计。
  • 管程提供了一种机制,线程可以临时放弃互斥访问,等待某些条件得到满足后,重新获得执行权恢复它的互斥访问。即:在管程中的线程可以临时放弃管程的互斥访问,让其他线程进入到管程中来。
  • 管程包含多个彼此可以交互并共用资源的线程的变量一个 互斥锁一个用来避免 竞态条件的 不变量
  • 一个管程的程序在运行一个线程前会先取得互斥锁,直到完成线程或是线程等待某个条件被满足才会放弃互斥锁。若每个执行中的线程在放弃互斥锁之前都能保证不变量成立,则所有线程皆不会导致竞态条件成立。
  • 管程是一种高级的同步原语。任意时刻管程中只能有一个活跃进程。它是一种编程语言的组件,所以编译器知道它们很特殊,并可以采用与其他过程调用不同的方法来处理它们。典型地,当一个进程调用管程中的过程,前几条指令将检查在管程中是否有其他的活跃进程。如果有,调用进程将挂起,直到另一个进程离开管程。如果没有,则调用进程便进入管程。
  • 管程提供了一种机制,线程可以临时放弃互斥访问,等待某些条件得到满足后,重新获得执行权恢复它的互斥访问。
  • 即:在管程中的线程可以临时放弃管程的互斥访问,让其他线程进入到管程中来。
  • 注意:管程是一个编程语言概念。编译器必须要识别出管程并用某种方式对互斥做出安排。C、Pascal及多数其他语言都没有管程,所以指望这些编译器来实现互斥规则是不可靠的。
  • 管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。
  • 进程只能互斥得使用管程,即当一个进程使用管程时,另一个进程必须等待。当一个进程使用完管程后,它必须释放管程并唤醒等待管程的某一个进程。
  • 紧急队列,它的优先级高于等待队列。

2. 管程的特征

1. 模块化

管程是一个基本的软件模块,可以被单独编译。

2. 抽象数据类型

管程中封装了数据及对于数据的操作,这点有点像面向对象编程语言中的类。

3. 信息隐藏

管程外的进程或其他软件模块只能通过管程对外的接口来访问管程提供的操作,

管程内部的实现细节对外界是透明的。

4. 使用的互斥性

任何一个时刻,管程只能由一个进程使用。进入管程时的互斥由编译器负责完成。

3. enter、leave、c、wait(c)、signal(c)

1. enter过程

一个进程进入管程前要提出申请,一般由管程提供一个外部过程--enter过程。如 Monitor.enter()

表示进程调用管程 Monitor 外部过程 enter 进入管程。

2. leave过程

当一个进程离开管程时,如果紧急队列不空,那么它就必须负责唤醒紧急队列中的一个进程,此

时也由管程提供一个外部过程—leave过程,如 Monitor.leave() 表示进程调用管程 Monitor 外部

过程 leave 离开管程。

3. 条件型变量c

条件型变量 c 实际上是一个指针,它指向一个等待该条件的 PCB 队列。如 notfull 表示缓冲区不

满,如果缓冲区已满,那么将要在缓冲区写入数据的进程就要等待 notfull,即 wait(notfull)。相

应的,如果一个进程在缓冲区读数据,当它读完一个数据后,要执行 signal(notempty),表示已经释放了一个缓冲区单元。

4. wait(c)

wait(c) 表示为进入管程的进程分配某种类型的资源,如果此时这种资源可用,那么进程使用,否则进程被阻塞,

进入紧急队列。

5. signal(c)

signal(c) 表示进入管程的进程使用的某种资源要释放,此时进程会唤醒由于等待这种资源而进入

紧急队列中的第一个进程。

4. 总结

管程(Monitors)提供了一种机制,线程可以临时放弃互斥访问,等待某些条件得到满足后,重

新获得执行权恢复它的互斥访问。

七、总结

现在如今各种程出现的根本原因是由于多核机器的流行,所以程序实现中也需要最大程度上考虑

并行、并发、异步执行,在最大程序上去将硬件机器应有的性能发挥出来。

以 Java 而言,本身多线程的方式是已经可以满足这些需求的,但 Java 中的线程资源比较昂贵,是直接与内核线

程映射的,所以在上下文切换、内核态和用户态转换上都需要浪费很多的资源开销,同时也受到

操作系统的限制,允许一个Java程序中创建的纤程数量是有限的。

所以对于这种一对一的线程模型有些无法满足需求了,最终才出现了各种程的概念。

从实现级别上来看:进程、线程、纤程是 OS 级别的实现,而绿色线程、协程这些则是语言级别

上的实现。

从调度方式上而言:进程、线程、绿色线程属于抢占式执行,而纤程、协程则属于合作式调度。

从包含关系上来说:一个OS中可以有多个进程,一个进程中可以有多条线程,而一条线程中则

可以有多个协程、纤程、微线程等。

标签:协程,纤程,process,管程,线程,进程,多线程
From: https://blog.csdn.net/qq_51226710/article/details/141626513

相关文章

  • 多线程篇(并发编程 - Java线程实现方式)(持续更新迭代)
    目录一、继承Thread类1.简介2.实现2.1.原始方式2.2.Lambda表达式二、实现Runnable接口1.简介2.实现2.1.原始方式2.2.Lambda表达式三、使用FutureTask1.简介2.实现2.1.原始方式2.2.Lambda表达式四、使用线程池1.ThreadPoolExecutornewCached......
  • 多线程篇(基本认识 - 锁优化)(持续更新迭代)
    目录一、前言二、阿里开发手册三、synchronized锁优化的背景四、Synchronized的性能变化1.Java5之前:用户态和内核态之间的切换2.java6开始:优化Synchronized五、锁升级1.无锁2.偏向锁2.1.前言2.2.什么是偏向锁2.3.偏向锁的工作过程2.4.为什么要引入偏向锁......
  • 多线程篇( 并发编程 - 多线程问题)(持续更新迭代)
    目录一、线程的上下文切换问题1.简介2.多线程一定比单线程快?3.如何减少上下文切换二、线程安全问题1.什么是线程安全?2.java语言中的线程安全2.1.不可变2.2.绝对线程安全2.3.相对线程安全2.4.线程兼容2.5.线程对立3.java实现线程安全的方法?3.1.互斥同......
  • JAVAEE初阶第二节——多线程基础(上)
    系列文章目录JAVAEE初阶第二节——多线程基础(上)计算机的工作原理认识线程(Thread)Thread类及常见方法线程的状态文章目录系列文章目录JAVAEE初阶第二节——多线程基础(上)计算机的工作原理一.认识线程(Thread)1.概念1.1为啥要有线程1.2线程1.2.1线程如何解决......
  • JAVAEE初阶第二节——多线程基础(中)
    系列文章目录JAVAEE初阶第二节——多线程基础(中)多线程基础(中)多线程带来的的风险-线程安全(重点)synchronized关键字volatile关键字wait和notify文章目录系列文章目录JAVAEE初阶第二节——多线程基础(中)多线程基础(中)一.多线程带来的的风险-线程安全(......
  • 多线程编程(面试重中之中,超简单理解)
    最近项目比较紧急,固本之旅卡顿了一段时间,抽时间看了一下多线程,面试重点知识!!!多线程编程优点:提高程序的响应速度,增加用户的体验;提高计算机系统CPU的利用率;优化程序结构,将一个复杂的单线程分化成多个清晰化的单线程,更有利于维护并行指两个以上的事物在同一时刻同时发......
  • python并发与并行(十) ———— 结合线程与协程,将代码顺利迁移到asyncio
    在前一篇中,我们用asyncio模块把通过线程来执行阻塞式I/O的TCP服务器迁移到了协程方案上面。当时我们一下子就完成了迁移,而没有分成多个步骤,这对于大型的项目来说,并不常见。如果项目比较大,那通常需要一点一点地迁移,也就是要边改边测,确保迁移过去的这一部分代码的效果跟原来相同。为......
  • python并发与并行(八) ———— 用协程实现高并发的I/O
    在前面几条里,我们以生命游戏为例,试着用各种方案解决I/O并行问题,这些方案在某些情况下确实可行,但如果同时需要执行的I/O任务有成千上万个,那么这些方案的效率就不太理想了像这种在并发方面要求比较高的I/O需求,可以用Python的协程(coroutine)来解决。协程能够制造出一种效果,让我们觉得Py......
  • JAVA多线程异步与线程池------JAVA
    初始化线程的四种方式继承Thread实现Runnable接口实现Callable接口+FutureTask(可以拿到返回结果,可以处理异常)线程池继承Thread和实现Runnable接口的方式,主进程无法获取线程的运算结果,不适合业务开发实现Callable接口+FutureTask可以获取线程内的返回结果,但是不利......
  • 基于live555开发的多线程RTSPServer轻量级流媒体服务器EasyRTSPServer开源代码及其调
    EasyRTSPServer参考live555testProg中的testOnDemandRTSPServer示例程序,将一个live555testOnDemandRTSPServer封装在一个类中,例如,我们称为ClassEasyRTSPServer,在EasyRTSPServer_Create接口调用时,我们新建一个EasyRTSPServer对象,再通过调用EasyRTSPServer_Startup接口,将EasyRTSP......