Guía práctica de Java Coroutine (1)

1. El trasfondo de la generación de rutinas

Hablando de rutinas, la primera impresión de la mayoría de las personas puede ser GoLang, que también es uno de los aspectos más atractivos del lenguaje Go, su soporte de concurrencia incorporado. La teoría del sistema de concurrencia del lenguaje Go es CSP (Communicating Sequential Process) propuesta por CAR Hoare en 1978. CSP tiene un modelo matemático preciso y en realidad se aplica a la computadora de propósito general T9000 diseñada por Hoare. Desde NewSqueak, Alef, Limbo hasta el lenguaje Go actual, para Rob Pike, quien tiene más de 20 años de experiencia práctica en CSP, está más preocupado por el potencial de aplicar CSP a lenguajes de programación de propósito general. Solo hay un concepto central de la teoría CSP que es el núcleo de la programación concurrente en Go: la comunicación síncrona.

En primer lugar, debe quedar claro un concepto: concurrencia no es paralelismo. La concurrencia está más relacionada con el nivel de diseño del programa. Los programas concurrentes se pueden ejecutar en secuencia, y solo en una CPU real de varios núcleos se pueden ejecutar al mismo tiempo. El paralelismo está más relacionado con el nivel de ejecución del programa. El paralelismo es generalmente simple y una gran cantidad de repeticiones. Por ejemplo, habrá una gran cantidad de operaciones paralelas para el procesamiento de imágenes en GPU. Para escribir mejor programas concurrentes, desde el comienzo de su diseño, el lenguaje Go se ha centrado en cómo diseñar un modelo abstracto conciso, seguro y eficiente a nivel de lenguaje de programación, lo que permite a los programadores concentrarse en descomponer problemas y combinar soluciones sin problemas. verse afectado por la gestión de subprocesos y la interacción de la señal. Evite estas tediosas operaciones para distraer su energía.

En la programación concurrente, el acceso correcto a los recursos compartidos requiere un control preciso. En la mayoría de los lenguajes en la actualidad, este difícil problema se resuelve mediante esquemas de sincronización de subprocesos como el bloqueo, pero el lenguaje Go ha adoptado un enfoque diferente. Compartirá el valor de se pasa a través del canal (en realidad, varios subprocesos que se ejecutan de forma independiente rara vez comparten recursos de forma activa). En un momento dado, preferiblemente solo un Goroutine puede poseer el recurso. La competencia de datos se elimina del nivel de diseño. Para promover esta forma de pensar, Go ha traducido su filosofía de programación concurrente en un eslogan:

No comuniques compartiendo la memoria, en cambio, comparte la memoria comunicándote.

Esta es una filosofía de programación concurrente de alto nivel (pasar por valor a través de conductos es la práctica recomendada en Go). Si bien los problemas simples de concurrencia, como el conteo de referencias, están bien con las operaciones atómicas o mutexes, controlar el acceso con Canales le permite escribir programas más concisos y correctos.

Los siete modelos de programación concurrente descritos en Siete modelos de concurrencia en siete semanas.

Referencia: www.cnblogs.com/barrywxx/p/…

  1. Subprocesos y bloqueos: el modelo de subprocesos y bloqueos tiene muchas deficiencias bien conocidas, pero sigue siendo la base técnica de otros modelos y la primera opción para muchos desarrollos de software simultáneos.

  2. Programación funcional: una de las razones por las que la programación funcional se está volviendo más importante es que brinda un buen soporte para la programación concurrente y paralela. La programación funcional elimina el estado mutable, por lo que es fundamentalmente seguro para subprocesos y fácil de ejecutar en paralelo.

  3. The Clojure Way: separando la identidad y el estado: el lenguaje de programación Clojure es una combinación de programación imperativa y funcional que logra un delicado equilibrio para explotar las fortalezas de ambos.

  4. Actor: el modelo de actor es un modelo de programación concurrente ampliamente aplicable, adecuado para modelos de memoria compartida y modelos de memoria distribuida, así como para resolver problemas distribuidos geográficamente, proporcionando una fuerte tolerancia a fallas.

  5. Comunicación de procesos secuenciales (CSP): en la superficie, el modelo CSP es muy similar al modelo actor, ambos basados ​​en el paso de mensajes. Sin embargo, el modelo CSP se enfoca en el canal para transmitir información, mientras que el modelo actor se enfoca en las entidades en ambos extremos del canal, y el código que usa el modelo CSP tendrá un estilo significativamente diferente.

  6. Paralelismo a nivel de datos: dentro de cada computadora portátil hay una supercomputadora: la GPU. Las GPU aprovechan el paralelismo a nivel de datos, no solo para el procesamiento rápido de imágenes, sino también para campos más amplios. Si desea realizar análisis de elementos finitos, cálculos de mecánica de fluidos u otros cálculos numéricos de gran volumen, el rendimiento de la GPU será la mejor opción.

  7. Arquitectura lambda: La llegada de la era del big data es inseparable del paralelismo, ahora solo falta aumentar los recursos de cómputo para tener la capacidad de procesar terabytes de datos. La arquitectura Lambda combina las características de MapReduce y el procesamiento de flujo, y es una arquitectura que puede manejar una variedad de problemas de big data.

Existen los siguientes modelos de concurrencia en lenguajes generales.

  • modelo de roscado

    Abstracción del sistema operativo, alta eficiencia de desarrollo, uso intensivo de E/S, alta sobrecarga de conmutación bajo alta concurrencia.

  • modelo asíncrono

    编程框架抽象,执行效率高,破坏结构化编程,开发门槛高。

  • 协程模型

    语言运行时抽象,轻量级线程,兼顾开发效率和执行效率。

二. Java协程发展历程

Java本身有着丰富的异步编程框架,比如说CompletableFuture,在一定程度上缓解了Java使用协程的紧迫性。

在2010年,JKU大学发表了一篇论文《高效的协程》,向OpenJdk社区提了一个协程框架的Patch,在2013年Quasar和Coroutine,这两种协程框架不需要修改Runtime,在协程切换时本来是要保存调用栈的,但是它们不保存这个调用栈,而是在切换时回溯调用链,生成一个状态机,将状态机保存起来。

Quasar和Coroutine并不是OpenJdk社区原生的协程解决方案,直到2018年1月,官方提出了Project Loom,到了2019年,Loom的首个EA版本问世,此时Java的协程类叫做Fiber,但社区觉得这引入了一个新的概念,于是在2019年10月将Fiber重新实现为了Thread的子类VirtualThread,兼容Thread的所有操作。

这时Project Loom的基本雏形已经完成了,在它的概念中,协程就是一个特殊的线程,是线程的一个子类,从Project Loom已经可以看到Open Jdk社区未来协程发展的方向, 但Loom还有很多的工作需要完成,并没有完全开发完。

三. Project Loom的目标与挑战

  • 目标

    易于理解的Java协程系统解决方案,协程即线程。

Virtual threads are just threads that are scheduled by the Java virtual machine rather than the operating system.

  • 挑战

    兼容庞大而复杂的标准类库、JVM特性,同时支持协程和线程。

四. Loom实现架构

在API层面Loom引入最重要的概念就是Virtual Thread,对于使用者来说可以当做Thread来理解。

下面是协程生命周期的描述,与线程相同需要一个start函数开始执行,接下来VirtualThread就会被调度执行,与线程不同的是,协程的上层需要一个调度器来调度它,而不是被操作系统直接调度,被调度执行后就是执行业务代码,此时我们业务代码可能会遇到一个数据库访问或者IO操作,这时当前协程就会被Park起来,与线程相同,此时我们的协程需要在切换前保存上下文,这步操作是由Runtime的Freeze来执行,等到IO操作完成,协程被唤醒继续执行,这时就要恢复上下文,这一步叫做Thaw。

1. Freeze操作

上图左侧是对Freeze的介绍,首先一个协程要被执行需要一个调度器,在Java生态本身就有一个非常不错的调度器ForkJoinPool,Loom也默认使用ForkJoinPool来作为调度器。

图中ForkJoinWorkerThread调用栈前半部分直到enterSpecial都是类库的调用栈,用户不需要考虑,A可以理解为用户自己的实现,从函数A调用到函数B,函数B调用函数C,函数C此时有一个数据访问,就会将当前协程挂起,yield操作会去保存当前协程的执行上下文,调用freeze,freeze会做一个stack walk,从当前调用栈的最后一层(yield)回溯到用户调用(函数A),将这些内容拷贝到一个stack。这也是协程栈大小不固定的原因,我们可以动态扩缩协程需要的空间,而线程栈大小默认1M,不管用没用到。而协程按需使用的特点,可以创建的数量非常多。extract_pop是Loom非常好的一个优化,它将ABC调用栈中的Java对象单独拷贝到一个refStack,在GC root时,如果把协程栈也当做root,几百万个协程会导致扫描停顿很久,Loom将所有对象都提到一个refStack里面,只需要处理这个stack即可,避免过多的协程栈增加GC时间。

2. Thaw操作

Thaw se usa para reanudar la ejecución. Puede llevar mucho tiempo copiar todo el ABC y el rendimiento en la pila de vuelta a la pila de ejecución, porque la pila de ejecución puede ser muy profunda. Después de la investigación, los miembros de la comunidad de Loom descubrieron que la función C puede tener más más de una operación de acceso a datos. , Después de restaurar la pila de ejecución, el contexto puede cambiarse nuevamente debido a la operación IO de C, por lo que Loom usa un método de copia perezoso, copiando solo una parte a la vez y return barriercontinuando copiándola en la pila una vez completada la ejecución. De esta forma, a excepción de la primera sobrecarga de conmutación, que es relativamente grande, todas las demás sobrecargas de conmutación serán pequeñas.

Por otro lado, el OOP guardado en refStack debe restaurarse, porque muchos GC pueden cambiar la dirección de OOP durante la ejecución y puede haber problemas con el acceso si no se restaura.

5. Uso del telar

  • Creación de subprocesos virtuales

    • Crear VirtualThread a través de Thread.builder

    • Cree una fábrica de VirtualThread a través de Thread.builder

    • Programador ForkJoinPool predeterminado (equilibrio de carga, expansión automática), admite programador personalizado

  • programador personalizado
static ExecutorService SCHEDULER_1 = Executors.newFixedThreadPool(1);
Thread thread = Thread.ofVirtual().scheduler(SCHEDULER_1).start(() -> System.out.println("Hello"));
thread.join();
复制代码
  • Crear un grupo de corrutinas
ThreadFactory factory;
if (usrFiber == false) {
    factory = Thread.builder().factory();
} else {
    factory = Thread.builder().ofVirtual().factory();
}
ExecutorService e = Executors.newFixThreadPool(threadCount, factory);
for (int i=0; i < requestCount; i++) {
    e.execute(r);
}
复制代码

Supongo que te gusta

Origin juejin.im/post/6974216114318508046
Recomendado
Clasificación