Vue d'ensemble du traitement parallèle et asynchrone dans une perspective full-stack (JavaScript (Node.js), Kotlin, Go, Java)

introduction

Cet article est CyberAgent Developers Advent Calendar Article du 14e jour 2019-

Je suis un ingénieur full-stack, mais à quoi ressemble la programmation d'un point de vue full-stack? J'espère pouvoir vous le dire.

Pour ceux qui apprennent plusieurs langues, lors de l'apprentissage d'une langue différente, il était possible de le faire dans cette langue, mais cette langue est pratique. Je pense que vous avez ressenti cela. Je ne pense pas qu'il y ait beaucoup d'articles qui expliquent côte à côte des techniques, des méthodes et des méthodes d'écriture dans différentes langues, alors j'oserai comparer des exemples de la même méthode dans différentes langues en un seul article.

Cette fois, je voudrais vous donner un exemple de quand vous voulez exécuter un traitement asynchrone en même temps, mais dans cet article, il y a de petites difficultés telles que «différence entre le traitement parallèle et parallèle» et «différence entre le multithread et la boucle d'événements». Je vais l'omettre. C'est une longue phrase car elle décrit plusieurs langues, mais j'espère que vous la lirez.

Cette fois, j'écris des exemples dans les classiques de l'ancien temps et dans les langues suivantes que j'utilise souvent ces jours-ci.

--JavaScript (exemple dans Node.js) --Kotlin (exemple sur Android)

Lancer le traitement de manière asynchrone en même temps

Je pense que les micro-services sont devenus populaires ces dernières années, mais en ce qui concerne les micro-services, plus vous vous rapprochez de la face avant, plus vous touchez d'API, en disant: "Ces données proviennent de cette API" et "Ces données proviennent de cette API ..." Trop, rassemblez les résultats et traitez-les pour l'écran. .. .. Je pense que le nombre de cas à faire augmente considérablement. Il y a des cas où cela se fait avec l'API BFF (Backend for frontend), mais je pense qu'il y a pas mal de cas où cela se fait du côté client (côté application JS ou smartphone). Comme dans tous les cas, plus vous frappez d'API, plus la vitesse de réponse et la vitesse d'affichage de l'écran seront affectées, donc au lieu d'exécuter les API une par une dans l'ordre, vous voulez traiter les API en même temps, puis traiter les résultats. Je pense que le nombre de cas augmente.

Par exemple, il est nécessaire d'exécuter une API qui prend 3 secondes, une API qui prend 2 secondes, une API qui prend 1 seconde et 3 API. Dans le cas de.

Si vous exécutez les API dans l'ordre indiqué dans la figure ci-dessous, il faudra 7 secondes pour afficher l'écran.

順番にAPIを実行した場合、合計7秒かかる図

Si vous exécutez l'API en même temps, elle peut être raccourcie à 4 secondes avant que l'écran ne s'affiche.

同時にAPIを実行した場合、合計4秒かかる図

Je pense que le nombre de cas où la méthode de lancer en même temps est requise augmente, mais je pense que la méthode d'écriture est assez différente selon la langue. Fondamentalement, le flux comme indiqué dans la figure ci-dessus ne change pas, donc

--En pensant

Une fois que vous avez appris, même si la langue change, vous pouvez presque l'imaginer avec un peu de recherche. Bien sûr, les caractéristiques diffèrent selon la langue, donc si vous ne creusez pas profondément lors de son utilisation, cela peut entraîner des problèmes inattendus, mais je pense que vous ne le comprendrez pas à moins que vous ne l'utilisiez réellement, donc je ne l'écrirai pas profondément ici.

Maintenant, comparons les programmes qui lancent réellement le traitement en même temps dans différentes langues.

Traitement asynchrone de JavaScript (Node.js)

Dans JavaScript (Node.js), utilisez Promise. Si vous regardez la source ci-dessous, est-ce que Promise est seulement Promise.all? Je pense, mais si vous ajoutez async à la méthode, tous les retours seront «Promise». C'est Promise.all qui fait quelque chose de proche de cela, exécute les promesses en même temps. Si vous attendez dans Promise.all, il attendra dans la ligne de Promise.all jusqu'à ce que tous les résultats des fonctions spécifiées dans le tableau de tous soient retournés. Puisque test1 et test2 sont exécutés presque en même temps, il est possible de renvoyer une réponse plus rapidement que de les exécuter en séquence. Bien qu'il s'agisse de JavaScript, il est basé sur Node.js. Avec JS sur le navigateur, vous pouvez utiliser Promise avec Chrome ou des navigateurs récents, mais avec IE11 ou des navigateurs plus anciens (bien que le nombre augmente là où le support est interrompu), il y a des choses qui ne peuvent pas être utilisées, alors mettez polyfill avec webpack etc. Vous devez le convertir une fois pour qu'il fonctionne sur les anciens navigateurs.

Source JavaScript (Node.js)

const main = async () => {
    console.log('[JavaScript] main start')
    //Lancer plusieurs processus de manière asynchrone, promesse.Attendez que tous les résultats soient renvoyés avec tous
    const result = await Promise.all([
        test1(),
        test2(),
    ])
    console.log(result[0])
    console.log(result[1])
    console.log('[JavaScript] main end')
}

const test1 = async () => {
    console.log("test1 method")
    await sleep(2000) //En supposant que vous appelez une API ou quelque chose du genre
    return 123
}

const test2 = async () => {
    console.log("test2 method")
    await sleep(1000) //En supposant que vous appelez une API ou quelque chose du genre
    return 456
}

//JavaScript est un thread utile.Puisqu'il n'y a pas de chose semblable au sommeil, je vais reproduire quelque chose de proche
function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

//Exécuter la fonction principale
main()

Résultat d'exécution

$ npm run main

[JavaScript] main start
test1 method
test2 method
123
456
[JavaScript] main end

Relation supplémentaire entre Promise et async / await

Promise sert à exécuter un traitement asynchrone, mais Node.js V8 ou une version antérieure (à l'exception de la version bêta) n'a pas async / await, et lors du traitement asynchrone, utilisez new Promise () et le résultat du traitement asynchrone. C'était un enfer de rappel pour faire une chaîne de rappel. Callback hell rend la source moins lisible, vous pouvez donc faire async / await, c'est plus lisible et vous n'avez pas à écrire trop de Promise. Si vous regardez la valeur de retour à l'aide de TypeScript, vous pouvez voir que la valeur de retour de la fonction avec async a toujours une promesse.

// TypeScript
const testFunc = (): Promise<number> => {
   return 123
}

Promise.all reste car il est utilisé simultanément, mais en gros, vous n'avez pas besoin d'utiliser trop de Promise, mais certaines bibliothèques plus anciennes renvoient toujours le résultat avec un rappel. Dans un tel cas, une fois que vous l'avez enveloppé dans Promise, vous pouvez le rendre asynchrone / attendu, donc je vais le décrire comme un supplément.

Source de l'échantillon

/**
 *Asynchronisez la bibliothèque qui est la fonction de rappel/Exemple lorsque vous voulez attendre
 */
const main = async () => {
    console.log('[JavaScript] main start')
    const result = await testFunc()
    console.log(result)
    console.log('[JavaScript] main end')
}

//rappel asynchrone avec promesse/Enveloppe l'API qui revient avec un rappel afin qu'elle puisse être exécutée avec await
const testFunc = () => {
    return new Promise(resolve => {
        console.log("testFunc method")
        callbackMethod(ret => {
            resolve(ret)
        })
    })
}

//Changement de bibliothèque qui est un rappel
const callbackMethod = callback => {
    callback(456)
}

main()

Traitement asynchrone Kotlin

Kotlin utilise «Coroutine». Coroutine fonctionne avec les versions plus récentes de kotlin, mais peut ne pas fonctionner avec les anciennes versions. Dans ce cas, vous devrez utiliser Thread de l'ère Java, donc je vais vous expliquer en supposant un nouveau Kotlin qui peut utiliser Coroutine. Coroutine est simplement une version facile à utiliser de Java Thread. Si vous regardez le numéro de thread dans le débogage lorsque vous exécutez Coroutine, vous pouvez voir qu'il s'exécute dans un autre thread. Fondamentalement, l'idée d'async / await est la même que celle de JS, la méthode exécutant Coroutine attach doit-elle se suspendre et attendre du côté réserve? Voudriez-vous? Je confie le jugement ou utilise runBlocking, etc. pour donner l'impression de revenir au traitement synchrone une fois appelé. Dans cet exemple, je voulais exécuter test1 et test2 en même temps, donc dans doAll, j'appelle deux fonctions avec async, j'attends que les deux résultats se terminent par await, puis je reviens à l'appelant. Il n'y a pas de Promise.all comme JS, donc si vous mettez le résultat dans un tableau, vous pouvez reproduire la méthode de type Promise.all.

Source Kotlin

package sample.kotlin

import kotlinx.coroutines.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

fun main() {
    println("[kotlin] main start")

    //Puisque doAll est suspendu, exécutez runBlocking et attendez que tout le traitement à l'intérieur soit terminé.
    runBlocking {
        val result = doAll()
        println(result[0])
        println(result[1])
    }

    println("[kotlin] main end")
}

/**
 * JavaScriptPromise.mise en œuvre similaire à tous les résultats de traitement
 *Service exécuteur appelable en Java
 */
suspend fun doAll(): Array<Int> = coroutineScope {
    val ret1 = async { test1() }
    val ret2 = async { test2() }

    // Promise.tout le vent
    //Quand je mets le résultat de l'exécution de la méthode dans un tableau et que je le renvoie, c'est une promesse.Devenir comme tout
    arrayOf(ret1.await(), ret2.await())
}

suspend fun test1(): Int {
    println("test1 method")
    delay(2000) //En supposant que vous appelez une API ou quelque chose du genre
    return 123
}

suspend fun test2(): Int {
    println("test2 method")
    delay(1000) //En supposant que vous appelez une API ou quelque chose du genre
    return 456
}

Résultat d'exécution

Cliquez avec le bouton droit sur Main.kt dans Android Studio et exécutez Exécuter.

(Je pensais que je pouvais mettre kotlinc ou quelque chose comme ça et le rendre possible à partir de la ligne de commande, mais j'ai utilisé coroutine et je n'ai pas eu le temps de le configurer, alors je l'ai fait depuis Android Studio pour gagner du temps m (_ _) m)

[kotlin] main start
test1 method
test2 method
123
456
[kotlin] main end

Supplément Coroutine tente également de convertir l'API de rappel en async / await

Coroutine est plus facile que Java Thread, mais il a beaucoup de fonctionnalités et laquelle dois-je utiliser? Je pense que ce sera. Cette fois, à titre d'exemple, si vous utilisez suspendCoroutine, vous pouvez arrêter le rappel de la même manière que async / await en utilisant le nouveau Promise () de JS.

cont.resume(it) C'est js resolve(ret) Si vous exécutez la reprise, elle sera renvoyée.

Cela permettra au rappel d'être asynchrone, comme dans cette ligne.

val func = async { testFunc() }
val result = func.await()

Vous pouvez faire ce que vous voulez dans différentes langues, même si vous l'écrivez différemment.

Source de l'échantillon

package sample.kotlin

import kotlinx.coroutines.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

fun main() {
    println("[kotlin] main start")

    //En JavaScript, c'est comme attendre. Il attendra que le traitement en runBlocking soit terminé dans la rue
    runBlocking {
        val func = async { testFunc() }
        val result = func.await()
        println(result)
    }
    println("[kotlin] main end")
}

/**
 *Nouvelle promesse en JavaScript()Le même effet que.
 *rappel asynchrone avec suspendCoroutine/Enveloppe l'API qui revient avec un rappel afin qu'elle puisse être exécutée avec await
 */
suspend fun testFunc(): Int {
    println("testFunc method")
    return suspendCoroutine { cont ->
        callbackMethod {
            cont.resume(it)
        }
    }
}

//Changement de bibliothèque qui est un rappel
fun callbackMethod(ret: (Int) -> Unit) {
    ret(456)
}

Passer au traitement asynchrone

go utilise goroutine et chan. goroutine devient asynchrone lorsque vous exécutez une fonction avec le mot-clé «go». Vous ne pouvez pas obtenir le résultat avec juste go, alors utilisez chan (channel) pour recevoir le résultat asynchrone. chan a le même effet que await et attendra avec <-. À part cela, il fonctionne dans le même flux qu'avant. Cependant, le mouvement du contenu est adopté par aller comme traitement parallèle. Je n'entrerai pas dans les détails sur les chaînes ici, mais dans le cas de go, il y a aussi un waitGroup. Si vous voulez en savoir plus, veuillez rechercher par «traitement parallèle et traitement parallèle».

J'ai choisi chan cette fois parce que je voulais stocker en toute sécurité le premier, le deuxième et les résultats, comme Promise.all de JS introduit jusqu'à présent. Le point ici est que nous créons deux canaux et ne réservons qu'un seul tampon pour le deuxième argument. Réglez le tampon sur 2 dans un canal comme indiqué ci-dessous.

channel := make(chan int, 2)

go Test1(chanel)
go Test2(chanel)

resultArray = append(resultArray, <- chanel)
resultArray = append(resultArray, <- chanel)

Vous pouvez l'écrire comme ceci, mais il n'y a aucune garantie que le résultat de Test1 sera renvoyé dans le premier tableau. Lorsqu'il est réellement exécuté, le résultat de Test2 peut être inclus dans le tableau 0 en fonction de la synchronisation du traitement. En séparant les canaux, le résultat de Test1 peut sûrement être placé dans channel1, donc je l'ai séparé dans cet échantillon. Dans cet esprit, je pense que vous devriez regarder les sources suivantes.

Aller source

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println("[go] main start")

	result := doAll()
	fmt.Println(result[0])
	fmt.Println(result[1])

	fmt.Println("[go] main end")
}

func doAll() []int {
	resultArray := make([]int, 0, 2)
	chanel1 := make(chan int, 1)
	chanel2 := make(chan int, 1)

	//Traitement asynchrone avec goroutine
	go Test1(chanel1)
	go Test2(chanel2)

	// Promise.Les résultats d'exécution de tous les styles sont regroupés dans un tableau.
	// <-C'est comme le mot-clé await dans js et kotlin. L'avenir à Java.get()
	//C'est comme attendre que tous les résultats de l'exécution soient achetés.
	resultArray = append(resultArray, <- chanel1)
	resultArray = append(resultArray, <- chanel2)

	close(chanel1)
	close(chanel2)
	return resultArray
}

func Test1(c chan int) {
	fmt.Println("test1 method")
	time.Sleep(time.Second * 2) //En supposant que vous appelez une API ou quelque chose du genre
	c <- 123
}

func Test2(c chan int) {
	fmt.Println("test2 method")
	time.Sleep(time.Second * 1) //En supposant que vous appelez une API ou quelque chose du genre
	c <- 456
}

Résultat d'exécution

$ go run src/main.go

[go] main start
test2 method
test1 method
123
456
[go] main end

Puisque goroutine ne garantit pas l'ordre d'exécution, test2 est exécuté en premier, mais comme il attend et attend, l'appelant n'a pas à s'en soucier.

Traitement asynchrone Java

Comme Java est un ancien langage, il existe plusieurs méthodes de traitement asynchrone, mais cette fois nous utiliserons Executor, qui a été ajouté à partir de Java 7. Il y a plusieurs façons de le faire, mais fondamentalement la même chose est faite avec le threading Java. Cette fois, je voulais recevoir le résultat de l'exécution asynchrone, donc je l'ai implémenté en utilisant la classe Callable. Le traitement asynchrone est exécuté lors de la soumission La partie où future.get () est défini est wait, qui attend le résultat de l'exécution. En mettant le résultat dans List, le résultat de l'exécution comme Promise.all est reproduit. Dans le cas de Java, il existe plus de procédures pour le rendre asynchrone, comme la création d'une classe ou l'utilisation de Future. J'ai utilisé Executor parce que Kotlin fonctionne sur JVM, donc le résultat est Java, mais au moment de la rédaction de cet article, lors de l'utilisation de la bibliothèque CameraX d'Android, j'ai dû passer une instance d'Executor, donc Je voulais faire un échantillon en utilisant Executor. C'est par ici. Google CodelabsGetting Started with CameraX

Java est un vieux langage et cela demande beaucoup de travail, mais même quand il s'agit de Kotlin, les bibliothèques Java sont souvent appelées, et qu'est-ce que Thread en premier lieu? Je pense qu'il est toujours utile de supprimer de telles choses.

Source Java

package sample;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

class JavaApplication {

    public static void main(String[] args) {
        System.out.println("[Java] main start");

        List<Integer> result = doAll();
        System.out.println(result.get(0));
        System.out.println(result.get(1));

        System.out.println("[Java] main end");
    }

    /**
     * JavaScriptPromise.mise en œuvre similaire à tous les résultats de traitement
     *Une implémentation similaire au résultat de l'async avec coroutineScope de Kotlin
     */
    private static List<Integer> doAll() {
        List<Future<Integer>> futures = new ArrayList<>();
        List<Integer> result = new ArrayList<>();
        ExecutorService executor = Executors.newFixedThreadPool(2);
        try {
            //Veuillez penser que ces deux lignes sont presque les mêmes que le mot-clé async ou Promise dans js ou kotlin.
            futures.add(executor.submit(new Text1Class()));
            futures.add(executor.submit(new Text2Class()));

            // Promise.tout le vent
            //Si vous mettez le résultat de l'exécution de la classe qui implémente Callable dans ArrayList et le renvoyez, promettez.Devenir comme tout
            for (Future<Integer> future : futures) {
                result.add(future.get()); // future.get()Est comme le mot-clé await dans js et kotlin
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            executor.shutdownNow(); //Java ne laisse pas le processus s'arrêter sauf si vous arrêtez explicitement le thread.
        }
        //On a l'impression d'attendre que tous les résultats d'exécution soient achetés dans cette boucle.
        return result;
    }
}

class Text1Class implements Callable<Integer> {

    @Override
    public Integer call() throws InterruptedException {
        System.out.println("test1 method");
        Thread.sleep(2000); //En supposant que vous appelez une API ou quelque chose du genre
        return 123;
    }
}

class Text2Class implements Callable<Integer> {

    @Override
    public Integer call() throws InterruptedException {
        System.out.println("test2 method"); //En supposant que vous appelez une API ou quelque chose du genre
        Thread.sleep(1000);
        return 456;
    }
}

Résultat d'exécution

$ cd src
$ javac sample/Main.java
$ java sample.JavaApplication

[Java] main start
test1 method
test2 method
123
456
[Java] main end

Résumé

Cette fois, j'ai donné un exemple d'exécution d'un traitement asynchrone en même temps en utilisant quatre langues. J'ai également combiné le code d'autres langages sous une forme similaire à l'exécution de JavaScript, mais l'idée de base est l'exécution asynchrone dans le thread et l'idée de recevoir un signal en attendant la fin de l'exécution du thread avec wait. JavaScript est une boucle d'événements à un seul thread, et les autres langages sont des threads au lieu de threads, mais l'idée de ʻasync / awaitet deflux de traitement peut être appliquée même si le langage est différent, j'ai donc dit idée` Je pense que cela conduira à l'amélioration de la capacité technique à acquérir. Cette asynchrone n'est qu'un exemple. Plutôt que d'apprendre une langue, il est important d'utiliser une langue pour comprendre les bases de l'architecture, des méthodes, des cycles de vie, des flux, etc.

--Processus, threads et programmes thread-safe pour le traitement asynchrone ――S'il s'agit d'un cycle de vie, les caractéristiques et les mouvements du cycle de vie de chaque framework ―― Ce que vous faites et ce que vous n'êtes pas bon dans cette langue

Ce qui précède n'est qu'un exemple, mais il est important de comprendre ces éléments de base à travers le programme.

finalement

Comment puis-je obtenir un stack complet? Est parfois demandé, mais sérieusement, «basique» est important. Cependant, si vous essayez de mémoriser un point largement et profondément (appliqué), bien sûr, il existe de petites différences dans chaque langue et architecture, et cela prend du temps tel quel. Il y a longtemps, les ingénieurs n'étaient pas divisés en petits domaines, mais ces dernières années, la division entre les ingénieurs frontaux, natifs, backend, infrastructures, etc. nécessite des connaissances et une expérience plus avancées dans chaque domaine, ce n'est donc pas exclusif. Je pense que c'est parce que le rattrapage est devenu difficile.

Même ainsi, je vais aller large et profond avec un stack complet. Bien sûr, je vais plus loin, donc je travaille et étudie deux ou trois fois plus de personnes. Je ne trahirai pas l'effort, et les choses qui peuvent être proposées comme une architecture entière ou une bibliothèque qui peuvent être proposées parce que je sais profondément dans tous les domaines vont s'étendre de façon spectaculaire. Je pense que c'est très amusant de pouvoir faire ce genre de développement.

Si vous venez de démarrer un programme, il vous sera plus facile de le comprendre si vous comprenez les bases des programmes ci-dessus dans une langue et apprenez la langue suivante après avoir pu l'utiliser librement. Je vais. De plus, l'informatique comme les ordinateurs et les systèmes d'exploitation apparaîtra plus tard, donc si vous ne savez pas comment étudier, vous pouvez passer l'examen d'ingénieur de traitement de l'information de base ou l'ingénieur en traitement appliqué. Vous pouvez également consulter des ouvrages de référence tels que des examens. J'ai une qualification, mais je ne me souviens pas que la qualification elle-même était très utile ^^; Cependant, le mécanisme de base que j'ai appris dans le processus est toujours utile.

Étant donné que les langages de programmation et les frameworks sont populaires, vous pouvez penser que vous ne pourrez pas utiliser ce que vous avez appris jusqu'à présent. Cependant, même si le langage ou le cadre change, les bases ne changent pas, donc même lorsque de nouvelles choses apparaissent, les «idées» que nous avons apprises seront certainement utiles quelque part.

Dans mon exemple, très récemment, j'ai créé Channel (Blocking Queue en Java), Reentrant Lock, communication TCP / UDP, etc. avec Kotlin sur Android et créé une bibliothèque originale thread-safe à partir d'une couche basse, mais 10 J'ai participé au développement d'un framework qui utilise des programmes thread-safe et des programmes liés à la communication dans un projet auquel j'ai participé il y a environ un an. C'est à cette époque que j'ai pensé que les idées et les compétences en design que j'avais acquises à cette époque étaient utiles maintenant.

La technologie est à la mode et obsolète à chaque époque,

«Je ne trahirai pas les idées que j'ai apprises grâce à mes efforts»

Enfin, je voudrais conclure par ce seul mot.

Voici l'exemple de projet que j'ai réalisé cette fois. https://github.com/tanaka-yui/parallel-async-sample

Merci de rester avec nous.

Recommended Posts

Vue d'ensemble du traitement parallèle et asynchrone dans une perspective full-stack (JavaScript (Node.js), Kotlin, Go, Java)
Traitement asynchrone de Python ~ Comprenez parfaitement async et attendez ~