协程&异步

协程&异步编程

线程存在的问题

上下文切换

单核CPU能处理多线程任务,是因为处理器给每个线程分配CPU时间片,线程在CPU时间片内执行任务。

当一个线程的时间片用完,或者因为其他原因挂起,CPU会转而去执行其他线程,这个转换的过程中就涉及上下文切换。

上下文切换的内容:

寄存器的存储内容:CPU寄存器负责存储已经、正在和将要执行的任务

程序计数器存储的指令内容:程序计数器负责存储CPU正在执行的指令位置、即将执行的下一条指令的位置

线程切换的时候需要进入内核态,而内核态(Kernel)的一些数据是共享的,读写时需要同步机制,所以操作一旦陷入内核态就会消耗更多的时间。

也就说,线程的上下文切换会带来一定的开销,线程数越多,上下文切换越频繁,理论上开销就越大,因此,多线程并不一定能带来速度的提升。

内存资源占用

系统在维护线程时需要分配额外的空间,所以线程数的增加还是会提高内存资源的消耗。默认情况下 Linux 系统给每条线程分配的栈空间最大是 6~8MB,这个大小是上限,并不是每条线程真实的栈使用情况。

依赖线程池

线程的使用依赖线程池来统一管理,实际业务代码中不允许使用野线程。

协程的概念

协程,又被称为“轻量级线程”、“微线程”、“纤程(fiber)”等。

协程不是系统级线程,它不像线程一样对应着操作系统中的物理线程。在这里插入图片描述

简单来说,协程就是用代码实现的一种多任务异步处理方案,它与操作系统层面是脱节的,因此,协程是用户态的,协程间的切换不需要进入内核态,协程的切换开销要远远小于线程的切换开销。
在这里插入图片描述

协程与线程的资源消耗性对比

这里选择java协程框架quasar做对比

maven依赖如下:
<dependency> <groupId>co.paralleluniverse</groupId> <artifactId>quasar-core</artifactId> <version>0.7.10</version> </dependency>

编写程序,创建10000个协程:

//协程
public static void main(String[] args) throws InterruptedException {
    CountDownLatch countDownLatch=new CountDownLatch(10000);
    long start = System.currentTimeMillis();

    for (int i = 0; i < 10000; i++) {
        new Fiber<>(new SuspendableRunnable(){
            @Override
            public void run() throws SuspendExecution, InterruptedException {
                //不能是Thread.sleep();
                Fiber.sleep(1000);
                countDownLatch.countDown();
            }
        }).start();
    }

    countDownLatch.await();
    long end = System.currentTimeMillis();
    System.out.println("Fiber use:"+(end-start)+" ms");
    Thread.sleep(1000000);
    System.out.println(1);
}

运行结果:

Fiber use:1480 ms
用时1.48秒,堆内存占用约250MB,总线程数28个
在这里插入图片描述

编写程序,创建10000个线程:

//线程
public static void main(String[] args) throws InterruptedException {
    //允许一个或者多个线程去等待其他线程完成操作
    //CountDownLatch接收一个int型参数,表示要等待的工作线程的个数。
    CountDownLatch countDownLatch=new CountDownLatch(10000);
    long start = System.currentTimeMillis();
    ExecutorService executor= Executors.newCachedThreadPool();
    for (int i = 0; i < 10000; i++) {
        executor.submit(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //使latch的值减1,如果减到了0,则会唤醒所有等待在这个latch上的线程。
            countDownLatch.countDown();
        });
    }
    //使当前线程进入同步队列进行等待,直到latch的值被减到0或者当前线程被中断,当前线程就会被唤醒。
    countDownLatch.await();
    long end = System.currentTimeMillis();
    System.out.println("Thread use:"+(end-start)+" ms");
   }

运行结果:

Thread use:3614 ms
总用时3.6秒,堆内存占用1500MB,总线程数10011个
在这里插入图片描述

可以看出,协程确实比线程占用的资源少

Go语言的协程调度模型

go从语言层面上支持了并发,GPM模型确实更适合处理并发,这也是go程序员的吹嘘点之一。

package main

func run(arg string) {
// 此线程的任务
}

func main() {
go run(“this is new thread”)
}

GPM协程调度模型

G: 表示goroutine,存储了goroutine的执行stack信息、goroutine状态以及goroutine的任务函数等;另外G对象是可以重用的。

P: 表示逻辑processor,P的数量决定了系统内最大可并行的G的数量(前提:系统的物理cpu核数>=P的数量);P的最大作用还是其拥有的各种G对象队列、链表、一些cache和状态。

M: M代表着真正的执行计算资源,也就是线程。在绑定有效的p后,进入schedule循环;而schedule循环的机制大致是从各种队列、p的本地队列中获取G,切换到G的执行栈上并执行G的函数,调用goexit做清理工作并回到m,如此反复。M并不保留G状态,这是G可以跨M调度的基础。
在这里插入图片描述

  • 程序启动时,先初始化和核数一样的P。

  • 当创建一个goroutine时,优先尝试放在本地队列,如果本地队列满了,则会把本地队列的前半部分和这个新的goroutine一起移到全局队列中。

  • 如果没有可用的P的时候,新goroutine加入全局队列中。

  • 如果获取到空闲的P,那么尝试去唤醒一个M,没有可用的M的时候新建一个M。

  • 当M关联上P时,且local队列有任务时,可以一直从p的local队列中取goroutine执行。

  • 当P的local队列中没有goroutine时,则会尝试从全局队列中拿一部分放在本地队列中,这个过程是加锁的。

  • 当从全局队列没取到时,会尝试从其他的P的local队列偷取一半放在自己的本地队列中

  • 当一个G发生系统调用的时候,P会断开与当前的M的关系,尝试从M的空闲队列中获取一个M来继续执行剩下的goroutine。

  • 当上面的G系统调用结束后,M尝试获取一个P来继续执行,如果没获取到,则会把这个g放到全局队列中,并且自己进入M的空闲队列。这里不是销毁M,避免后面又要创建M,造成不必要的开销。

什么是系统调用?

操作系统提供给程序员的接口就是系统调用,系统调用是用户程序和硬件设备之间的桥梁,系统调用会进入内核态。

六大类系统调用

进程控制(process control)

文件管理(file manipulation)

设备管理(device manipulation)

信息维护(information maintenance)

通信(communication)

保护(protection)

事件循环(Event Loop)

这又是一种新的实现异步编程的方式

Event Loop:所有同步任务在主线程上执行,形成一个执行栈---->主线程之外还有任务队列,当异步任务执行有结果的时候就会在任务队列放置一个事件------>当执行栈中的同步任务执行完毕,就会读取任务队列中的事件,将其对应的异步任务放入执行栈执行,这个不断循环往复的过程,就称为事件循环,也就是Event Loop

见下文node.js

对比不同语言的异步编程方式

Java

上文提到过的协程库Quasar,看一下某篇博客里做的性能测试
在这里插入图片描述

为什么java几乎不提协程?

这里有必要说明一下,协程要想完全发挥威力,需要一个前提条件,那就是你的代码必须是无阻塞的,这点很好理解,因为一旦协程方法里出现阻塞操作,那么当前线程也会被阻塞。然而我们知道,整个java生态中,阻塞的库太多。如果考虑能把java中的I/O操作都封装成非阻塞的(例如Netty),那么Quasar还是有用武之地的。

几个java和异步编程相关的项目:

  • Vert.x3 java版的Node.js

  • dragonwell8 阿里OpenJDK,JVM层面实现的协程

  • java中的CompletableFuture ,有回调地狱问题

  • 京东异步编排框架asyncTool

Node.js

node在异步编程这块,可以简单用三个词来概括:单线程、非阻塞I/O、事件驱动

这里的单线程其实指的是主线程

前两个好理解。

事件驱动,可以这样来简单描述:

1、有几个事件队列

2、当有新事件发生时,加入到对应事件队列中

3、有个大循环,不断的从事件队列中取出事件并执行,循环往复。

复杂点描述是这样的:

┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
//注意:途中每个方框被称为事件循环的一个阶段,这六个阶段为一轮事件循环 Event loop

  • timer阶段(定时器):执行setTimeout(callback)和setInterval(callback)

  • I/O callbacks阶段(I/O回调):执行某些系统操作的回调(例如TCP错误类型)

  • idle、prepare阶段(空转):仅node内部使用

  • poll阶段(轮询):获取新的I/O事件,例如操作读取文件

  • check阶段(检查):执行 setImmediate() 设定的callbacks;

  • close callbacks阶段(关闭回调):比如 socket.on(‘close’, callback) 的callback会在这个阶段执行;

当有异步任务产生时,会交给C++维护libuv的线程来处理,异步任务的回调函数会被注册进对应的事件队列,node主线程会不断的从这些事件队列中取出事件来执行。

因此,在代码层面上,node的异步场景与回调函数是分不开的。但是,当业务逻辑复杂时,回调里面套回调,嵌套了无数层,导致代码成了这个样子:
在这里插入图片描述

这就是回调地狱。

node怎么解决回调地狱问题?https://juejin.cn/post/7110513834875420680

C#

C#和java很像,原生支持多线程

Python

不了解

C/C++

什么都可以实现的底层语言

PHP(世界上最好的语言)

PHP原生并不支持多线程

传统的 php-fpm是一个进程执行一次请求,有多少并发,就要有多少进程,不愧是世界上最好的编程语言!但这显然是不能接受的,因此PHP有很多异步编程的框架,要么是实现了事件循环的框架,要么是协程调度框架(基于yield生成器)。

和事件循环有关的框架有:Swoole 和 WorkerMan

和协程有关的框架:ampphp 和 swoole

参考文档

https://zhuanlan.zhihu.com/p/446993465

https://www.cnblogs.com/carySir/p/12530133.html

https://baijiahao.baidu.com/s?id=1702448043936293527&wfr=spider&for=pc

https://blog.csdn.net/weixin_44211968/article/details/120035592

https://zhuanlan.zhihu.com/p/469899375

https://blog.csdn.net/Dimuzero/article/details/121795963

在这里插入图片描述

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值