並行プログラミングにはさまざまなモデルがあり、それぞれに特有の利点と欠点があります。Asynchronous Programming in Rust 第1章の内容に基づき、OSスレッド、イベント駆動、コルーチン、アクターモデルの各モデルについて例を交えながら説明します。
OSスレッド
OSスレッドは、オペレーティングシステムによって管理されるスレッドを使用する並行プログラミングモデルです。既存の同期コードのプログラミングモデルを大きく変更せず利用できる点で優れており、少数のタスクに対しては簡単に並行処理を表現できます。しかし、スレッド間の同期の難しさやコンテキストスイッチによるパフォーマンスのオーバーヘッドが大きいため、大量のタスクやIOバウンドのワークロードには向いていません。スレッドプールを使用することで一部のコストを軽減できますが、すべての問題を解決するわけではありません。
public class ThreadExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 1; i <= 10; i++) {
System.out.println("Hello from the spawned thread: " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
thread.start();
for (int i = 1; i <= 5; i++) {
System.out.println("Hello from the main thread: " + i);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
try {
thread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
イベント駆動プログラミング
イベント駆動プログラミングは、イベントが発生したときにそれに対応する処理を実行するモデルです。コールバックやイベントハンドラーを使用して、非同期イベントに応答します。
const fs = require('fs');
fs.readFile('example.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});
console.log('This will run before the file is read');
高いパフォーマンスを実現できる一方でコードが非線形になりがちであり、データフローやエラーの伝搬が追跡しにくいです。
コルーチン
コルーチンは、一時停止と再開が可能な関数のようなものです。自然な制御フローを保ちながら並行処理を行うことができます。
import asyncio
async def async_function():
print('Start')
await asyncio.sleep(2)
print('End')
asyncio.run(async_function())
スレッドと同じくプログラミングモデルを変える必要がなく使いやすいですが、低レベルの詳細は抽象化されています。
アクターモデル
アクターモデルは、すべての並行計算をアクターと呼ばれる独立した単位に分割するモデルです。アクターは、メッセージを通じて通信します。
import akka.actor.AbstractActor;
import akka.actor.ActorRef;
import akka.actor.ActorSystem;
import akka.actor.Props;
public class AkkaExample {
// Pingアクター
static class PingActor extends AbstractActor {
private final ActorRef pongActor;
public PingActor(ActorRef pongActor) {
this.pongActor = pongActor;
}
@Override
public Receive createReceive() {
return receiveBuilder()
.matchEquals("ping", msg -> {
System.out.println("Ping received");
pongActor.tell("pong", getSelf());
})
.build();
}
}
// Pongアクター
static class PongActor extends AbstractActor {
@Override
public Receive createReceive() {
return receiveBuilder()
.matchEquals("pong", msg -> {
System.out.println("Pong received");
getSender().tell("ping", getSelf());
})
.build();
}
}
public static void main(String[] args) {
final ActorSystem system = ActorSystem.create("pingpong-system");
final ActorRef pongActor = system.actorOf(Props.create(PongActor.class), "pongActor");
final ActorRef pingActor = system.actorOf(Props.create(PingActor.class, pongActor), "pingActor");
// 初めのメッセージ送信
pingActor.tell("ping", ActorRef.noSender());
// システムを終了させる前に少し待機
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
system.terminate();
}
}
}
アクター間の通信はメッセージを通じて行われるためスレッドセーフな並行処理が実現できますが、フロー制御やリトライロジックなどの実用的な問題を解決する必要があります。
OSスレッドと非同期(Async)の比較
OSスレッドは少数のタスクであれば既存の同期コードをほぼそのまま使える利便性がありますが、CPUとメモリのオーバーヘッドが大きく、大規模なIOバウンドタスクには向きません。
非同期(Async)は軽量で効率的に大量のIOバウンドタスクを処理できますが、非同期用のランタイムがバンドルされるため、バイナリサイズが大きくなる傾向があります。それぞれ向き不向きがあるため、シナリオに応じて適切に選択することが大切です。
まとめ
- OSスレッド: シンプルで使いやすいが、リソースのオーバーヘッドが大きく、スケーラビリティに課題がある。
- イベント駆動: 高性能だが、コードが非線形になりがち。
- コルーチン: 自然な制御フローを保ちながら並行処理を実現可能。
- アクターモデル: スレッドセーフな並行処理が可能だが、制御フローが複雑。
カテゴリー: