2023年8月8日に「GO TechTalk #21 並列処理をGo/Rust/Kotlin/Python/JSで解説!思想の違いを体感しよう」(connpass)を開催しました。
本記事では当日の内容を簡単に紹介します。
GO TechTalkとは?
GO TechTalkは、GO株式会社のエンジニアたちが、タクシーアプリ『GO』をはじめとしたサービスやプロダクトを開発する中で得た技術的ナレッジを共有するイベントです。
GO株式会社には、タクシーアプリ『GO』のユーザアプリや車載アプリ、次世代AIドラレコサービス『DRIVE CHART』、それらを支えるバックエンドAPIやAI基盤など、さまざまなプロダクトがあります。それらはプロダクトの特性に応じて Go、Python、Kotlin、JavaScript、Rust などさまざまな言語で実装しています。
- Go: ほとんどのバックエンドサーバ
- Python: AIモジュールのバックエンドサーバ
- Kotlin: 乗務員、ユーザ向け Android アプリ
- JavaScript: 事業者管理画面などWebUIのフロントエンド
- Rust: AIドラレコなどエッジコンピューティング
今回のイベントでは、テーマを「並列処理および並行処理の手法」として、5つのプログラミング言語に関する解説を行いました。この度は1000人以上からお申し込みいただき、イベント当日には多くの方から感想や質問を受け取りました。当日にお答えすることができなかった質問についても、本記事にて回答しております。参加できなかった方はもちろん、参加された方もぜひご一読ください。
こちらのツイートのスレッドで当日の様子や雰囲気を感じていただけると思います。
GO TechTalk #21 「並列処理をGo/Rust/Kotlin/Python/JSで解説!思想の違いを体感しよう」
— GO Inc. dev | タクシーアプリ『GO』 (@goinc_techtalk) August 8, 2023
はじまります!
ライブ配信:https://t.co/221cAgF0E5
イベント概要:https://t.co/NZuK99FlIf#goinc_tech_talk
登壇者紹介
今回はこちらのメンバーが登壇しました。
- エンジニア:森下 篤(@74th)
- エンジニア:橘 ゆう
- エンジニア:鈴木 文太(@maikii_chan)
- エンジニア:狩谷 洋平
- エンジニア:西川 大亮
- 広報:高堂 和芽(@sandgirl_14)
当日のスライド
技術書典への取り組みの紹介
GO株式会社は技術書典10の回からスポンサーとして協賛しております。さらに技術書典11からは有志によるサークル参加も始め、これまでに4冊の技術同人誌を頒布してきました。
今回のイベントは、技術書典12および13で頒布した技術同人誌をもとに構成しました。該当のPDFは技術書典マーケットプレイスで無料配布中ですので、ぜひご一読ください!
※ 各書籍には頒布した当時の情報が収められています。書籍中の Mobility Technologies は GO株式会社の旧社名です。
またイベント中に「技術書典に出る時このテーマはどうやって決めたんですか?」という質問をいただきました。社内に技術書典企画グループがあり、そこで今回の企画はどうしようかと練っています。今回は、一人の言語比較できたら面白そうというアイディアがきっかけになりました。
(動画 / スライド)
並列処理の基本
それぞれの言語パートに入る前に、前提知識として以下3つについて紹介しました。
- 処理の実行のしかた:「並列処理」と「並行処理」とは
- 処理の連携のしかた:「同期処理」と「非同期処理」とは
- OSでの実行の基本「プロセス」「スレッド」とは
Go編「Go ルーチンで並列処理を実装しよう」
Go編では、タクシーアプリ『GO』のバックエンドで行っている車両のGPS座標データをDBに取り込むストリーミング処理を例に、Goルーチンとチャネルを利用した並列処理の基本的な実装方法を紹介しました。さらに、よく発生するトラブルとして、期待したスループットが得られないチャネル詰まりが起こったときの対処法についても紹介しています。
(動画 / スライド)
Q. Goルーチンの実行が異なるコアに効率よく振り分けられないと、あまり良い並列性は得られない気がしたのですが、ランタイムにそれを伝えるオプションなどあるのでしょうか?(Erlangで似たような経験をしたことがあります)
オプションは特に用意されておらず、Goランタイムに委ねられる言語仕様となっています。経験上、1プロセスに対するCPUの割り当てを増やすよりも、複数のコンテナを立ち上げてプロセス数を増やし、それによってスケールする方法で解決しています。
Q. Goルーチンがスレッド管理よりも軽量な理由がのった書籍等はありますか。TLBがフラッシュされないので軽量等が知りたいです。
書籍『Go言語による並行処理』の第6章に、GoランタイムがGoルーチン実行をどのように行うか動きを扱った章がありました。OSスレッドとの比較はされていませんが、OSスレッドを切り替えることなくスレッドとメモリに載っている呼び出し元の処理にGoランタイムが戻すことができるケースが上げられています。
Q. チャネルで分割した場合、タスクのキャンセルや停止時にチャネルにたまった処理を消費しきるなど実装の工夫が必要に見えますがどの様な工夫をしていますか?
チャネルをクローズすると、データを新たに追加できないが、空になるまで取り出し続けられる機能は提供されているので、それを用いてGraceful Shutdownを実装しています。
Q. 社内では社名のgoと言語名のgoはどうやって区別してるんですか?
社名は「GO Inc.(ゴーインク)」、アプリは「アプリのGO」や「GOアプリ」、言語は「Go言語」や「Golang(ゴーラング)」などと呼ぶようにして区別しています。
Q. OSがやっているというのはカーネルがやっている、で合っていますか?
はい、OSのプロセス、スレッドの説明では、CPUコアに割り当てる処理はOSのカーネルがやっているという意図でお話ししました。
Q. 思想的には、ユーザー側で並列処理のロジックを考えて実装するというより、Goのランタイムに任せるような認識であっていますか?
ひとえに並列処理のロジックと言っても、排他制御とか、チャネルをどう組むか等は考える必要があります。あくまで、並列処理を行うGoルーチンの実行スケジューリングと、同期処理、非同期処理の使い分けをやってくれるという理解が正しいです。常にCPUを使うわけではないI/Oや通信の処理と、CPUを使う処理のGoルーチンを混ぜて実行しても、CPUを効率良くつかって並列処理を実現してくれます。
Python編「ちょっとしたデータ分析の並列化」
PythonにはGIL(Global Interpreter Lock)という機構があり、これによりプロセス内で同時に実行可能なバイトコードのスレッドは1つに制限されるため、並列処理は得意ではありません。このPython編では、ProcessPoolExecutorを使用して複数のプロセスを起動することで並列処理を行う手法や、その際のTips、そして並列化に伴う落とし穴を紹介しました。
(動画 / スライド)
Q. 並列実行苦手なのに Python を利用した方が良いのでしょうか? Pythonにこだわりたいポイントなどが知りたいです。
もともとPythonで直列処理として実装されていた遅い処理を即時的に並列化したい場合、Python内部での並列化が良いと思います。ただ複雑なシチュエーションならPythonの外側で並列化を試みるのが良いと思います。
Q. pickle化したものを渡せないライブラリで有名どころは何がありますか?
典型的なのはDBコネクションだと思います。
Q. 処理内容にもよると思いますが、Spark (pyspark) を使って並列化した方が楽だったりすることはありませんか。
ミドルウェア環境があるのなら、最初からそちらに載せるのもいいと思います。とはいえ「ちょっとしたデータ分析」から使うのは準備が重いかもしれません。
Q. Pythonでは特にLinux以外では特殊な方法でプロセスをスポーンするとの事ですが、プロセスをスポーンするコストは大きいと感じますか?またスポーンするコストで困ったりしたことはありましたか?
プロセス作成のコストは今のCPUの処理速度から見ると結構重いと思います。タスクの処理時間が1秒切るようなケースだと割に合わないので困ると思います。なのでPythonの並列処理は使えるのは1回の処理が数分かかるようなケースからかなと思っています。
Q. プロセスを増やすことで並列性を得るとありましたが、プロセスを新たに作るのはオーバヘッドが大きい印象があります。並列化による恩恵を得るのが難しいと思いましたが、性能を得るための工夫などあれば教えてください。
ProcessPoolExecutorは初期化時に使うプロセスを最初に全部作りその後使い回します。なのでずっとプロセス作成が負担になるわけではないですが、処理量が少ない、全体で数秒で終わるものを早くする目的には向きません。反対に処理量が多く、特に1つの処理で数分以上かかるようなケースで使うのがおすすめです。
Rust編「Rustにおける並列処理」
Rust編では、スレッド間でのデータ共有が不要な場合と、必要な場合、それぞれの並列処理に対して具体的なコードを交えて紹介しました。またMutexとArcを用いてスレッド間のデータ共有を安全に行う方法についてもサンプルコードを交えながら紹介しています。
(動画 / スライド)
Q. Go編と単語としては共通の「チャネル」が出てきましたが、RustのチャネルとGoのチャネルは概念としてはどの程度共通なんでしょうか?(同じ単語だけど全く別物でしょうか?)
概念は同じですが、性質はGoとRustでは異なると思います。
Q. 複数プロセス複数スレッドの場合でも同じように上手いことやってくれますか?
複数スレッド間の通信はコンパイルが通れば上手いことやってくれます。複数プロセスはOS側の話なのでRust単体では安全性は担保できないと思います。
JavaScript編「JSの⾮同期処理のパターン Promise、async/await を理解する」
JavaScript編では、シングルスレッドで動作するJavaScriptの非同期実行の進化を紹介しました。標準化前のコールバック処理を駆使していた時代から、Promise、そしてasync/awaitへと進化していく過程を取り上げています。それぞれの段階でのサンプルコードも示し、コードが段階的にシンプルになっていく様子が視覚的に理解しやすいと思います。
(動画 / スライド)
Q. JavaScript を実行できるブラウザが、スマホ、IE、Chromeなど複数の環境がありますが、全てのブラウザで async/await は使えるのでしょうか。
現行もサポートされているモダンブラウザであれば利用できると思います。
Q. JavaScriptはシングルスレッドしか使えないと、できないことは多いのでしょうか
特に思い至らないのですが、CPUリソースを大量に使うようなユースケースがある場合は、注意点や問題が生じる可能性はあると思います。
Kotlin編「Kotlin の 並⾏処理へのアプローチ」
Kotlin編では、⾮同期処理の基礎となる Coroutine について紹介しました。Coroutine はOSが提供するスレッドではなく、いわゆるグリーンスレッドと呼ばれるランタイムがスケジューリングするスレッドです。この Coroutine がどうやって動いているかコードを交えて紹介しています。
(動画 / スライド)
Q. Coroutine内に書いた処理はいつまで継続されるのでしょうか。アクティビティが更に切り替わったり、アプリが終了されても継続されるのでしょうか。
CoroutineScope を使うことで、Coroutine をアクティビティなどのライフサイクルに紐づけることができるので、画面の切り替わりに合わせて Coroutine をキャンセルできます。
Q. 他の言語では見られなかった構文がいろいろありましたが、これらはAndroidアプリ開発が念頭にあって設計された言語だからでしょうか
KotlinにはGo編で紹介されたチャネルのような仕組みも存在しますが、より簡単に扱えるAPIが提供されています。他にも多くのAPIが提供されており、個人的な印象ですが、Androidアプリの開発を楽にしていくためにいろいろなAPIが追加されているのかなと思います。
アーカイブ動画
開催履歴・開催予定
GO TechTalk は不定期開催しています。過去の開催レポートは こちら にもありますので、ぜひご覧ください!
GO株式会社の最新技術情報は公式Twitterアカウント @goinc_techtalkで随時発信していきますので、ぜひフォローして続報をお待ちください!