Guide pratique de la coroutine Java (1)

1. Le contexte de la génération de coroutine

En parlant de coroutines, la première impression de la plupart des gens peut être GoLang, qui est également l'un des aspects très attrayants du langage Go, sa prise en charge intégrée de la simultanéité. La théorie du système de concurrence du langage Go est le CSP (Communicating Sequential Process) proposé par CAR Hoare en 1978. CSP a un modèle mathématique précis et est en fait appliqué à l'ordinateur polyvalent T9000 conçu par Hoare. De NewSqueak, Alef, Limbo au langage Go actuel, pour Rob Pike, qui a plus de 20 ans d'expérience pratique dans CSP, il est plus préoccupé par le potentiel d'application de CSP aux langages de programmation à usage général. Il n'y a qu'un seul concept de base de la théorie CSP qui est au cœur de la programmation concurrente en Go : la communication synchrone.

Tout d'abord, un concept doit être clair : la concurrence n'est pas le parallélisme. La simultanéité concerne davantage le niveau de conception du programme. Les programmes simultanés peuvent être exécutés en séquence, et ce n'est que sur un véritable processeur multicœur qu'ils peuvent s'exécuter en même temps. Le parallélisme concerne plus le niveau d'exécution du programme. Le parallélisme est généralement simple et un grand nombre de répétitions. Par exemple, il y aura un grand nombre d'opérations parallèles pour le traitement d'image en GPU. Afin de mieux écrire des programmes concurrents, dès le début de sa conception, le langage Go s'est concentré sur la façon de concevoir un modèle abstrait concis, sûr et efficace au niveau du langage de programmation, permettant aux programmeurs de se concentrer sur la décomposition des problèmes et la combinaison de solutions sans être affecté par la gestion des threads et l'interaction des signaux. Évitez ces opérations fastidieuses pour distraire votre énergie.

En programmation concurrente, l'accès correct aux ressources partagées nécessite un contrôle précis. Dans la plupart des langages actuels, ce problème difficile est résolu par des schémas de synchronisation de threads tels que le verrouillage, mais le langage Go a adopté une approche différente. Il partagera la valeur de est transmis via Channel (en fait, plusieurs threads s'exécutant indépendamment partagent rarement activement des ressources). A tout moment, de préférence un seul Goroutine peut posséder la ressource. La concurrence des données est éliminée au niveau de la conception. Pour promouvoir cette façon de penser, Go a traduit sa philosophie de programmation concurrente en un slogan :

Ne communiquez pas en partageant la mémoire ; au lieu de cela, partagez la mémoire en communiquant.

Il s'agit d'une philosophie de programmation concurrente de niveau supérieur (le passage par valeur à travers des canaux est la pratique recommandée en Go). Alors que de simples problèmes de concurrence tels que le comptage de références conviennent aux opérations atomiques ou aux mutex, le contrôle de l'accès avec les canaux vous permet d'écrire des programmes plus concis et corrects.

Les sept modèles de programmation simultanée décrits dans Seven Concurrency Models in Seven Weeks.

Référence : www.cnblogs.com/barrywxx/p/…

  1. Threads et verrous : le modèle de thread et de verrou présente de nombreuses lacunes bien connues, mais il reste la base technique d'autres modèles et le premier choix pour de nombreux développements logiciels simultanés.

  2. Programmation fonctionnelle : L'une des raisons pour lesquelles la programmation fonctionnelle devient de plus en plus importante est qu'elle offre un bon support pour la programmation simultanée et parallèle. La programmation fonctionnelle élimine l'état mutable, elle est donc fondamentalement thread-safe et facile à exécuter en parallèle.

  3. La méthode Clojure - Séparer l'identité et l'état : Le langage de programmation Clojure est un mélange de programmation impérative et fonctionnelle qui établit un équilibre délicat pour exploiter les points forts des deux.

  4. Acteur : le modèle d'acteur est un modèle de programmation concurrente largement applicable, adapté aux modèles de mémoire partagée et aux modèles de mémoire distribuée, ainsi qu'à la résolution de problèmes géographiquement distribués, offrant une forte tolérance aux pannes.

  5. Processus séquentiels de communication (CSP) : en surface, le modèle CSP est très similaire au modèle d'acteur, tous deux basés sur la transmission de messages. Cependant, le modèle CSP se concentre sur le canal de transmission des informations, tandis que le modèle d'acteur se concentre sur les entités aux deux extrémités du canal, et le code utilisant le modèle CSP aura un style sensiblement différent.

  6. Parallélisme au niveau des données : à l'intérieur de chaque ordinateur portable se trouve un superordinateur, le GPU. Les GPU tirent parti du parallélisme au niveau des données, non seulement pour un traitement rapide des images, mais également pour des champs plus larges. Si vous souhaitez faire de l'analyse par éléments finis, des calculs de mécanique des fluides ou d'autres calculs numériques à grand volume, les performances du GPU seront le meilleur choix.

  7. Architecture Lambda : L'arrivée de l'ère du big data est indissociable du parallélisme - il suffit désormais d'augmenter les ressources de calcul pour avoir la capacité de traiter des téraoctets de données. L'architecture Lambda combine les caractéristiques de MapReduce et du traitement de flux, et est une architecture qui peut gérer une variété de problèmes de Big Data.

Il existe les modèles de concurrence suivants dans les langages généraux.

  • modèle de filetage

    Abstraction du système d'exploitation, efficacité de développement élevée, forte consommation d'E/S, surcharge de commutation élevée avec une simultanéité élevée.

  • modèle asynchrone

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

  • 协程模型

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

二. 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 est utilisé pour reprendre l'exécution. Il peut être long de copier tout ABC et de céder la pile dans la pile d'exécution, car la pile d'exécution peut être très profonde. Après enquête, les membres de la communauté Loom ont découvert que la fonction C peut avoir plus plus d'une opération d'accès aux données. , Une fois la pile d'exécution restaurée, le contexte peut être à nouveau commuté en raison de l'opération IO de C, donc Loom utilise une méthode de copie paresseuse, ne copiant qu'une partie à la fois et return barriercontinuant à la copier dans la pile une fois l'exécution terminée. De cette manière, à l'exception du premier surdébit de commutation, qui est relativement important, tous les autres surdébits de commutation seront faibles.

D'autre part, la POO enregistrée dans refStack doit être restaurée, car de nombreux GC peuvent modifier l'adresse OOP pendant l'exécution, et il peut y avoir des problèmes d'accès s'il n'est pas restauré.

5. Utilisation du métier à tisser

  • Création de thread virtuel

    • Créer VirtualThread via Thread.builder

    • Créer une usine VirtualThread via Thread.builder

    • Planificateur ForkJoinPool par défaut (équilibrage de charge, expansion automatique), prend en charge le planificateur personnalisé

  • planificateur personnalisé
static ExecutorService SCHEDULER_1 = Executors.newFixedThreadPool(1);
Thread thread = Thread.ofVirtual().scheduler(SCHEDULER_1).start(() -> System.out.println("Hello"));
thread.join();
复制代码
  • Créer un pool de coroutines
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);
}
复制代码

Guess you like

Origin juejin.im/post/6974216114318508046