Javaにおけるリーダーフォロワーパターン:動的なワーカー割り当てによる効率向上
リーダー/フォロワーデザインパターンの目的
ワーカースレッドのセットを効率的に管理するために、リーダーフォロワーパターンは、複数のスレッドがイベントソースのセットを順番に共有できるようにすることで、ソースごとに1つのスレッドを使用するアプローチと比較して、リソース使用率を最適化し、パフォーマンスを向上させます。
現実世界の例を用いたリーダー/フォロワーパターンの詳細な説明
現実世界の例
複数のサーバーと1人のホストがいる賑やかなレストランの管理を想像してみてください。ホストは「リーダー」として機能し、ゲストへの挨拶、ウェイティングリストの管理、ゲストの着席を担当します。ゲストが着席すると、ホストは入り口に戻り、新しい到着者を管理します。「フォロワー」であるサーバーは、ホストがテーブルを割り当てるのを待ちます。この割り当ては、次に利用可能なサーバーが次のゲストグループを受け取るというローテーションベースで行われます。このシステムは、ホストがゲストの流入を効率的に処理する一方で、サーバーはサービスの提供に集中できるようにします。これは、リーダーとフォロワーパターンがソフトウェアシステムのスレッドとタスクを管理する方法に似ています。このアプローチは、リソース使用率(この場合はスタッフ)を最適化し、ピーク時にスムーズな運用を保証します。これは、コンピューティング環境でのスレッド使用率の最適化とよく似ています。
簡単な言葉で言うと
リーダーフォロワーデザインパターンは、単一のリーダースレッドを使用して複数のフォロワースレッド間で作業を分散し、タスクの委任、スレッドの同期を効果的に管理し、並行プログラミングにおけるリソース効率を向上させます。
martinfowler.com は次のように述べています。
クラスタ内の1つのサーバーをリーダーとして選択します。リーダーは、クラスタ全体に代わって意思決定を行い、その意思決定を他のすべてのサーバーに伝達する役割を担います。
Javaにおけるリーダーフォロワーパターンのプログラム例
リーダーフォロワーパターンは、1つのスレッド(リーダー)が作業の到着を待ち、逆多重化、ディスパッチ、および処理を行う並行性設計パターンであり、CPUキャッシュアフィニティを高め、イベントディスパッチのレイテンシを削減します。リーダーは作業の処理を完了すると、フォロワースレッドの1つを新しいリーダーに昇格させます。このパターンは、CPUキャッシュアフィニティの向上、ロックオーバーヘッドの最小化、イベントディスパッチレイテンシの削減に役立ちます。
提供されたコードでは、`WorkCenter` クラスが `Worker` スレッドのグループを管理しています。これらのワーカーの1つはリーダーとして指定され、タスクの受信と処理を担当します。タスクが処理されると、リーダーは残りのワーカーから新しいリーダーを昇格させます。
// WorkCenter class
public class WorkCenter {
@Getter
private Worker leader;
private final List<Worker> workers = new CopyOnWriteArrayList<>();
// Method to create workers and set the initial leader
public void createWorkers(int numberOfWorkers, TaskSet taskSet, TaskHandler taskHandler) {
for (var id = 1; id <= numberOfWorkers; id++) {
var worker = new Worker(id, this, taskSet, taskHandler);
workers.add(worker);
}
promoteLeader();
}
// Method to promote a new leader
public void promoteLeader() {
Worker leader = null;
if (!workers.isEmpty()) {
leader = workers.get(0);
}
this.leader = leader;
}
}
`Worker` クラスでは、各ワーカーは処理するタスクを待つスレッドです。ワーカーがリーダーの場合、タスクを処理し、新しいリーダーを昇格させます。
// Worker class
public class Worker implements Runnable {
private final long id;
private final WorkCenter workCenter;
private final TaskSet taskSet;
private final TaskHandler taskHandler;
@Override
public void run() {
while (!Thread.interrupted()) {
try {
if (workCenter.getLeader() != null && !workCenter.getLeader().equals(this)) {
synchronized (workCenter) {
if (workCenter.getLeader() != null && !workCenter.getLeader().equals(this)) {
workCenter.wait();
continue;
}
}
}
final Task task = taskSet.getTask();
synchronized (workCenter) {
workCenter.removeWorker(this);
workCenter.promoteLeader();
workCenter.notifyAll();
}
taskHandler.handleTask(task);
workCenter.addWorker(this);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
}
`App` クラスでは、`WorkCenter` を作成し、`TaskSet` にタスクを追加してから、ワーカーを開始します。リーダワーカーはタスクの処理を開始し、タスクが完了すると新しいリーダーを昇格させます。
// App class
public class App {
public static void main(String[] args) throws InterruptedException {
var taskSet = new TaskSet();
var taskHandler = new TaskHandler();
var workCenter = new WorkCenter();
workCenter.createWorkers(4, taskSet, taskHandler);
addTasks(taskSet);
startWorkers(workCenter);
}
private static void addTasks(TaskSet taskSet) throws InterruptedException {
var rand = new SecureRandom();
for (var i = 0; i < 5; i++) {
var time = Math.abs(rand.nextInt(1000));
taskSet.addTask(new Task(time));
}
}
private static void startWorkers(WorkCenter workCenter) throws InterruptedException {
var workers = workCenter.getWorkers();
var exec = Executors.newFixedThreadPool(workers.size());
workers.forEach(exec::submit);
exec.awaitTermination(2, TimeUnit.SECONDS);
exec.shutdownNow();
}
}
これは、リーダー/フォロワーパターンの基本的な例です。リーダワーカーはタスクを処理し、タスクが完了すると新しいリーダーを昇格させます。新しいリーダーは次のタスクの処理を開始し、サイクルが続きます。
Javaでリーダー/フォロワーパターンを使用する場合
- リーダーフォロワーパターンは、単一スレッドで複数のサービスを効率的に処理し、リソーススラッシングを回避し、並行プログラミング環境でのスケーラビリティを向上させる必要があるシナリオに役立ちます。
- 複数クライアント要求を最小限のリソース消費で同時に処理する必要があるサーバー環境に適用可能です。
Javaにおけるリーダーフォロワーパターンの実際の適用例
- 複数の着信接続を処理するネットワークサーバー。
- 多数の入出力ソースを管理するイベント駆動型アプリケーション。
リーダー/フォロワーパターンの利点とトレードオフ
利点
- スレッド数とコンテキストスイッチングを削減し、パフォーマンスの向上とリソース使用率の低下につながります。
- システムのスケーラビリティと応答性を向上させます。
トレードオフ
- リーダーとフォロワー間の同期の管理が複雑になります。
- 正しく実装されていない場合、リソースが十分に活用されない可能性があります。
関連するJavaデザインパターン
- ハーフ同期/ハーフ非同期:リーダーとフォロワーは、同期がリーダー(同期処理)とフォロワー(非同期で待機)の間で分割されるバリエーションと見なすことができます。
- スレッドプール:どちらのパターンもワーカースレッドのプールを管理しますが、スレッドプールはリーダーを使用して作業を分散するのではなく、利用可能なスレッドにタスクを割り当てます。