Slide 1

Slide 1 text

© GO Inc.

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

© GO Inc. 3 GO株式会社について CO2削減・タクシーEV化


Slide 4

Slide 4 text

© 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 終了

Slide 5

Slide 5 text

技術書典への取り組みの紹介 並列処理の基本 Go編「Go ルーチンで並列処理を実装しよう」 Atsushi Morimoto 5

Slide 6

Slide 6 text

© GO Inc. 6 自己紹介 GO株式会社 AI技術開発部 データプラットフォームグループ AI寄りのアーキテクト 『Visual Studio Code実践ガイド』技術評論社 技術書典 3 ~ 14 に新刊参加 @74th Atsushi Morimoto

Slide 7

Slide 7 text

© GO Inc. 本題の前に GO Inc. 技術書典への参加 01 7

Slide 8

Slide 8 text

© GO Inc. ● 年2回ペースで開催されている 技術系同人誌即売会 ● オフラインとオンラインイベントが併設 ● 技術書典10 ~ 14 にて GO Inc. もスポンサー! ● 技術書典11 ~ 14 にて 会社の有志サークルとして参加し 今まで4冊もの書籍を配布。 ※ GO Inc. は技術書典以外にも   Droid Kaigi、iOSDC、Go Conference など   多数協賛しています 技術書典とは 8

Slide 9

Slide 9 text

© 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

Slide 10

Slide 10 text

© GO Inc. ● 電子版無料にて、技術書典マーケットプレイスにて配布中 https://techbookfest.org/organization/4925641218588672 技術書典11 ~ 14 でサークル参加 10

Slide 11

Slide 11 text

© GO Inc. 並列処理の基本 02 11

Slide 12

Slide 12 text

© GO Inc. 12 本題に入る前に以下を説明します。 ● 処理の実行のしかた:「並列処理」と「並行処理」とは ● 処理の連携のしかた:「同期処理」と「非同期処理」とは ● OSでの実行の基本「プロセス」「スレッド」とは はじめに

Slide 13

Slide 13 text

© GO Inc. 13 ● 処理の実行のしかた:「並列処理」と「並行処理」とは ● 処理の連携のしかた:「同期処理」と「非同期処理」とは ● OSでの実行の基本「プロセス」「スレッド」とは 並列処理の基本

Slide 14

Slide 14 text

© GO Inc. 例2: 2つのレーンを用意して 2人が同時に処理する 例1: 2つのタスクのレーンを用意して 時間で切り替えて処理する 14 複数の処理を同時に扱うこと。 並行処理(Concurrent)とは Task1 Task2 Task1 Task2 Task1 Task2 Task1 Task2 2つの仕事がある時 シゴトシテ

Slide 15

Slide 15 text

© GO Inc. 15 複数の処理を扱えず、前の処理が終わってからしか、次の処理を与えられない。 間髪入れず、次の処理を与えないと、何もしない時間(アイドル時間)ができる。 並行処理(Concurrent)ではない タスク1 シゴトシテ オケ Task1 タスク2モ アルヨ ムリ ソロソロ ヤッテ オケ ヒマ Task2 タスク3モ アルヨ ムリ アイドル時間

Slide 16

Slide 16 text

© GO Inc. 並行処理 例2: 2つのレーンを用意して2人が同時に処理する 16 複数の処理を「同時に実行」すること。並行処理の方法の1つ。 より、適切にタスクを与えないと、何もしない時間ができる。 並列処理(Parallel)とは Task1 Task2 アイドル時間

Slide 17

Slide 17 text

© GO Inc. 17 ● 処理の実行のしかた:「並列処理」と「並行処理」とは ● 処理の連携のしかた:「同期処理」と「非同期処理」とは ● OSでの実行の基本「プロセス」「スレッド」とは 並列処理の基本

Slide 18

Slide 18 text

© GO Inc. 18 同じ時間を共有して、メッセージをやりとりする方法。片方が処理する間に待機が発生する。 同期処理によるメッセージ Work1 Work1 シゴトシテ オワッタ ヒマ Work2 シゴトシテ オワッタ Work3 ヒマ ヒマ ヒマ ヒマ Work2

Slide 19

Slide 19 text

© GO Inc. 19 メッセージを「溜めて」、処理する方式。 非同期処理によるメッセージ Work1 Work1 シゴトシテ Work2 オボエトク ヒマ Work2 マダアッタ Work3 Work4 ・Work2 ・Work3 ・Work4 Work5 ・Work4 ・Work5 3ツマデネ イケル? Work3

Slide 20

Slide 20 text

© GO Inc. 20 ● 処理の実行のしかた:「並列処理」と「並行処理」とは ● 処理の連携のしかた:「同期処理」と「非同期処理」とは ● OSでの実行の基本「プロセス」「スレッド」とは 並列処理の基本

Slide 21

Slide 21 text

© GO Inc. Program Program メモリ 21 プログラムを起動すると、メモリ上に展開され、プロセスとしてOSで管理される。 プロセス内は実行の単位としてスレッドを用意し、OSはスレッドごとにCPUに割り当てる。 プロセスとスレッド プロセス1 スレッド1A スレッド1B プロセス2 スレッド2A スレッド2B 起動 プロセス3 スレッド3A スレッド3B CPU コア1 コア2 スレッド3A スレッド2B 起動 起動 OS 時間で 割り当て

Slide 22

Slide 22 text

© GO Inc. 22 ● 複数の処理の扱い方 ○ not並行処理 :終わってからでないと、次の処理を渡せない。 ○ 並行処理 :複数の処理を同時に「扱う」こと。 ○ 並列処理 :複数の処理を同時に「処理する」こと。 ● 処理間の連携の仕方 ○ 同期処理 :同じ時間を共有して、メッセージをやりとりする方法。 ○ 非同期処理 :メッセージを「溜めて」、処理する方式。 ● プロセスとスレッド ○ プログラムは、起動すると「プロセス」としてメモリに展開される。 ○ プロセス内の処理は、複数の「スレッド」を立てることができる。 ○ OSは、CPUの各コアにスレッドを割り当てて、実行される。 まとめ

Slide 23

Slide 23 text

© GO Inc. (lang) ※ 以後、会社をGO、言語を Go または と表記します 03 23

Slide 24

Slide 24 text

© GO Inc. ● 独自の軽量スレッド「Goルーチン」と、 Goルーチン同士の同期・非同期通信の仕組み「チャネル」が、 構文 に含まれている(後ほど解説)。 ● GoランタイムがOSのスレッドを隠蔽し、Goルーチンに時分割に割り当ててうごかす。 割り当てのスケジューリングはGoランタイムに任されていて、指示できない。 ○ つまり、Goルーチン = 並列処理 ではない ● Goルーチンは、スレッド管理よりも軽量なため、使い捨てがかなりできる。 での並列・並行処理 OSスレッド OSスレッド OSスレッド OSプロセス Go ルーチン Go ルーチン Go ルーチン Go ルーチン Go ルーチン Go ルーチン 実装者に見えている世界 Goランタイムの仕事 24

Slide 25

Slide 25 text

© 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ルーチンを起動

Slide 26

Slide 26 text

© GO Inc. 26 ストリーミング処理にGoルーチンを使う ● 処理を段階に分けて、分割する。 ● より高負荷な処理(デコード・整形処理)で、Goルーチンをたくさん起動しておき、 並列数を増やして処理をする。 ● では、Goルーチンの起動時以外に、 このGoルーチン間のデータ渡しはどうすれば良いか?→次スライド Go ルーチン Go ルーチン Go ルーチン Go ルーチン Go ルーチン データ受信処理 デコード・整形処理 Go ルーチン Go ルーチン DB書込処理 Data Data Data Data 例:車両のGPS座標データをDBに取り込む処理

Slide 27

Slide 27 text

© 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 } }() } }

Slide 28

Slide 28 text

© 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

Slide 29

Slide 29 text

© GO Inc. ● どこかが詰まっていて、期待するスループットが出ていないことがある。 ● 一定時間毎に、チャネルの内容量をログ出力しておく。 ● MAXサイズのチャネルがあると、その次の処理で詰まっていることがわかる。 ○ 対処例1: 詰まっている処理のGoルーチンの数を更に増やす。 通信処理の場合、相手先マイクロサービスで詰まってないか確認する。 ○ 対処例2: CPUが100%ならば、プロセスの限界のため、 プロセスのスケールアウト、スケールアップを検討する。 Goルーチンと、チャネルでのトラブル Go ルーチン Go ルーチン Go ルーチン Go ルーチン Go ルーチン データ受信処理 デコード・整形処理 Go ルーチン Go ルーチン DB書込処理 チャネル チャネル 29 現在の内容量 63/64 現在の内容量 1/64

Slide 30

Slide 30 text

© GO Inc. ● 「Goルーチン」「チャネル」という、 軽量スレッドと、軽量スレッド間通信バッファが 構文 レベルで提供されていて、 すっきり記述できて良い。 ● 経験上、Goルーチンは 1ms 以上の処理なら、1処理で逐次起動してもよい。 (事前に起動しておいて、仕事に応じて割り当てることは実装しなくて良い) ● ストリーミング処理にはチャネルを使い、処理を部分的に並列に実行できる。 ● チャネルの内容量をログ出力しておくと、ストリーミングで詰まってる処理がわかる。 の並列処理のまとめ 30

Slide 31

Slide 31 text

Python編 ちょっとしたデータ分析の並列化 NISHIKAWA, Daisuke GO Inc.

Slide 32

Slide 32 text

© GO Inc. 32 自己紹介 GO株式会社 西川大亮(NISHIKAWA, Daisuke) 2019年 DeNA AIシステム部にJOIN 2020年 事業統合によりGOに転籍 お客様探索ナビ(タクシー/ハイヤーの営業を効率化するサービス)の ● データサイエンティスト ● エンジニア(バックエンド[Python/Go]とWeb[TypeScript]) ● サービスマネージャ ● 渉外/サポート など色々担当 Pythonはアドホックなデータ分析と定型のデータ加工でよく使います。

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

© 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つしか  使えない ❌ こうならずに ⭕こうなる

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

© 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

Slide 37

Slide 37 text

© GO Inc. 37 プロセスの作られ方に癖がある ▪ Unix系OS ▪ forkシステムコールを呼ぶ(簡単!) ▪ macOS, Windows ▪ 新規にプロセスを作り(spawn) ▪ Python処理系を起動 ▪ submitを呼んだオブジェクトから到達可 能なオブジェクトをpickle/unpickle†で 転送 Python編 ちょっとしたデータ分析の並列化 (落とし穴)pickle化できないオブジェクトが欠落する! ▪ ファイルオブジェクト ▪ DBコネクション ▪ S3クライアント ▪ lambda関数 (copy on write) †Pythonが標準で提供するシリアライズ機構

Slide 38

Slide 38 text

© GO Inc. ▪ 対話型インタプリタで動かない ▪ Jupyter(Lab/Notebook)が該当 ▪ pickleはコード実体を保持せず、関数名を保持するため ▪ コード断片の実行順が不明なので新しいプロセスで再現できない ▪ これは対策がないので諦めて.pyで動くように書き直す ▪ submit時にpickle化できないオブジェクトがいると落ちる ▪ 使っているライブラリ内のオブジェクトで落ちると辛い ▪ 初期化をsubmitの後で行うように変更が必要 ▪ lambdaもsubmitの後で作ればOK ▪ pickle化できるかをチェックするコードがあると良い 38 実装上の落とし穴 Python編 ちょっとしたデータ分析の並列化

Slide 39

Slide 39 text

© 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)

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

© GO Inc. この資料の元ネタとなった技術書典13レビューにて: 42 おわりに …落とし穴とその回避方法を中心に紹介したので、こんな感想を持たれる方も 多いかと思います。  もちろん、メリットが大きいケースもあります(次スライド) 面白く読めました。データ読み込みとかも個別プロセスで行うならば、複数プロセス 化はPythonの外でしたほうがいいんじゃないか※とか思ってしまいました。 ※強調は登壇者 デスヨネー 😭 Python編 ちょっとしたデータ分析の並列化 監修者

Slide 43

Slide 43 text

© GO Inc. Pythonで並列化するメリットが大きいケース ▪ データ分析のコードがPythonである ▪ 並列化するパートを試行錯誤したい ▪ ちょっとしたデータ分析である ▪ 手元の端末で実行できるぐらいにシンプル ▪ CPU3桁必要とかメモリ4桁G必要とかではない ▪ バッチ処理であり応答性を求めない 43 おわりに Pythonでの並列処理は、優れた設計のシステムに後から†無理な要求を入れて しまい、色々と穴が開いてしまった事例だと思います。 とはいえちょっとしたデータ分析には便利ですよ! Python編 ちょっとしたデータ分析の並列化 †バージョン 2.6 で追加

Slide 44

Slide 44 text

Rustにおける並行処理 Fumita Suzuki 44

Slide 45

Slide 45 text

© GO Inc. 45 自己紹介 プロフィール写真 正方形にトリミングした写 真を「図形に合わせてトリ ミング」で円形にすると真 円になる GO株式会社 エンジニア / Suzuki Fumita 略歴 最初に入った受託系企業で官公庁のR&D案件を何回かやったのち、ス タートアップ業界に。2021年6月にGOに入社。現在はタクシー事業者向け の管理画面開発に注力。1児の父。趣味は船釣り。 @maikii_chan

Slide 46

Slide 46 text

© GO Inc. アジェンダ 1. プロセスと並行・並列処理の基本 2. スレッド間でデータ共有がない場合 3. スレッド間でデータ共有がある場合 4. Rustで並行処理を扱う際のメリット・デメリット 5. 所感 46

Slide 47

Slide 47 text

© GO Inc. プロセス プロセス: コンピュータ上での計算を示す概念。下図のよう な状態遷移をする。 時間 t0 t1 計算途中状態 実行前状態 実行状態 待機状態 実行状態 実行終了状態 47

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

© GO Inc. 並行処理の何が難しいのか 難しい問題の代表としてはプロセス間でのデータ共有。 デッドロックなどが有名。 デッドロック: プロセスやスレッドがお互いのリソースの解 放を待ち続けそれぞれの処理が進行しなくなる状態。 RDBMSなどで見聞きすることが多いかも 50

Slide 51

Slide 51 text

© 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 データ共有がない場合は、 スレッドを起動するだけ

Slide 52

Slide 52 text

© GO Inc. データ共有があるパターンの並行処理 Rustが標準ライブラリで提供しているチャネルを用いてス レッド間の通信を行うことができる。 チャネル: スレッド安全なキューのようなもの。 (スレッド安全: データ競合などの未定義動作を引き起こさないということ) 52

Slide 53

Slide 53 text

© 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

Slide 54

Slide 54 text

© GO Inc. mpscクレートについて Rustではstd::mpscをインポートすることでチャネルを使う ことができる。multi-producer, single-consumerの略。 以下の特徴がある。 multi-producer sender(producer)を複数個作成することが可能。 single-consumer receiver(consumer) は1個しか作成できない。 54

Slide 55

Slide 55 text

© GO Inc. receiverを複数作りたいとき mpscクレートだとreceiverが一つしか作れないため、困る事があ る。  例:Webアプリサーバーでログへの書き込みが間に合わない receiverを複数作りたいときは、receiverをスレッド安全にして複数 のスレッドからデータを読むようにする。 スレッド安全にするために、MutexとArcを使う。 55

Slide 56

Slide 56 text

© GO Inc. Mutex, Arcについて std::sync::Mutex - 複数のスレッドからアクセスされると困るデータを保護 するためにロックの仕組みを提供する。 std::sync::Arc - 「Atomic Reference Count」の略。 - 複数のスレッド間でデータを共有するための所有権を管 理するための仕組みを提供する。 MutexとArcは一緒に使うことが多い(気がする)。 56

Slide 57

Slide 57 text

© 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値 排他ロック アトミック参照カウント

Slide 58

Slide 58 text

© GO Inc. Rustで並行処理を扱う際のメリデメ メリット - コンパイルが通った時点で安全性がかなり担保されてい る - 安全性を確保しつつ、速度も最高レベル(ゼロコスト抽象 化) デメリット - Arc(参照カウント)やMutex(ロック)など、幅広い知識が 必要。 58

Slide 59

Slide 59 text

© GO Inc. 所感 - データ競合の問題をコンパイラレベルで弾けるのが便利 - 他の言語でデータ競合のバグが発生してしまった場合、再現や修 正が大変になりがち。Rustの場合は発生可能性がある時はコンパ イルエラーになるので安心感がある。 - 日本語のわかりやすい資料も増えてきて、学習コストは 下がってきている。 59

Slide 60

Slide 60 text

© GO Inc. まとめ - プロセスと並行・並列処理の基本を説明した。 - データを共有するときと共有しないときで、Rustの並行 処理の書き方を説明した。 - 標準のライブラリを単純に利用するだけでは実現できな い、複数スレッドでチャネルを読む方法を説明した。 - Rustで並行処理を扱う際のメリット・デメリット、所感 を発表した。 60

Slide 61

Slide 61 text

Confidential © GO Inc. JSの非同期処理のパターン Promise、async/awaitを理解する Yu Tachibana 61

Slide 62

Slide 62 text

© 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

Slide 63

Slide 63 text

© GO Inc. Confidential Webアプリでの非同期処理 63

Slide 64

Slide 64 text

© GO Inc. Confidential ● 本発表はブラウザー上で動くJSの話に絞る ● ブラウザ上Webアプリの世界は非同期処理まみれ ○ いつ来るか分からないイベント ○ いつ終わるか分からない通信 Webアプリでの非同期処理について 64

Slide 65

Slide 65 text

© GO Inc. Confidential ● 私が業務上で開発した、→ のようなユーザの入力と地図 描画を同時にするようなWeb アプリでは、ますます非同期 処理が必須となる ○ 地図はタイルごとに画像を読 み込む必要があり、非同期で 行われる Webアプリでの非同期処理の例 65

Slide 66

Slide 66 text

© GO Inc. Confidential ● 非同期処理が多いが、ブラウザ上のJSはシングルスレッドでしか動作し ない ● シングルスレッドで何も工夫しないと、データ待ちやイベント待ちなど で、画面が固まってしまう ● 擬似的な非同期処理で解決する ○ シングルスレッドの環境で、命令の呼び出しのタイミングを工 夫し、無駄な待ち時間などを減らし、非同期処理として動いて いるかのように動作させる Webアプリでの非同期処理について 66

Slide 67

Slide 67 text

© 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(); }

Slide 68

Slide 68 text

© GO Inc. Confidential JS非同期処理の歴史と現在 68

Slide 69

Slide 69 text

© GO Inc. Confidential ● 標準化されていない非同期ライブラリで頑張る時代 ● Promiseの登場 ● async/awaitの登場 JS非同期処理の歴史と現在 69

Slide 70

Slide 70 text

© 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); } ); }); コールバック 関数

Slide 71

Slide 71 text

© GO Inc. Confidential つらみポイント ● コールバック地獄 ○ コールバックを使った非同期関数を多くchainすると 階層構造が深くなってしまう ● ライブラリによって書き方や引数の指定の仕方がまちまち ○ jQueryだと.done() と .fail() ○ axiosだと.then() と .catch() ○ 他にも.end()、.await()、.success() などなど 標準化されていない非同期ライブラリで頑張る時代 71

Slide 72

Slide 72 text

© 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を実行する例

Slide 73

Slide 73 text

© GO Inc. Confidential Promiseというのは? ● 非同期処理が終わった時に結果やエラーを返すためのJSのオブジェクト です ● 作成時にコンストラクター(Executor関数)にresolve, rejectのパラ メーターを渡す ○ resolve → 正常時の処理 ○ reject → エラー処理 ● 呼び出し時にthen, catchのinterfaceを呼ぶ ○ .then() → 正常に戻り値を返した後に行う処理を定義できる ○ .catch() → エラーを返した時に行う処理を定義できる Promise の登場 73

Slide 74

Slide 74 text

© 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")); } }); }

Slide 75

Slide 75 text

© 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));

Slide 76

Slide 76 text

© GO Inc. Confidential そして、 ● ES6以降、Promise がJSの標準仕様として装備された ● Promiseを使っている非同期ライブラリーが増えました → より読みやすい、書きやすい、分かりやすいコードに。嬉しい。 Promiseが世の中で増える 76

Slide 77

Slide 77 text

© GO Inc. Confidential   Promiseを更に分かりやすく・書きやすくした概念 ● ES2017から、JSにasync/await構文が導入された ● Promiseのインターフェースが表に出なくてよくなった ● Promiseのsyntactic sugarである(裏側はバリバリPromise) ● そして導入後、色んなライブラリーがawaitに対応しました async/awaitの登場 77

Slide 78

Slide 78 text

© 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 → 見た目が同期処理っぽい書き方だから何が起きているか分かりやすい

Slide 79

Slide 79 text

© 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

Slide 80

Slide 80 text

© 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

Slide 81

Slide 81 text

© GO Inc. Confidential - 今日の話についてもっと詳しく読みたい場合: 是非 Tech It Up Vol.2を参照 技術書典にもGOで出典しています 81

Slide 82

Slide 82 text

Kotlin の並行処理へのアプローチ Yohei Kariya

Slide 83

Slide 83 text

© GO Inc. 83 自己紹介 GO株式会社 IoT開発部 / 狩谷 洋平 2018年4月 サーバーサイドエンジニアとして入社 2020年1月 車載Androidアプリ開発チームへ異動 現在は車載アプリの自動テストやデリバリー改善に注力

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

© GO Inc. 85 どうやって並行処理をするの? ● Coroutineで並行処理を実現する ● launch {} ブロックで Coroutine を生成し、 Coroutine でブロック 内に記述した処理を順次実行する ● 待ちが発生した Coroutine 以外の 処理を実行できる button.onClickListener { showProgress() launch { checkDevices() // <- 時間が掛かる hideProgress() } } 他の処理を実行できるため 画面が固まらずタップすると 反応がある!

Slide 86

Slide 86 text

© GO Inc. ● Coroutine は OS が提供するスレッド(ネイティブスレッド)ではない ● Coroutine はただの Kotlin におけるオブジェクト ○ 一般的にグリーンスレッドと呼ばれるもの ● 1つのスレッドで複数の Coroutine を切り替えて実行できる 86 Coroutine はグリーンスレッド Coroutine A Coroutine B スレッド

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

© GO Inc. 88 Coroutine とスレッド Kotlin ランタイムが ● 複数の Coroutine から実行対象を選んでくれる ● 用途別のスレッドのまとまりが用意されていて、複数のスレッドから選び割 り当ててくれる プログラマーは ● 用途別のスレッドのまとまりを指 定する ● スレッド1つ1つまでは意識しなく ていい button.onClickListener { showProgress() launch(Dispatchers.IO) { checkDevices() // <- 時間が掛かるので待つ hideProgress() } } 前ページの「I/Oで使う スレッドのまとまり」

Slide 89

Slide 89 text

© GO Inc. 89 Coroutine の切り替え ● Coroutine の数がスレッドより多い場合は、スレッドに対して割り込む必要 がある ○ UIスレッドは1つしかないので割り込めないと並行処理できない ● Go言語などでは実行系が強制的に割り込むが Kotlin では中断を明示的に宣 言したタイミングでしか割り込まない Coroutine A Coroutine B スレッド 中断 中断

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

© 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() { … }

Slide 92

Slide 92 text

© GO Inc. ● Kotlin の Coroutine において中断の目印は suspend というキーワードを付 与したメソッドを呼び出したタイミング 92 中断の目印 suspend Coroutine A Coroutine B suspend fun を呼んだ →中断 suspend fun を呼んだ →中断 再開 suspend fun b() { … }

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

© 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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

© GO Inc. 99 Coroutine を管理する CoroutineScope ● CoroutineScope で親子関係を持つ 複数の Coroutine を管理する ● 例えば、どれか1つの Coroutine で例外が発生したら全ての Coroutine がキャンセルされるよ うにする ● Structured concurrency と呼ぶ Coroutine Coroutine Coroutine Coroutine Scope Coroutine が実行しっぱなし になることを防げる

Slide 100

Slide 100 text

© GO Inc. 10 0 CoroutineScope の例 ● Androidでは標準ライブラリから提 供される CoroutineScope がある ● 例えば、表示中の画面と紐づく CoroutineScope ○ 画面表示中はカーナビゲーションを更 新し続ける ● 画面が切り替わったら Coroutine をキャンセルしてくれるので、実 行しっぱなしにならない viewLifecycleOwner.lifecycleScope.launch { while (isActive) { updateNavigation() } }

Slide 101

Slide 101 text

© GO Inc. 10 1 まとめ ● Kotlin がスレッドに割り当てる Coroutine の切り替えをやってくれる ● プログラマーが Coroutine を程よく制御できる機能がある ○ Coroutine に割り当てて欲しいスレッドのまとまりを指定する ○ Coroutine を構造化してキャンセルの取り扱い方を決める ● 触れなかったけれど async や flow などコードの書きやすさ/読みやすさを上 げるAPIが用意されている ● → Android アプリを実装しやすくすることにつながっているなぁと感じる

Slide 102

Slide 102 text

© GO Inc. 102 ⚫X(Twitter)アカウント 技術全般 @goinc_techtalk AI関連 @goinc_ai_tech ⚫技術書典 電子版を無料配布中! https://techbookfest.org/organization/4925641218588672 Thank You! We Are Hiring!

Slide 103

Slide 103 text

文章・画像等の内容の無断転載及び複製等の行為はご遠慮ください。 © GO Inc. 103