Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

GO TechTalk #21 並列処理をGo/Rust/Kotlin/Python/JSで解...

GO TechTalk #21 並列処理をGo/Rust/Kotlin/Python/JSで解説!思想の違いを体感しよう

■ 内容
・技術書典への取り組みの紹介 (森下) p. 7 ~
・並列処理の基本 (森下) p. 11 ~
・Go編「Go ルーチンで並列処理を実装しよう」(森下) p. 23 ~
・Python編「ちょっとしたデータ分析の並列化・ Python」(西川) p. 31 ~
・Rust編「Rustにおける並列処理」(鈴木) p. 44 ~
・JavaScript編「JavaScript の 非同期処理 Promise、async/await を理解する」(橘) p. 61 ~
・Kotlin編「並行処理・非同期処理のアプローチ Kotlin」(狩谷) p. 82 ~

■ YouTube
https://www.youtube.com/live/m-1Drlk2G8w?feature=share&t=172

■ connpass
https://jtx.connpass.com/event/289233/

GO Inc. dev

August 09, 2023
Tweet

More Decks by GO Inc. dev

Other Decks in Programming

Transcript

  1. © GO Inc. タイムテーブル 4 19:00-19:05 オープニング 19:05-20:05 ・技術書典への取り組みの紹介 (森下)

    ・並列処理の基本 (森下) ・Go編「Go ルーチンで並列処理を実装しよう」(森下) ・Python編「ちょっとしたデータ分析の並列化・ Python」(西川) ・Rust編「Rustにおける並列処理」(鈴木) ・JavaScript編「JavaScript の 非同期処理 Promise、async/await を理解する」(橘) ・Kotlin編「並行処理・非同期処理のアプローチ Kotlin」(狩谷) 20:05-20:10 クロージング 20:10 終了
  2. © GO Inc. 6 自己紹介 GO株式会社 AI技術開発部 データプラットフォームグループ AI寄りのアーキテクト 『Visual

    Studio Code実践ガイド』技術評論社 技術書典 3 ~ 14 に新刊参加 @74th Atsushi Morimoto
  3. © GO Inc. • 年2回ペースで開催されている 技術系同人誌即売会 • オフラインとオンラインイベントが併設 • 技術書典10

    ~ 14 にて GO Inc. もスポンサー! • 技術書典11 ~ 14 にて 会社の有志サークルとして参加し 今まで4冊もの書籍を配布。 ※ GO Inc. は技術書典以外にも   Droid Kaigi、iOSDC、Go Conference など   多数協賛しています 技術書典とは 8
  4. © GO Inc. GO Inc. では社内に様々なサービスがあり、 それぞれ適材適所のプログラミング言語で実装されています。 それぞれの言語で、共通したテーマを解説したら面白いのでは? とテーマを定め有志で記事を執筆し、技術書典13で頒布しまし た。

    1. 並行処理・非同期処理のアプローチ …JS、Kotlin、Python、Rust 2. グラフアルゴリズム…Rust、Python 3. ロガーの組み方 …Go、Kotlin 4. 型とDI の扱い …Go、Python 特集『4テーマを GO Inc. が使う複数の言語で解説してみよう』 WebAPI、タクシー車載アプリ、ユーザアプリ、AIドラレコ、 事業者向け管理Webサイト、AIサービス、GISサービス、などなど 9
  5. © GO Inc. 例2: 2つのレーンを用意して 2人が同時に処理する 例1: 2つのタスクのレーンを用意して 時間で切り替えて処理する 14

    複数の処理を同時に扱うこと。 並行処理(Concurrent)とは Task1 Task2 Task1 Task2 Task1 Task2 Task1 Task2 2つの仕事がある時 シゴトシテ
  6. © GO Inc. 19 メッセージを「溜めて」、処理する方式。 非同期処理によるメッセージ Work1 Work1 シゴトシテ Work2

    オボエトク ヒマ Work2 マダアッタ Work3 Work4 ・Work2 ・Work3 ・Work4 Work5 ・Work4 ・Work5 3ツマデネ イケル? Work3
  7. © GO Inc. Program Program メモリ 21 プログラムを起動すると、メモリ上に展開され、プロセスとしてOSで管理される。 プロセス内は実行の単位としてスレッドを用意し、OSはスレッドごとにCPUに割り当てる。 プロセスとスレッド

    プロセス1 スレッド1A スレッド1B プロセス2 スレッド2A スレッド2B 起動 プロセス3 スレッド3A スレッド3B CPU コア1 コア2 スレッド3A スレッド2B 起動 起動 OS 時間で 割り当て
  8. © GO Inc. 22 • 複数の処理の扱い方 ◦ not並行処理 :終わってからでないと、次の処理を渡せない。 ◦

    並行処理 :複数の処理を同時に「扱う」こと。 ◦ 並列処理 :複数の処理を同時に「処理する」こと。 • 処理間の連携の仕方 ◦ 同期処理 :同じ時間を共有して、メッセージをやりとりする方法。 ◦ 非同期処理 :メッセージを「溜めて」、処理する方式。 • プロセスとスレッド ◦ プログラムは、起動すると「プロセス」としてメモリに展開される。 ◦ プロセス内の処理は、複数の「スレッド」を立てることができる。 ◦ OSは、CPUの各コアにスレッドを割り当てて、実行される。 まとめ
  9. © GO Inc. • 独自の軽量スレッド「Goルーチン」と、 Goルーチン同士の同期・非同期通信の仕組み「チャネル」が、 構文 に含まれている(後ほど解説)。 • GoランタイムがOSのスレッドを隠蔽し、Goルーチンに時分割に割り当ててうごかす。

    割り当てのスケジューリングはGoランタイムに任されていて、指示できない。 ◦ つまり、Goルーチン = 並列処理 ではない • Goルーチンは、スレッド管理よりも軽量なため、使い捨てがかなりできる。 での並列・並行処理 OSスレッド OSスレッド OSスレッド OSプロセス Go ルーチン Go ルーチン Go ルーチン Go ルーチン Go ルーチン Go ルーチン 実装者に見えている世界 Goランタイムの仕事 24
  10. © GO Inc. • go 関数( ) で実行。 即時関数を使うと go

    func(){ }() にな る。 • 経験則では 1ms 以上のタスクであれば 逐次で起動しても 大きなオーバーヘッドにならない。 • CPUを可能な限り使って、 Goのランタイムが実行してくれる。 • ネットワーク通信などの処理でも、 積極的にGoルーチン化する。 Goルーチンの実行方法 func ParallelFunc(requests []entity.Request) { for _, request := range requests { go func(request entity.Request) { // 処理 fmt.Print(request) }(request) } } 25 例: 引数の配列の要素単位にGoルーチンを起動
  11. © GO Inc. 26 ストリーミング処理にGoルーチンを使う • 処理を段階に分けて、分割する。 • より高負荷な処理(デコード・整形処理)で、Goルーチンをたくさん起動しておき、 並列数を増やして処理をする。

    • では、Goルーチンの起動時以外に、 このGoルーチン間のデータ渡しはどうすれば良いか?→次スライド Go ルーチン Go ルーチン Go ルーチン Go ルーチン Go ルーチン データ受信処理 デコード・整形処理 Go ルーチン Go ルーチン DB書込処理 Data Data Data Data 例:車両のGPS座標データをDBに取り込む処理
  12. © GO Inc. • チャネルは FIFO バッファ • 変数 <-

    チャネル名 で受け取り チャネル名 <- 変数 で受け渡す。 • 事前にGoルーチンを起動しおき、 処理の入力・出力をチャネルにして、 for ループさせたりする。 • バッファサイズを指定でき、 “0”だと同期処理に、 ”1以上”だと非同期処理になる。 ストリーミング処理のデータ渡しに チャネル を使う Go ルーチン Go ルーチン Go ルーチン Go ルーチン Go ルーチン データ受信処理 デコード・整形処理 Go ルーチン Go ルーチン DB書込処理 チャネル チャネル 27 Data Data Data func Run(upstream <-chan *entity.Request, downStream chan<- *entity.Request) { for i := 0; i < 64; i++ { go func() { for { // チャネルから受け取り request, ok := <-upstream if !ok { return } // 処理 fmt.Print(request) // 次のチャネルへ流す downStream <- request } }() } }
  13. © GO Inc. 28 チャネルの実行イメージ Go ルーチン Go ルーチン Go

    ルーチン Go ルーチン データ受信処理 デコード・整形処理 Go ルーチン Go ルーチン DB書込処理 チャネルA チャネルB Data Data Data 1. データ送信側は チャネルにデータを入れる。 入れたら即座に、 次のデータを処理する。 Data 2. 次の処理の各GoルーチンはチャネルAから データを1つ取り出して処理する。 チャネルAが空なければ待機する。 3. 処理を終えると、次のチャネルBにデータを渡し、 再びチャネルAからデータを取り出す。 Data 4. また、処理の負荷によって、 以下を調整する ・チャネルに入るデータの数 ・Goルーチンの数 Data
  14. © GO Inc. • どこかが詰まっていて、期待するスループットが出ていないことがある。 • 一定時間毎に、チャネルの内容量をログ出力しておく。 • MAXサイズのチャネルがあると、その次の処理で詰まっていることがわかる。 ◦

    対処例1: 詰まっている処理のGoルーチンの数を更に増やす。 通信処理の場合、相手先マイクロサービスで詰まってないか確認する。 ◦ 対処例2: CPUが100%ならば、プロセスの限界のため、 プロセスのスケールアウト、スケールアップを検討する。 Goルーチンと、チャネルでのトラブル Go ルーチン Go ルーチン Go ルーチン Go ルーチン Go ルーチン データ受信処理 デコード・整形処理 Go ルーチン Go ルーチン DB書込処理 チャネル チャネル 29 現在の内容量 63/64 現在の内容量 1/64
  15. © GO Inc. • 「Goルーチン」「チャネル」という、 軽量スレッドと、軽量スレッド間通信バッファが 構文 レベルで提供されていて、 すっきり記述できて良い。 •

    経験上、Goルーチンは 1ms 以上の処理なら、1処理で逐次起動してもよい。 (事前に起動しておいて、仕事に応じて割り当てることは実装しなくて良い) • ストリーミング処理にはチャネルを使い、処理を部分的に並列に実行できる。 • チャネルの内容量をログ出力しておくと、ストリーミングで詰まってる処理がわかる。 の並列処理のまとめ 30
  16. © GO Inc. 32 自己紹介 GO株式会社 西川大亮(NISHIKAWA, Daisuke) 2019年 DeNA

    AIシステム部にJOIN 2020年 事業統合によりGOに転籍 お客様探索ナビ(タクシー/ハイヤーの営業を効率化するサービス)の • データサイエンティスト • エンジニア(バックエンド[Python/Go]とWeb[TypeScript]) • サービスマネージャ • 渉外/サポート など色々担当 Pythonはアドホックなデータ分析と定型のデータ加工でよく使います。
  17. © GO Inc. 33 やりたいこと Python編 ちょっとしたデータ分析の並列化 3ヶ月 1週間 特徴量

    ラベル 特徴量 ラベル 特徴量 ラベル 処理は同じだけど集計期間が違う →並列化したい 訓練データ作成の並列化 †ラベル: 『GO』を使う、クーポンを使う、etc... ‡特徴量: デモグラ、利用実績、アプリ起動回数、etc... ▪ 過去3ヶ月のデータから向こう1週間を推論するデータセットを作りたい ▪ 特徴量作成は試行錯誤するのでPythonで楽に作りたい ▪ 過去5年分作ると52x5で260回の繰り返しで遅い ▪ 繰り返しの所をPythonで並列化したい
  18. © GO Inc. ▪ 今回やりたいのは並列処理 すごく端折って書くと: ▪ 並行: 複数処理を1CPUで ▪

    並列: 複数処理を複数CPUで ▪ 並行処理は通信などのIOバウンド な処理で有効 ▪ IOウェイトではCPUを使わないため ▪ 複数のS3ファイルをローカルに落と すタスクとか ▪ 数値計算などのCPUバウンドに並 行処理は向かない ▪ CPUが空かない 34 並列処理≠並行処理 Python編 ちょっとしたデータ分析の並列化 IOバウンドなら並行処理でOK CPUバウンドだと並行処理は効果なし CPU IO download #1 download #2 upload #1 feature engineering #1 feature engineering #2 ※CPUは同時に1つしか  使っていない CPU IO IO IO IO feature engineering #3 ※CPUは同時に1つしか  使えない ❌ こうならずに ⭕こうなる
  19. © GO Inc. 35 Pythonは並列処理が苦手 ▪ Global Interpreter Lock (GIL)

    ▪ プロセス内でバイトコードを実行できるスレッドは1つだけ ▪ CPythonのメモリ管理がスレッドセーフではないため ▪ 初学者が詰まりやすい所をあらかじめ消している(とも言える) Java/Kotlin, Go と Pythonの並 列化アーキテクチャの違い ▪ ではどうする? ▪ プロセスを分ける ▪ 別プロセス≒別アプリ ▪ 他の言語と全く違うアプローチ ▪ 以降では標準モジュールの concurrent.futuresの ProcessPoolExecutorで解説 Python編 ちょっとしたデータ分析の並列化
  20. © GO Inc. 36 並列処理の書き方 ▪ funcが並列化したい関数 ▪ ProcessPoolExecutorのsubmitに関 数と関数に渡す引数を指定する

    ▪ 実行のたびに1,2,3の順番が変わ り、並列実行されている ▪ とっても簡単 Python編 ちょっとしたデータ分析の並列化 from concurrent.futures import ProcessPoolExecutor from random import random from time import sleep def func(key): # keyを条件とする「ちょっとしたデータ分析」処理 sleep(random()) print(key) def do_parallel(): with ProcessPoolExecutor() as executor: for key in [1, 2, 3]: executor.submit(func, key) if __name__ == '__main__': do_parallel() % python3 sample.py 3 2 1 % python3 sample.py 2 1 3 実行結果 sample.py
  21. © GO Inc. 37 プロセスの作られ方に癖がある ▪ Unix系OS ▪ forkシステムコールを呼ぶ(簡単!) ▪

    macOS, Windows ▪ 新規にプロセスを作り(spawn) ▪ Python処理系を起動 ▪ submitを呼んだオブジェクトから到達可 能なオブジェクトをpickle/unpickle†で 転送 Python編 ちょっとしたデータ分析の並列化 (落とし穴)pickle化できないオブジェクトが欠落する! ▪ ファイルオブジェクト ▪ DBコネクション ▪ S3クライアント ▪ lambda関数 (copy on write) †Pythonが標準で提供するシリアライズ機構
  22. © GO Inc. ▪ 対話型インタプリタで動かない ▪ Jupyter(Lab/Notebook)が該当 ▪ pickleはコード実体を保持せず、関数名を保持するため ▪

    コード断片の実行順が不明なので新しいプロセスで再現できない ▪ これは対策がないので諦めて.pyで動くように書き直す ▪ submit時にpickle化できないオブジェクトがいると落ちる ▪ 使っているライブラリ内のオブジェクトで落ちると辛い ▪ 初期化をsubmitの後で行うように変更が必要 ▪ lambdaもsubmitの後で作ればOK ▪ pickle化できるかをチェックするコードがあると良い 38 実装上の落とし穴 Python編 ちょっとしたデータ分析の並列化
  23. © GO Inc. 39 Pythonで並列処理を書く時のTips 1 ▪ submit回数をコア数程度に絞る ▪ pickle化できない都合でsubmitの先で初期化するため

    ▪ 1000のタスクを2並列で行うなら • submitを1000回呼ぶと初期化1000回 • 500のずつに分割してsubmitを2回呼ぶと初期化も2回 ▪ 後段のデータ読み込みもまとめられるとなお良い ▪ 初期化したいオブジェクトのキャッシュもあり ▪ とはいえリソース解放のタイミングが難しい Python編 ちょっとしたデータ分析の並列化 def do_parallel(): with ProcessPoolExecutor() as executor: keys = list(range(1000)) for key in keys: executor.submit(func, key) def do_parallel(): with ProcessPoolExecutor() as executor: keys = list(range(1000)) for key in [keys[:500], keys[500:]]: executor.submit(func, key)
  24. © GO Inc. 40 Pythonで並列処理を書く時のTips 2 ▪ データの読み書きをsubmitの先で行う ▪ pickle

    → プロセス間通信 → unpickleよりも都度読んだ方が速い ▪ キャッシュの効果が大きい ▪ 遅いNWを挟まない想定 ▪ 結果の戻しもそれぞれで書き出した方が速い ▪ データが少ない場合を除く ▪ 一貫性はRDBなど外部サービスで担保 ▪ 分割処理した結果は最後にまとめる必要がある ▪ 項目が安定していてRDBに書き出せればデータ結合は楽 Python編 ちょっとしたデータ分析の並列化
  25. © GO Inc. 41 Pythonでの並列化の設計方針 ▪ CPUバウンドなら並列化が有効 ▪ IOバウンドな処理は並行処理 ▪

    Pythonの並列処理実装が特殊であることを忘れない ▪ 忘れると穴に落ちる ▪ pickle化できないオブジェクトに気を付ける ▪ コネクションなどは使う直前で作るようにする ▪ データの読み書きはサブプロセスで行う ▪ プロセス間通信を減らす(pickle/unpickleが重い) ▪ 外部サービスが楽に使えればなお良い ▪ タスク数を無闇に増やさない ▪ 初期化とデータ読み込みの効率化 ▪ 処理するCPU数ぐらいにタスクをグループにまとめる Python編 ちょっとしたデータ分析の並列化
  26. © GO Inc. Pythonで並列化するメリットが大きいケース ▪ データ分析のコードがPythonである ▪ 並列化するパートを試行錯誤したい ▪ ちょっとしたデータ分析である

    ▪ 手元の端末で実行できるぐらいにシンプル ▪ CPU3桁必要とかメモリ4桁G必要とかではない ▪ バッチ処理であり応答性を求めない 43 おわりに Pythonでの並列処理は、優れた設計のシステムに後から†無理な要求を入れて しまい、色々と穴が開いてしまった事例だと思います。 とはいえちょっとしたデータ分析には便利ですよ! Python編 ちょっとしたデータ分析の並列化 †バージョン 2.6 で追加
  27. © GO Inc. 45 自己紹介 プロフィール写真 正方形にトリミングした写 真を「図形に合わせてトリ ミング」で円形にすると真 円になる

    GO株式会社 エンジニア / Suzuki Fumita 略歴 最初に入った受託系企業で官公庁のR&D案件を何回かやったのち、ス タートアップ業界に。2021年6月にGOに入社。現在はタクシー事業者向け の管理画面開発に注力。1児の父。趣味は船釣り。 @maikii_chan
  28. © GO Inc. プロセス プロセス: コンピュータ上での計算を示す概念。下図のよう な状態遷移をする。 時間 t0 t1

    計算途中状態 実行前状態 実行状態 待機状態 実行状態 実行終了状態 47
  29. © GO Inc. 並行性とは? 時刻tにおいて、ある複数のプロセスが計算途中にあること 時間 t0 t1 並行実行中 実行前状態

    実行状態 待機状態 実行状態 終了状態 実行前状態 実行状態 待機状態 実行状態 終了状態 48
  30. © GO Inc. 並列性とは? 時刻tにおいて、ある複数のプロセスが実行状態にあること 時間 t0 t1 並列実行中 実行前状態

    実行状態 待機状態 実行状態 終了状態 実行前状態 実行状態 待機状態 実行状態 終了状態 t2 t3 並列実行中 49
  31. © GO Inc. データ共有がないパターンの並行処理 use std::thread; use std::time::Duration; fn main()

    { // スレッド1を生成します。このスレッドは 2秒間待ってからメッセージを表示します。 let handle1 = thread::spawn(|| { // spawn()で新しいスレッドを立てる。 thread::sleep(Duration::from_secs(2)); println!("Hello from thread 1"); }); // スレッド2を生成します。このスレッドは 1秒間待ってからメッセージを表示します。 let handle2 = thread::spawn(|| { thread::sleep(Duration::from_secs(1)); println!("Hello from thread 2"); }); // スレッド1が終了するのを待ちます。 handle1.join().unwrap(); // スレッド2が終了するのを待ちます。 handle2.join().unwrap(); // 実行結果 // Hello from thread 2 // Hello from thread 1 } 51 データ共有がない場合は、 スレッドを起動するだけ
  32. © GO Inc. チャネルの使い方 use std::sync::mpsc; use std::thread; fn main()

    { // mpsc::channel()でチャネルを生成します (後述) let (sender, receiver) = mpsc::channel(); thread::spawn(move || { let val = String::from("World!"); // send()でチャネルにデータを送信します sender.send(val).unwrap(); }); // recv()でチャネルからデータを受信します let received = receiver.recv().unwrap(); println!("Hello, {}", received); // Hello, World! } 53
  33. © GO Inc. Mutex, Arcについて std::sync::Mutex - 複数のスレッドからアクセスされると困るデータを保護 するためにロックの仕組みを提供する。 std::sync::Arc

    - 「Atomic Reference Count」の略。 - 複数のスレッド間でデータを共有するための所有権を管 理するための仕組みを提供する。 MutexとArcは一緒に使うことが多い(気がする)。 56
  34. © GO Inc. Mutex, Arcを使ってスレッド安全にreceiverを読む use std::sync::{mpsc, Arc, Mutex}; use

    std::thread; fn main() { let (sender, receiver) = mpsc::channel(); // Mutexを使ってreceiverをラップし、その MutexをArcでラップします。 // これにより、 Mutexを複数のスレッドで共有できるようになります。 let rx = Arc::new(Mutex::new(receiver)); for i in 0..5 { // Arc::cloneを使って、rxの参照カウントを増やします。 // これにより、複数のスレッドで rxを共有できます。 let rx = Arc::clone(&rx); thread::spawn(move || { // ロックを獲得してチャネルからメッセージを受信します。 // ロックはスコープを抜けると自動的に解放されます。 let rx = rx.lock().unwrap(); println!("thread {} received {}", i, rx.recv().unwrap()); }); } for i in 0..5 { // 5つのメッセージを送信します。 // 各スレッドは 1つのメッセージを受信します。 sender.send(i).unwrap(); } } 57 Arc::new(Mutex::new(receiver)) receiver値 排他ロック アトミック参照カウント
  35. © GO Inc. まとめ - プロセスと並行・並列処理の基本を説明した。 - データを共有するときと共有しないときで、Rustの並行 処理の書き方を説明した。 -

    標準のライブラリを単純に利用するだけでは実現できな い、複数スレッドでチャネルを読む方法を説明した。 - Rustで並行処理を扱う際のメリット・デメリット、所感 を発表した。 60
  36. © GO Inc. 62 自己紹介 プロフィール写真 正方形にトリミングした写 真を「図形に合わせてトリ ミング」で円形にすると真 円になる

    GO株式会社  AI技術開発部 データプラットフォームG Yu Tachibana 2015年5月  TomTom Japan入社 2018年3月  DeNAオートモーティブ事業部に入社 2020年4月〜 事業統合によりGOに転籍 サーバーサイド開発、MLops周りの開発が多い(GCP、Python、Go) たまにWebアプリのフロントエンドも 技術書典に5回出典 @z_reactor
  37. © GO Inc. Confidential • 私が業務上で開発した、→ のようなユーザの入力と地図 描画を同時にするようなWeb アプリでは、ますます非同期 処理が必須となる

    ◦ 地図はタイルごとに画像を読 み込む必要があり、非同期で 行われる Webアプリでの非同期処理の例 65
  38. © GO Inc. Confidential • 非同期処理が多いが、ブラウザ上のJSはシングルスレッドでしか動作し ない • シングルスレッドで何も工夫しないと、データ待ちやイベント待ちなど で、画面が固まってしまう

    • 擬似的な非同期処理で解決する ◦ シングルスレッドの環境で、命令の呼び出しのタイミングを工 夫し、無駄な待ち時間などを減らし、非同期処理として動いて いるかのように動作させる Webアプリでの非同期処理について 66
  39. © GO Inc. Confidential console.log("A"); setTimeout(() => { console.log("B"); },

    100); Promise.resolve(C).then((res) => { console.log("D") }); console.log("E"); → 命令がPromiseのコールバック、setTimeout等のasyncコードの場合、 Queueに投入し実行を遅らせることで擬似的非同期処理を実現している どうやって擬似的に非同期を実現しているか →call stack行き →queue行き →queue行き →call stack行き 67 • ブラウザーのJS runtimeに主な構成要素が三つある ◦ queue : 実行する命令を入れるFIFOの構造体 ◦ イベントループ : queueを常に監視し、Call Stackに命令を積む処理 ◦ Call stack : LIFO構造体。上にある命令から順番に実行される Call stack ソース コード イベント ループ queue パーサー& コンパイラー 実行 while (queue.waitForMessage()) { queue.processNextMessage(); }
  40. © GO Inc. Confidential 70 • ユーザーがWebページに求める機能性・利便性の向上に伴い、画面遷移 せずに画面の一部を更新するAjax通信が必要となった。 • Ajax通信の実現には非同期処理が必要であり、様々な非同期ライブラリ

    が登場してきた。 • 非同期ライブラリは、非同期に呼び出す関数をコールバック関数として 受け取っていました。 標準化されていない非同期ライブラリで頑張る時代 $("button").click(function(){ $.get("http://xxxx/example.json", function(data, status){ console.log("Data came with status:", data, status); } ); }); コールバック 関数
  41. © GO Inc. Confidential つらみポイント • コールバック地獄 ◦ コールバックを使った非同期関数を多くchainすると 階層構造が深くなってしまう

    • ライブラリによって書き方や引数の指定の仕方がまちまち ◦ jQueryだと.done() と .fail() ◦ axiosだと.then() と .catch() ◦ 他にも.end()、.await()、.success() などなど 標準化されていない非同期ライブラリで頑張る時代 71
  42. © GO Inc. Confidential コールバック地獄 72 A(Aarg, (Ares) => {

    B(Ares.field, (Bres) => { C(Bres, (Cres) => { D(Cres, (Dres) => { console.log("結果:", Dres); }) }) }) }); • とにかくネストが深い コードの説明 Aの実行が終わったら、 Bを非同期で実行し、終わったらCを 非同期で実行し、終わったらDを非 同期で実行し、終わったら console.logを実行する例
  43. © GO Inc. Confidential Promiseというのは? • 非同期処理が終わった時に結果やエラーを返すためのJSのオブジェクト です • 作成時にコンストラクター(Executor関数)にresolve,

    rejectのパラ メーターを渡す ◦ resolve → 正常時の処理 ◦ reject → エラー処理 • 呼び出し時にthen, catchのinterfaceを呼ぶ ◦ .then() → 正常に戻り値を返した後に行う処理を定義できる ◦ .catch() → エラーを返した時に行う処理を定義できる Promise の登場 73
  44. © GO Inc. Confidential A(Aarg, (Ares) => { B(Ares.field, (Bres)

    => { C(Bres, (Cres) => { D(Cres, (Dres) => { console.log("結果:", Dres); }) }) }) }); Promiseによるコールバック地獄の解消 74 → メソッドチェーンによりネ ストを深くすることなく、 多段の非同期処理が記述でき るようになった A(Aarg) .then((Ares) => B(Ares.field)) .then((Bres) => C(Bres)) .then((Cres) => D(Cres)) .then((Dres) => { console.log("結果:", Dres); }); function A() { return new Promise((resolve, reject) => { const req = fetchData(url); if (req.status == 200) { resolve(req.data); } else { reject(new Error("Oops")); } }); }
  45. © GO Inc. Confidential • 共通のインターフェースに統一した ◦ 正常終了は .then() ◦

    異常終了は .catch() 例: ◦ エラーなどの処理が楽になった Promiseにより書き方がまちまちの解決 75 // callbacksだけで実現しようとするエラー処理 A(Aarg, (err, Aarg) => { if (err) { // エラー処理 } else { B(Aarg.field, (err, Ares) => { if (err) { // 別のエラー処理 } else { // 以下略 } }) } }); // 最後にcatchがあると、upstreamのどのPromiseでエラーが起きても catchできる A(Aarg) .then((Ares) => B(Ares.field)) .then((Bres) => C(Bres)) .then((Cres) => D(Cres)) .then((Dres) => { console.log("結果:", Dres); }); .catch((err) => console.log("Some Error happened", err.message));
  46. © GO Inc. Confidential そして、 • ES6以降、Promise がJSの標準仕様として装備された • Promiseを使っている非同期ライブラリーが増えました

    → より読みやすい、書きやすい、分かりやすいコードに。嬉しい。 Promiseが世の中で増える 76
  47. © GO Inc. Confidential   Promiseを更に分かりやすく・書きやすくした概念 • ES2017から、JSにasync/await構文が導入された • Promiseのインターフェースが表に出なくてよくなった •

    Promiseのsyntactic sugarである(裏側はバリバリPromise) • そして導入後、色んなライブラリーがawaitに対応しました async/awaitの登場 77
  48. © GO Inc. Confidential // async/await を使う関数の定義の仕方 async function fetchData()

    { // 非同期処理その1 const response = await fetch(url);   // 非同期処理その2 const data = await response.json();   // dataを処理する } async/awaitの書きやすさその1:Promiseインターフェースの隠蔽 // Promiseのままで処理すると function fetchData() { fetch(url).then(response => { return response.json(); }).then(data => { // dataを処理する }) } Before (Promiseだけを使った書き方) After (async/awaitを使った書き方) 78 → 見た目が同期処理っぽい書き方だから何が起きているか分かりやすい
  49. © GO Inc. Confidential • 呼び出し側が下に下にまっすぐプログラムを書けるようになりました → try { }

    、catch{ } などを使えば出来る • Promiseの作成、then()や catch()が不要になった async/awaitの書きやすさその2:例外処理 // async/awaitを使う関数のtry/catch処理 async function fetchData() { try { const res = await fetch(url); // dataを処理する } catch (err) { throw new Error("my error") } } 79
  50. © GO Inc. Confidential - Promise.all() が便利→複数のPromiseをチェインしやすい - asyncを使うことで、同期処理っぽく書けるため、コードが読みやすくなります -

    最初からasync/awaitを使ってコードを書いていたから、コールバック地獄や Promiseの辛さを味わうことなく、平穏に暮らせています。先人に感謝♪ 発表者の所感 Promise.all([ getPolygons(), // Promiseを返す getMapTiles(), // Promiseを返す getStations(), // Promiseを返す ]).then((values) => { renderMapTiles(values[1]); processStations(values[2]); processPolygons(values[0]); }) 80
  51. © GO Inc. 83 自己紹介 GO株式会社 IoT開発部 / 狩谷 洋平

    2018年4月 サーバーサイドエンジニアとして入社 2020年1月 車載Androidアプリ開発チームへ異動 現在は車載アプリの自動テストやデリバリー改善に注力
  52. © GO Inc. 84 どうして並行処理が必要なの? • Kotlin といえば Android アプリ

    ◦ 例えば、ボタンをタップするとデバイ スとの接続状況を確認する機能 • GUIアプリケーションではUIス レッドで時間が掛かる処理をする と画面が固まる • 待ち時間に別の処理を実行できる 必要がある => 並行処理が必要 button.onClickListener { showProgress() checkDevices() // <- 時間が掛かる hideProgress() } 画面が固まって タップしても 反応がない!!
  53. © GO Inc. 85 どうやって並行処理をするの? • Coroutineで並行処理を実現する • launch {}

    ブロックで Coroutine を生成し、 Coroutine でブロック 内に記述した処理を順次実行する • 待ちが発生した Coroutine 以外の 処理を実行できる button.onClickListener { showProgress() launch { checkDevices() // <- 時間が掛かる hideProgress() } } 他の処理を実行できるため 画面が固まらずタップすると 反応がある!
  54. © GO Inc. • Coroutine は OS が提供するスレッド(ネイティブスレッド)ではない • Coroutine

    はただの Kotlin におけるオブジェクト ◦ 一般的にグリーンスレッドと呼ばれるもの • 1つのスレッドで複数の Coroutine を切り替えて実行できる 86 Coroutine はグリーンスレッド Coroutine A Coroutine B スレッド
  55. © GO Inc. 87 Coroutine とスレッド Kotlin ランタイムが • 複数の

    Coroutine から実行対象を選んでくれる • 用途別のスレッドのまとまりが用意されていて、複数のスレッドから選び割 り当ててくれる Coroutine Coroutine Coroutine Coroutine スレッド スレッド スレッド UIスレッド I/Oで使うスレッドのまとまり Coroutine Coroutine Kotlin
  56. © GO Inc. 88 Coroutine とスレッド Kotlin ランタイムが • 複数の

    Coroutine から実行対象を選んでくれる • 用途別のスレッドのまとまりが用意されていて、複数のスレッドから選び割 り当ててくれる プログラマーは • 用途別のスレッドのまとまりを指 定する • スレッド1つ1つまでは意識しなく ていい button.onClickListener { showProgress() launch(Dispatchers.IO) { checkDevices() // <- 時間が掛かるので待つ hideProgress() } } 前ページの「I/Oで使う スレッドのまとまり」
  57. © GO Inc. 89 Coroutine の切り替え • Coroutine の数がスレッドより多い場合は、スレッドに対して割り込む必要 がある

    ◦ UIスレッドは1つしかないので割り込めないと並行処理できない • Go言語などでは実行系が強制的に割り込むが Kotlin では中断を明示的に宣 言したタイミングでしか割り込まない Coroutine A Coroutine B スレッド 中断 中断
  58. © GO Inc. 90 中断の目印 suspend • Kotlin の Coroutine

    において中断の目印は suspend というキーワードを付 与したメソッドを呼び出したタイミング suspend fun b() { … }
  59. © GO Inc. 91 中断の目印 suspend • Kotlin の Coroutine

    において中断の目印は suspend というキーワードを付 与したメソッドを呼び出したタイミング • コンパイラは Coroutine で実行し たい処理を中断できる処理に変換 する launch { a() b() c() } fun invoke () { when (this.state) { 0 -> { a() this.state = 1 b() return … } 1 -> { c() } } } 変換イメージ ※あくまでイメージ suspend fun b() { … }
  60. © GO Inc. • Kotlin の Coroutine において中断の目印は suspend というキーワードを付

    与したメソッドを呼び出したタイミング 92 中断の目印 suspend Coroutine A Coroutine B suspend fun を呼んだ →中断 suspend fun を呼んだ →中断 再開 suspend fun b() { … }
  61. © GO Inc. 93 Coroutine は親子関係を持てる • launch { }

    内で launch { } すると Coroutineが親子関係を持つ launch { launch { … } launch { … } } Coroutine Coroutine Coroutine
  62. © GO Inc. 94 Coroutine は親子関係を持てる • launch { }

    内で launch { } すると Coroutineが親子関係を持つ • 実は launch は CoroutineScope の メソッド coroutineScope.launch { launch { … } launch { … } } Coroutine Coroutine Coroutine Coroutine Scope
  63. © GO Inc. 95 Coroutine は親子関係を持てる • launch { }

    内で launch { } すると Coroutineが親子関係を持つ • 実は launch は CoroutineScope の メソッド • launch { } ブロック内では this が 親 Coroutineの CoroutineScope になる ( this. は省略可能) coroutineScope.launch { this.launch { … } this.launch { … } } Coroutine Coroutine Coroutine Coroutine Scope this
  64. © GO Inc. 96 Coroutine を管理する CoroutineScope • CoroutineScope で親子関係を持つ

    複数の Coroutine を管理する Coroutine Coroutine Coroutine Coroutine Scope
  65. © GO Inc. 97 Coroutine を管理する CoroutineScope • CoroutineScope で親子関係を持つ

    複数の Coroutine を管理する • 例えば、どれか1つの Coroutine で例外が発生したら全ての Coroutine がキャンセルされるよ うにする Coroutine Coroutine Coroutine Coroutine Scope 例外 throw
  66. © GO Inc. 98 Coroutine を管理する CoroutineScope • CoroutineScope で親子関係を持つ

    複数の Coroutine を管理する • 例えば、どれか1つの Coroutine で例外が発生したら全ての Coroutine がキャンセルされるよ うにする Coroutine Coroutine Coroutine Coroutine Scope 例外 throw cancel cancel
  67. © GO Inc. 99 Coroutine を管理する CoroutineScope • CoroutineScope で親子関係を持つ

    複数の Coroutine を管理する • 例えば、どれか1つの Coroutine で例外が発生したら全ての Coroutine がキャンセルされるよ うにする • Structured concurrency と呼ぶ Coroutine Coroutine Coroutine Coroutine Scope Coroutine が実行しっぱなし になることを防げる
  68. © GO Inc. 10 0 CoroutineScope の例 • Androidでは標準ライブラリから提 供される

    CoroutineScope がある • 例えば、表示中の画面と紐づく CoroutineScope ◦ 画面表示中はカーナビゲーションを更 新し続ける • 画面が切り替わったら Coroutine をキャンセルしてくれるので、実 行しっぱなしにならない viewLifecycleOwner.lifecycleScope.launch { while (isActive) { updateNavigation() } }
  69. © GO Inc. 10 1 まとめ • Kotlin がスレッドに割り当てる Coroutine

    の切り替えをやってくれる • プログラマーが Coroutine を程よく制御できる機能がある ◦ Coroutine に割り当てて欲しいスレッドのまとまりを指定する ◦ Coroutine を構造化してキャンセルの取り扱い方を決める • 触れなかったけれど async や flow などコードの書きやすさ/読みやすさを上 げるAPIが用意されている • → Android アプリを実装しやすくすることにつながっているなぁと感じる
  70. © GO Inc. 102 ⚫X(Twitter)アカウント 技術全般 @goinc_techtalk AI関連 @goinc_ai_tech ⚫技術書典

    電子版を無料配布中! https://techbookfest.org/organization/4925641218588672 Thank You! We Are Hiring!