Dieser Beitrag ist CyberAgent Developers Adventskalender 2019 14. Tag Artikel-
Ich bin ein Full-Stack-Ingenieur, aber wie sieht die Programmierung aus einer Full-Stack-Perspektive aus? Ich hoffe ich kann dir das sagen.
Für diejenigen, die mehrere Sprachen lernen, wenn ich eine andere Sprache lerne, könnte ich dies mit dieser Sprache tun, aber diese Sprache ist praktisch. Ich denke, du hast das gefühlt. Ich dachte, dass es nicht viele Artikel gibt, die Techniken, Methoden, Schreibmethoden usw. in verschiedenen Sprachen nebeneinander erklären, deshalb werde ich es wagen, Beispiele derselben Methode in verschiedenen Sprachen als einen Artikel zu vergleichen.
Dieses Mal möchte ich Ihnen ein Beispiel geben, wann Sie die asynchrone Verarbeitung gleichzeitig ausführen möchten. In diesem Artikel gibt es jedoch kleine Schwierigkeiten wie "Unterschied zwischen paralleler und paralleler Verarbeitung" und "Unterschied zwischen Multithread- und Ereignisschleife". Ich werde es weglassen. Es ist ein langer Satz, weil er mehrere Sprachen beschreibt, aber ich hoffe, Sie werden ihn lesen.
Dieses Mal schreibe ich Beispiele in den Klassikern aus alten Zeiten und den folgenden Sprachen, die ich heutzutage oft benutze.
--JavaScript (Beispiel in Node.js) --Kotlin (Beispiel auf Android)
Ich denke, dass Mikrodienste in den letzten Jahren populär geworden sind, aber wenn es um Mikrodienste geht, treffen Sie umso mehr APIs, je näher Sie der Vorderseite kommen. Sie sagen: "Diese Daten stammen von dieser API" und "Diese Daten stammen von dieser API ...". Stellen Sie zu viel die Ergebnisse zusammen und verarbeiten Sie sie für den Bildschirm. .. .. Ich denke, dass die Zahl der zu erledigenden Fälle erheblich zunimmt. Es gibt Fälle, in denen dies mit der BFF-API (Backend for Frontend) erfolgt, aber ich denke, es gibt einige Fälle, in denen dies auf der Clientseite (JS- oder Smartphone-Anwendungsseite) erfolgt. Je mehr APIs Sie treffen, desto schneller werden die Antwortgeschwindigkeit und die Geschwindigkeit der Bildschirmanzeige beeinflusst. Anstatt die APIs nacheinander auszuführen, möchten Sie die APIs gleichzeitig verarbeiten und dann die Ergebnisse verarbeiten. Ich denke, dass die Zahl der Fälle zunimmt.
Beispielsweise ist es erforderlich, eine API auszuführen, die 3 Sekunden dauert, eine API, die 2 Sekunden dauert, eine API, die 1 Sekunde dauert, und 3 APIs, und es gibt einen Prozess, bei dem sie für den Bildschirm 1 Sekunde lang verarbeitet wird. Wenn die Ergebnisse angehängt werden, kann sie auf dem Bildschirm angezeigt werden. Im Falle von.
Als Voraussetzung ist Overhead wie Klassenaufruf ausgeschlossen
Wenn Sie die APIs in der in der folgenden Abbildung gezeigten Reihenfolge ausführen, dauert es 7 Sekunden, bis der Bildschirm angezeigt wird.
Wenn Sie die API gleichzeitig ausführen, kann sie auf 4 Sekunden verkürzt werden, bevor der Bildschirm angezeigt wird.
Ich denke, dass die Anzahl der Fälle, in denen die Methode des gleichzeitigen Werfens erforderlich ist, zunimmt, aber ich denke, dass die Schreibmethode je nach Sprache sehr unterschiedlich ist. Grundsätzlich ändert sich der in der obigen Abbildung gezeigte Durchfluss nicht
--Denken
Sobald Sie gelernt haben, können Sie es sich mit ein wenig Recherche fast vorstellen, auch wenn sich die Sprache ändert. Natürlich unterscheiden sich die Eigenschaften je nach Sprache. Wenn Sie also bei der Verwendung nicht tief graben, kann dies zu unerwarteten Problemen führen, aber ich denke, dass Sie es nur verstehen werden, wenn Sie es tatsächlich verwenden. Deshalb werde ich es hier nicht tief schreiben.
Vergleichen wir nun die Programme, die gleichzeitig die Verarbeitung in verschiedenen Sprachen auslösen.
Verwenden Sie in JavaScript (Node.js) "Promise". Wenn Sie sich die Quelle unten ansehen, ist Promise nur Promise.all? Ich denke, aber wenn Sie der Methode Async hinzufügen, werden alle Rückgaben "Versprechen" sein. Es ist "Promise.all", das etwas in der Nähe davon tut und gleichzeitig Versprechen ausführt. Wenn Sie in Promise.all warten, wird in der Zeile Promise.all gewartet, bis alle Ergebnisse der im Array all angegebenen Funktionen zurückgegeben werden. Da test1 und test2 fast gleichzeitig ausgeführt werden, ist es möglich, eine Antwort schneller zurückzugeben, als sie nacheinander auszuführen. Obwohl es sich um JavaScript handelt, wird es auf Node.js vorausgesetzt. Mit JS im Browser können Sie Promise mit Chrome oder neueren Browsern verwenden, aber mit IE11 oder älteren Browsern (obwohl die Anzahl steigt, wenn die Unterstützung eingestellt wird) gibt es Dinge, die nicht verwendet werden können. Legen Sie also Polyfill mit Webpack usw. Sie müssen es einmal konvertieren, damit es in älteren Browsern funktioniert.
const main = async () => {
console.log('[JavaScript] main start')
//Wirf mehrere Prozesse asynchron, Promise.Warten Sie, bis alle Ergebnisse mit allen zurückgegeben wurden
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) //Angenommen, Sie rufen eine API oder etwas auf
return 123
}
const test2 = async () => {
console.log("test2 method")
await sleep(1000) //Angenommen, Sie rufen eine API oder etwas auf
return 456
}
//JavaScript ist ein nützlicher Thread.Da es kein schlafähnliches Ding gibt, werde ich etwas Nahes reproduzieren
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
//Hauptfunktion ausführen
main()
$ npm run main
[JavaScript] main start
test1 method
test2 method
123
456
[JavaScript] main end
Promise dient zum Ausführen der asynchronen Verarbeitung, aber Node.js V8 oder früher (mit Ausnahme der Beta-Version) verfügt nicht über async / await. Verwenden Sie bei der asynchronen Verarbeitung "new Promise ()" und das Ergebnis der asynchronen Verarbeitung. Es war eine Hölle des Rückrufs, eine Rückrufkette zu bilden. Die Callback-Hölle macht die Quelle weniger lesbar, sodass Sie asynchronisieren / warten können, sie besser lesbar ist und Sie nicht zu viel Versprechen schreiben müssen. Wenn Sie den Rückgabewert mit TypeScript betrachten, können Sie sehen, dass der Rückgabewert der Funktion mit Async immer ein Versprechen hat.
// TypeScript
const testFunc = (): Promise<number> => {
return 123
}
Promise.all bleibt bestehen, da es gleichzeitig verwendet wird. Grundsätzlich müssen Sie Promise jedoch nicht zu häufig verwenden. Einige ältere Bibliotheken geben das Ergebnis jedoch weiterhin mit einem Rückruf zurück. In einem solchen Fall können Sie es, sobald Sie es in Promise verpackt haben, asynchronisieren / warten lassen, sodass ich es als Ergänzung beschreiben werde.
/**
*Asynchronisieren Sie die Bibliothek, die die Rückruffunktion ist/Beispiel, wenn Sie warten möchten
*/
const main = async () => {
console.log('[JavaScript] main start')
const result = await testFunc()
console.log(result)
console.log('[JavaScript] main end')
}
//asynchroner Rückruf mit Versprechen/Umschließt die API, die mit dem Rückruf zurückkommt, damit sie mit Wartezeit ausgeführt werden kann
const testFunc = () => {
return new Promise(resolve => {
console.log("testFunc method")
callbackMethod(ret => {
resolve(ret)
})
})
}
//Änderung der Bibliothek, die ein Rückruf ist
const callbackMethod = callback => {
callback(456)
}
main()
Kotlin benutzt "Coroutine". Coroutine funktioniert mit neueren Versionen von Kotlin, möglicherweise jedoch nicht mit älteren Versionen. In diesem Fall müssen Sie Thread aus der Java-Ära verwenden, daher erkläre ich die Annahme eines neuen Kotlins, der Coroutine verwenden kann. Coroutine ist einfach eine benutzerfreundliche Version von Java Thread. Wenn Sie sich die Thread-Nummer im Debug ansehen, wenn Sie Coroutine ausführen, können Sie sehen, dass sie in einem anderen Thread ausgeführt wird. Grundsätzlich ist die Idee von async / await dieselbe wie die von JS. Sollte die Methode, mit der Coroutine Attach ausgeführt wird, suspend and await auf der Reserveseite ausführen? Würden Sie gerne? Ich vertraue das Urteil an oder verwende runBlocking usw., damit es sich anfühlt, als würde es zur synchronen Verarbeitung zurückkehren, wenn es aufgerufen wird. In diesem Beispiel wollte ich test1 und test2 gleichzeitig ausführen. In doAll rufe ich zwei Funktionen asynchron auf, warte, bis die beiden Ergebnisse mit await abgeschlossen sind, und kehre dann zum Aufrufer zurück. Es gibt kein Promise.all wie JS. Wenn Sie also das Ergebnis in ein Array einfügen, können Sie die Promise.all-ähnliche Methode reproduzieren.
package sample.kotlin
import kotlinx.coroutines.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
fun main() {
println("[kotlin] main start")
//Da doAll angehalten ist, führen Sie runBlocking aus und warten Sie, bis die gesamte Verarbeitung abgeschlossen ist.
runBlocking {
val result = doAll()
println(result[0])
println(result[1])
}
println("[kotlin] main end")
}
/**
* JavaScriptPromise.Implementierung ähnlich allen Verarbeitungsergebnissen
*Executor Service In Java aufrufbar
*/
suspend fun doAll(): Array<Int> = coroutineScope {
val ret1 = async { test1() }
val ret2 = async { test2() }
// Promise.alles Wind
//Wenn ich das Ausführungsergebnis der Methode in ein Array lege und es zurückgebe, ist es ein Versprechen.Werde wie alle
arrayOf(ret1.await(), ret2.await())
}
suspend fun test1(): Int {
println("test1 method")
delay(2000) //Angenommen, Sie rufen eine API oder etwas auf
return 123
}
suspend fun test2(): Int {
println("test2 method")
delay(1000) //Angenommen, Sie rufen eine API oder etwas auf
return 456
}
Klicken Sie in Android Studio mit der rechten Maustaste auf Main.kt und führen Sie Ausführen aus.
(Ich dachte, ich könnte Kotlinc oder so etwas setzen und es über die Befehlszeile ermöglichen, aber ich habe Coroutine verwendet und hatte keine Zeit, es einzustellen, also habe ich es von Android Studio aus gemacht, um Zeit zu sparen m (_ _) m)
[kotlin] main start
test1 method
test2 method
123
456
[kotlin] main end
Coroutine ist einfacher als Java Thread, hat aber viele Funktionen und welche sollte ich verwenden? Ich denke, es wird. Wenn Sie diesmal beispielsweise suspendCoroutine verwenden, können Sie den Rückruf auf die gleiche Weise wie async / await mit JS 'neuem Promise () stoppen.
cont.resume(it)
Das ist js
resolve(ret)
Wenn Sie den Lebenslauf ausführen, wird er zurückgegeben.
Dadurch kann der Rückruf wie in dieser Zeile asynchron ausgeführt werden.
val func = async { testFunc() }
val result = func.await()
Sie können das, was Sie wollen, in verschiedenen Sprachen tun, auch wenn Sie es anders schreiben.
package sample.kotlin
import kotlinx.coroutines.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
fun main() {
println("[kotlin] main start")
//In JavaScript fühlt es sich an, als würde man warten. Es wird warten, bis die Verarbeitung in runBlocking auf der Straße abgeschlossen ist
runBlocking {
val func = async { testFunc() }
val result = func.await()
println(result)
}
println("[kotlin] main end")
}
/**
*Neues Versprechen in JavaScript()Der gleiche Effekt wie.
*asynchroner Rückruf mit suspendCoroutine/Umschließt die API, die mit dem Rückruf zurückkommt, damit sie mit Wartezeit ausgeführt werden kann
*/
suspend fun testFunc(): Int {
println("testFunc method")
return suspendCoroutine { cont ->
callbackMethod {
cont.resume(it)
}
}
}
//Änderung der Bibliothek, die ein Rückruf ist
fun callbackMethod(ret: (Int) -> Unit) {
ret(456)
}
go verwendet Goroutine und Chan. goroutine wird asynchron, wenn Sie eine Funktion mit dem Schlüsselwort go
ausführen. Sie können das Ergebnis nicht einfach mit "go" erhalten. Verwenden Sie also "chan" (Kanal), um das asynchrone Ergebnis zu erhalten. Chan hat den gleichen Effekt wie Warten und wird mit <-
warten. Davon abgesehen funktioniert es im gleichen Ablauf wie zuvor. Die Bewegung des Inhalts wird jedoch von go als Parallelverarbeitung übernommen. Ich werde hier nicht so detailliert auf Kanäle eingehen, aber im Falle von go gibt es auch eine waitGroup. Wenn Sie mehr wissen möchten, suchen Sie bitte nach "Parallelverarbeitung und Parallelverarbeitung".
Diesmal habe ich mich für Chan entschieden, weil ich das erste, zweite und zuverlässige Ergebnis speichern wollte, wie z. B. das bisher eingeführte Promise.all von JS. Der Punkt hier ist, dass wir zwei Kanäle erstellen und nur einen Puffer für das zweite Argument reservieren. Stellen Sie den Puffer in einem Kanal auf 2 ein, wie unten gezeigt.
channel := make(chan int, 2)
go Test1(chanel)
go Test2(chanel)
resultArray = append(resultArray, <- chanel)
resultArray = append(resultArray, <- chanel)
Sie können es so schreiben, es gibt jedoch keine Garantie dafür, dass das Ergebnis von Test1 im ersten Array zurückgegeben wird. Bei tatsächlicher Ausführung kann das Ergebnis von Test2 abhängig vom Zeitpunkt der Verarbeitung in Array 0 enthalten sein. Durch die Trennung der Kanäle kann das Ergebnis von Test1 sicher in Kanal1 eingefügt werden, daher habe ich es in diesem Beispiel getrennt. In diesem Sinne sollten Sie sich die folgenden Quellen ansehen.
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)
//Asynchrone Verarbeitung mit Goroutine
go Test1(chanel1)
go Test2(chanel2)
// Promise.Alle Ausführungsergebnisse werden in ein Array gepackt.
// <-Ist wie das Schlüsselwort await in js und kotlin. Zukunft in Java.get()
//Es fühlt sich an, als würde man warten, bis alle Ausführungsergebnisse gekauft sind.
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) //Angenommen, Sie rufen eine API oder etwas auf
c <- 123
}
func Test2(c chan int) {
fmt.Println("test2 method")
time.Sleep(time.Second * 1) //Angenommen, Sie rufen eine API oder etwas auf
c <- 456
}
$ go run src/main.go
[go] main start
test2 method
test1 method
123
456
[go] main end
Da goroutine die Ausführungsreihenfolge nicht garantiert, wird test2 zuerst ausgeführt, aber da es wartet und wartet, muss sich der Anrufer nicht darum kümmern.
Da Java eine alte Sprache ist, gibt es verschiedene Methoden der asynchronen Verarbeitung. Dieses Mal verwenden wir jedoch Executor, der um Java7 herum hinzugefügt wurde. Es gibt viele Möglichkeiten, dies zu tun, aber im Grunde wird das gleiche mit Java-Threading gemacht. Dieses Mal wollte ich das Ergebnis der asynchronen Ausführung erhalten, also habe ich es mit der Klasse "Callable" implementiert. Die asynchrone Verarbeitung wird beim Senden ausgeführt Der Teil, in dem "future.get ()" gesetzt ist, wartet, der auf das Ausführungsergebnis wartet. Durch Einfügen des Ergebnisses in List wird das Ausführungsergebnis wie Promise.all reproduziert. Im Fall von Java gibt es weitere Verfahren, um es asynchron zu machen, z. B. das Erstellen einer Klasse oder die Verwendung von Future. Ich habe Executor verwendet, weil Kotlin auf JVM ausgeführt wird. Das Ergebnis ist also Java. Als ich diesen Artikel schrieb, musste ich bei Verwendung der CameraX-Bibliothek von Android eine Instanz von Executor übergeben Ich wollte mit Executor ein Beispiel erstellen. Es ist hier in der Nähe. Google CodelabsGetting Started with CameraX
Java ist eine alte Sprache und erfordert viel Arbeit, aber selbst wenn es um Kotlin geht, werden Java-Bibliotheken oft aufgerufen, und was ist Thread überhaupt? Ich denke, dass es immer noch nützlich ist, solche Dinge zu unterdrücken.
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.Implementierung ähnlich allen Verarbeitungsergebnissen
*Eine Implementierung ähnlich dem Ergebnis der Asynchronisierung mit Kotlins coroutineScope
*/
private static List<Integer> doAll() {
List<Future<Integer>> futures = new ArrayList<>();
List<Integer> result = new ArrayList<>();
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
//Bitte denken Sie, dass diese beiden Zeilen fast mit dem asynchronen Schlüsselwort oder Promise in js oder kotlin identisch sind.
futures.add(executor.submit(new Text1Class()));
futures.add(executor.submit(new Text2Class()));
// Promise.alles Wind
//Wenn Sie das Ausführungsergebnis der Klasse, die Callable implementiert, in ArrayList einfügen und zurückgeben, versprechen Sie dies.Werde wie alle
for (Future<Integer> future : futures) {
result.add(future.get()); // future.get()Ist wie das Schlüsselwort await in js und kotlin
}
} catch (Exception e) {
e.printStackTrace();
} finally {
executor.shutdownNow(); //Java lässt den Prozess nicht herunter, es sei denn, Sie stoppen den Thread explizit.
}
//Es fühlt sich an, als würde man warten, bis alle Ausführungsergebnisse in dieser Schleife gekauft wurden.
return result;
}
}
class Text1Class implements Callable<Integer> {
@Override
public Integer call() throws InterruptedException {
System.out.println("test1 method");
Thread.sleep(2000); //Angenommen, Sie rufen eine API oder etwas auf
return 123;
}
}
class Text2Class implements Callable<Integer> {
@Override
public Integer call() throws InterruptedException {
System.out.println("test2 method"); //Angenommen, Sie rufen eine API oder etwas auf
Thread.sleep(1000);
return 456;
}
}
$ cd src
$ javac sample/Main.java
$ java sample.JavaApplication
[Java] main start
test1 method
test2 method
123
456
[Java] main end
Dieses Mal gab ich ein Beispiel für die gleichzeitige Ausführung der asynchronen Verarbeitung in vier Sprachen. Ich habe auch den Code anderer Sprachen in einer Form kombiniert, die der Ausführung von JavaScript ähnelt, aber die Grundidee ist die asynchrone Ausführung im Thread und die Idee, ein Signal zu empfangen, das auf den Abschluss der Ausführung des Threads mit wait wartet. JavaScript ist eine Single-Threaded-Ereignisschleife, und andere Sprachen sind Threads anstelle von Threads, aber das "asynchrone / wartende Konzept" und der "Verarbeitungsablauf" können angewendet werden, selbst wenn die Sprache unterschiedlich ist, so die "Idee" Ich denke, dass dies zu einer Verbesserung der technischen Erwerbsfähigkeit führen wird. Diese asynchrone ist nur ein Beispiel. Anstatt eine Sprache zu lernen, ist es wichtig, eine Sprache zu verwenden, um die Grundlagen von Architektur, Methoden, Lebenszyklen, Flüssen usw. zu verstehen.
Das Obige ist nur ein Beispiel, aber es ist wichtig, diese grundlegenden Dinge durch das Programm zu verstehen.
Wie kann ich Full Stack bekommen? Wird manchmal gefragt, aber im Ernst, "grundlegend" ist wichtig. Wenn Sie jedoch versuchen, einen Punkt breit und tief auswendig zu lernen (angewendet), gibt es natürlich kleine Unterschiede in jeder Sprache und Architektur, und es braucht Zeit, wie es ist. Vor langer Zeit waren Ingenieure nicht in kleine Bereiche unterteilt, aber in den letzten Jahren erfordert die Trennung zwischen Front-, Native-, Backend-, Infrastrukturingenieuren usw. fortgeschrittenere Kenntnisse und Erfahrungen in jedem Bereich, sodass sie nicht exklusiv sind. Ich denke, das liegt daran, dass das Aufholen schwierig geworden ist.
Trotzdem werde ich mit einem vollen Stapel weit und tief gehen. Natürlich gehe ich tiefer, also arbeite und studiere ich zwei- oder dreimal so viele Menschen. Ich werde die Bemühungen nicht verraten, und die Dinge, die als ganze Architektur oder als Bibliothek vorgeschlagen werden können, die nur vorgeschlagen werden können, weil ich tief in allen Bereichen weiß, werden sich dramatisch erweitern. Ich denke, es macht viel Spaß, diese Art von Entwicklung durchführen zu können.
Wenn Sie gerade ein Programm starten, ist es für Sie einfacher, es zu verstehen, wenn Sie die Grundlagen der oben genannten Programme in einer Sprache verstehen und die nächste Sprache lernen, nachdem Sie es frei verwenden können. Ich werde. Darüber hinaus werden Informatik wie Computer und Betriebssysteme später vorgestellt. Wenn Sie also nicht wissen, wie man studiert, können Sie die Prüfung zum Ingenieur für grundlegende Informationsverarbeitung oder zum Ingenieur für angewandte Verarbeitung ablegen. Sie können sich auch Nachschlagewerke wie Prüfungen ansehen. Ich habe eine Qualifikation, aber ich erinnere mich nicht, dass die Qualifikation selbst sehr nützlich war ^^; Der grundlegende Mechanismus, den ich dabei gelernt habe, ist jedoch immer noch nützlich.
Da Programmiersprachen und Frameworks beliebt sind, denken Sie möglicherweise, dass Sie das bisher Gelernte nicht verwenden können. Selbst wenn sich die Sprache oder das Framework ändern, ändern sich die Grundlagen nicht. Selbst wenn neue Dinge auftauchen, werden die gelernten "Ideen" definitiv irgendwo nützlich sein.
In meinem Beispiel habe ich kürzlich mit Kotlin unter Android Channel (Blocking Queue in Java), Reentrant Lock, TCP / UDP-Kommunikation usw. erstellt und eine thread-sichere Originalbibliothek aus einer niedrigen Schicht, aber 10, erstellt Ich war an der Entwicklung eines Frameworks beteiligt, das threadsichere Programme und kommunikationsbezogene Programme in einem Projekt verwendet, an dem ich vor etwa einem Jahr teilgenommen habe. Ungefähr zu dieser Zeit dachte ich, dass die Ideen und Designfähigkeiten, die ich damals gelernt hatte, jetzt nützlich waren.
Technologie ist in jeder Epoche in Mode und veraltet.
"Ich werde die Ideen, die ich durch meine Bemühungen gelernt habe, nicht verraten."
Abschließend möchte ich mit diesem einen Wort schließen.
Hier ist das Beispielprojekt, das ich dieses Mal gemacht habe. https://github.com/tanaka-yui/parallel-async-sample
Vielen Dank für Ihren Aufenthalt bei uns.