简介

Web后台服务处理请求的典型场景:访问基础服务,然后将结果简单计算后返回给前端。

void do_msg(Handler handler) {
    req = handler.read(); //阻塞io
    rsp = do_invoke_svc(req); //阻塞函数
    handler.write(rsp++); //阻塞io
}
Rsp do_invoke_svc(Req req) {
    io_write(); //阻塞io
    io_read(); //阻塞io
}

后台服务的并发模型有以下3种:

  • 多进程模型
    process-per-request每个进程处理一个请求。程序很简单和安全,但是性能很低。比如Apache早期的prework模式。
  • 多线程模型
    thread-per-request每个线程处理一个请求。性能比多进程好,但是线程间共享了很多资源,稳定性略差。比如Java的Web容器Servlet、Apache的worker模式等。
  • 单线程事件驱动模型
    event-loop单线程事件驱动模型。性能非常好,但是函数不能有阻塞。比如Nignx、Redis。do_msg如果想使用这种模式,阻塞函数do_msg、do_invoke_svc都需要做异步化改造,很容易掉入回调地狱。

多线程模型虽然比多进程的性能要高很多,但是每个请求还是需要占用一个线程直到处理完毕,如果阻塞时间比较长,内存压力就会很大(每个线程至少占2M的内存),导致服务并发度还是上不去。事件模型又会陷入异步回调地狱,代码难以维护。这时就很期待有一种既能用同步方式写代码,又能高并发执行的方法。也就是当程序执行到函数的阻塞处时,能暂时挂起自己(暂停这个函数的执行并且把CPU执行权让度出去),当阻塞恢复时CPU又能回这个暂停点继续执行后面的逻辑。也就是需要函数有暂停和恢复继续执行的能力,这种支持暂停和恢复的函数就是协程。

不只后台服务有这种需求,前端界面渲染也有类似的需要。

协程概念处理出来后,成为各语言追逐的方向。甚至出现以协程为卖点的Golang。
JavaScript和C#通过async/await关键字早早支持了协程。Java也在经过loom项目长期孵化后,终于在Java21里加入了协程。C++20规范也终于迎来了协程,只是不知道编译器什么时候能跟上。

其中JavaScript、C#是无栈协程(Stackless Coroutines),Go、Java是有栈协程(Stackfull Coroutines)。C++看标准是无栈协程的思路,提供了co_yield、co_await、co_return等关键字。

有栈协程

有栈协程,顾名思义就是通过栈的上下文切换来实现的协程。将协程的暂停状态以栈桢的形式保存起来。有栈协程的栈切换和用户线程的栈切换步骤很类似,所以也会被称为虚拟线程、纤程等。栈切换在Linux内核调度、Linux系统调用Trap都有涉及,JVM虚拟机的栈帧那块也详细介绍过。协程这里把栈上下文切换在用户态代码里做了一遍。

本函数被暂停后,CPU的执行权被让度到给谁呢,有栈协程里有三种可能。Go是让给协程的调度器,Python是让给本函数的调用方caller(非对称协程),甚至可以直接让给其他暂停的协程(对称协程)。

有栈协程的实现一般都和语言自身特性紧密结合。

  • Java
    Java一直把协程当作虚拟线程看待,和以前的Thread、Executors接口保持高度一致。历史代码通过极小的改动就可以切换到协程模式。https://openjdk.org/jeps/444
//do_msg里的阻塞io处会自动处理暂停和恢复
//不只阻塞io,其他阻塞操作比如queue.take也都会被自动处理
Thread.ofVirtual().start((handler) -> do_msg(handler));

Goals
Enable server applications written in the simple thread-per-request style to scale with near-optimal hardware utilization.
Enable existing code that uses the java.lang.Thread API to adopt virtual threads with minimal change.
Enable easy troubleshooting, debugging, and profiling of virtual threads with existing JDK tools.
Non-Goals
It is not a goal to remove the traditional implementation of threads, or to silently migrate existing applications to use virtual threads.
It is not a goal to change the basic concurrency model of Java.
It is not a goal to offer a new data parallelism construct in either the Java language or the Java libraries. The Stream API remains the preferred way to process large data sets in parallel.

  • Python
    Python把协程看成是生成器+异步IO。
  • Go
    GO的协程混合进了更多的语言特性。

无栈协程

无栈协程,是指不依赖栈桢而使用独立的协程桢来保存状态的协程。一般用async关键字标记这个是一个协程,需要生成协程桢,await标记这里需要处理执行权转移。

一般情况下,无栈协程的性能更好。只是无法在任意的函数调用层暂停,需要通过await手动标记。

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

class EchoServer {
    public static async Task StartAsync() {
        TcpListener server = new TcpListener(IPAddress.Any, 80);
        server.Start();
        while (true) {
            TcpClient client = await server.AcceptTcpClientAsync();
            _ = HandleClientAsync(client);
        }
        server.Stop();
    }

    private static async Task HandleClientAsync(TcpClient client) {
        var stream = client.GetStream();
        var buffer = new byte[1024];
        while (true) {
            int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
            if (bytesRead == 0) {
                break;
            }
            await do_invoke_svc(); //远程调用
            await stream.WriteAsync(buffer, 0, bytesRead);
        }
        client.Close();
    }

    static void Main(string[] args) {
        Task serverTask = StartAsync();
        serverTask.Wait();
    }
}

只需加两个关键字async、await。代码结构、流程和同步阻塞模式下完全一致。
对比之下,用epoll实现类似的功能,各种handle event回调要多丑有多丑。



微信扫描下方的二维码阅读本文

上一篇: golang

下一篇: Java协程详解

Categories: 编程语言

0 Comments

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注