天の月

ソフトウェア開発をしていく上での悩み, 考えたこと, 学びを書いてきます(たまに関係ない雑記も)

並列処理をGo/Rust/Kotlin/Python/JSで解説!思想の違いを体感しように参加してきた

jtx.connpass.com

こちらのイベントに参加してきたので、会の様子と感想を書いていこうと思います。

会の概要

以下、イベントページから引用です。

今回のイベントでは、「並列処理、並行処理の手法」というテーマに絞って、Go、Python、Kotlin、Rust、TypeScript の5つの言語でそれぞれ解説します。言語によって、並列・並行処理の仕組みも、考え方も、全然違います。言語毎に実装の仕方を見比べたり、隣の言語がもつ概念を知ることで、よりそれぞれの言語への理解が深まったり、面白がっていただければと思います。

なお、本イベントの内容は、技術書典12と13での出版物を元に構成されています。技術書典での出版物は本ページの下部を御覧ください。

会の様子

技術書典への取り組みの紹介

最初にGo株式会社さんが技術書典に対してどのように関わっているのか?という話がありました。

個人として関わっている方は何人か知っているのですが、会社として関わっているというのはなかなか聞かないので、すごく良い会社だなあと思いながら聞いていました。

並列処理の基本

続いて、並列処理の基本的な部分のお話がありました。以下の3点が話としてありました。

  • 複数の処理を同時に扱う処理(=並行処理)の一種が並列処理である
  • 同じ時間を共有する同期処理に対し、メッセージを「溜めて」処理するのは非同期処理といえる
  • OSで管理されるのがプロセスであり、プロセス内は実行の単位としてスレッドを用意する

Go ルーチンで並列処理を実装しよう

最初にGo言語における並列処理に関して発表がありました。

Goルーチンの基本知識

Goでは、独自のスレッドであるGoルーチンが実装されており、Goルーチン同士の同期/非同期通信の仕組み(=チャネル)が構文に含まれているという点が特徴として説明されました。
Goルーチンはスレッド管理よりも軽量であり、割当のスケジューリングはGoランタイムに任されてという話がありました。
また、Goルーチンは(即時関数であれば)go func(){ }()で実行でき、1ms以上のタスクであれば逐次起動も十分に可能だということです。

Goルーチンのデータ渡し

Goルーチン間のデータ渡しは、チャネルを通して行われますが、これは以下の順序で実行されるというお話でした。

  1. データ送信側がチャネルにデータを入れ、即座に次のデータ処理に移る
  2. GoルーチンはチャネルAからデータを1つ取り出して処理をする。チャネルがなければ待機する
  3. 処理を終えると、次のチャネルにデータを渡し、再びチャネルからデータを取り出す
  4. 処理の負荷に応じてチャネルに入れるデータの数とGoルーチンの数を調整する

チャネルを使用すると、処理づまりが起こることがあるため、一定時間ごとにチャネルの内容をログ出力しておくことがおすすめだそうで、MAXサイズのチャネルがあればその次の処理で詰まっていることが分かるというお話でした。

ちょっとしたデータ分析の並列化・ Python

続いて、Pythonにおける並列処理のお話が「過去3ヶ月のデータから向こう1週間を推論するデータセットを作る」というテーマでありました。

Pythonで並列処理を実装する際のアプローチ

Pythonはプロセス内でバイトコードを実行できるスレッドは1つに定められているため、並列処理は苦手だということで、他言語と違いプロセスを分けるアプローチを取るということです。

具体的な実装と注意点

実装自体は、ProcessPoolExecutorのsubmitに関数と引数を指定するだけなので、処理自体は非常に簡単にかけるそうですが、対話型インタプリタでは動かない点とsubmit時にpickle化できないオブジェクトがいると落ちてしまう点は要注意だというお話がありました。

Pythonで並列処理を書く際のTips

これまでの話を踏まえて、Pythonで並列処理を書く際のTipsの紹介がありました。

  • submit回数をコア数程度に絞る
  • データの読み書きをsubmitの先で行う
  • 一貫性はRDBなど外部サービスで担保

が紹介されていました。

Pythonで並列化する際の設計方針

最後に、Pythonで並列化する際の設計方針について話がありました。以下の点が設計方針として挙げられていました。

  • CPUバウンドなら並列化が有効
  • Pythonの並列処理は実装が特殊であることを忘れないようにする
  • pickle化できないオブジェクトに気をつける
  • データの読み書きはサブプロセスで行う
  • タスク数を無闇に増やさない

Rustにおける並列処理

続いて、Rustにおける並列処理の話がありました。

スレッド間でデータ共有がない場合

データ共有がない場合は、スレッドを起動するだけで良いという話でした。

スレッド間でデータ共有がある場合

データ共有がある場合は、Rustが標準提供しているチャネルを用いてスレッド間通信を行うということで、チャネルはstd::mpsc*1をインポートするというお話でした。

receiverを複数作りたいときは、スレッド安全にするためにMutexとArcを使うのが一般的だということです。

Rustで並行処理を扱うメリット/デメリット

メリットとしては、コンパイルが通った時点で安全性が担保*2されつつ、速度が最高レベルである点が挙げられていました。

デメリットとしては、ArcやMutexなど幅広い知識が実装の際に必要になる点が挙げられていました。(ただし日本語資料も増えているので学習コストは下がりつつある)

JavaScript の ⾮同期処理 Promise、 async/await を理解する

次に、JSの並列処理に関する話がありました。

Webアプリの非同期処理

最初にWebアプリで非同期処理をする際のポイントについて話がありました。

  • 非同期処理が多いが、ブラウザ上のJSはシングルスレッドでしか動作しない
  • シングルスレッドで何も工夫しないとデータ待ちやイベント待ちで画面が固まる
  • 擬似的な非同期処理で上記問題を解決する
JSの非同期処理の歴史

続いて、JSの非同期処理に関する歴史が紹介されました。以下、歴史の変遷を紹介します。

  1. 標準化されていない非同期ライブラリでなんとかする時代(コールバック地獄で階層構造が深い上に、ライブラリによって書き方や引数の指定方法がバラバラ)
  2. Promiseの登場によるコールバック地獄からの脱却
  3. async/awaitの登場により、Promiseが更にわかりやすくなった

並⾏処理・⾮同期処理のアプローチKotlin

最後に、Kotlinにおける並列処理の話がありました。

Kotlinで並行処理が必要な理由

KotlinといえばAndroidですが、Androidアプリでは待ち時間に別の処理を実行する必要があるため、並行処理が必要になってくるということでした。

並行処理の実装方法

Coroutineで並行処理を実現するということでした。(launch{}でCoroutineを生成し、ブロック内に記述した処理を逐次実行)

Coroutineとスレッド

Kotlinランタイムが複数のCoroutineから実行対象を選んでくれ、複数スレッドから用途別スレッドに割当が行われるということです。

そのためプログラマーは用途別のスレッドのまとまりを指定しさえすれば、スレッド1つ1つを意識しなくても良いというお話でした。

また、Coroutineは親子関係を持てるということでした。(launchはCoroutineScopeのメソッドである)

Coroutineの切り替え

Coroutineの数がスレッドより多い場合、スレッドに対して割り込む必要があるというお話がありました。*3

なお、Kotlinでは中断を明示的に宣言したタイミングでしか割り込まないようになっているということです。

suspend

中断はsuspendで行うという話がありました。

この際、コンパイラはCoroutineで実行したい処理を中断できる処理に変換するそうです。

CoroutineScope

CoroutineScopeは親子関係を持つ複数のCoroutineを管理するということで、例えばどれか1つのCoroutineで例外が発生したら全てのCoroutineがキャンセルされるようにするということでした。

会全体を通した感想

知っている言語の話も知らない言語の話もあって、それぞれで違う楽しみ方ができて非常によかったです。

今回は他言語との比較みたいな話は少なかったので、今回紹介がなかった言語の話もどこかで紹介した上で、「どの言語が一番並行処理に適しているか?」みたいなバトル形式(?)で話があったりしても面白そうだなあと思って聞いていました。

*1:sender(producer)を複数個作成できるmulti-producerとreceiver(consumer)は1個しか作成できないsingle-consumerの略

*2:データ融合の問題をコンパイラレベルで弾ける

*3:UIスレッドは1つしかないので割り込まないと並行処理ができない