19. CountDownLatchとCyclicBarrier:マルチスレッドのペースを一貫させる方法は?-並行処理ツール

1.調整モデルの概要

照合システムの操作ロジック

  • ユーザーがオンラインモールで注文すると、電子注文が生成され、注文データベースに保存されます。
  • その後、ロジスティクスはユーザーへの配送用の配送注文を生成し、配送注文は配送注文データベースに保存されます。
  • 配達ミスや反復配達を防ぐために、調整システムは異常な注文がないか毎日チェックします。

モデル図は次のとおりで、
ここに画像の説明を挿入
コードの抽象化は次のとおりです。

while(存在未对账订单){
  // 查询未对账订单
  pos = getPOrders();
  // 查询派送单
  dos = getDOrders();
  // 执行对账操作
  diff = check(pos, dos);
  // 差异写入差异库
  save(diff);
} 

2.並列最適化調整システムを使用する

シングルスレッドシステム
ここに画像の説明を挿入
がマルチスレッドに変更された場合、
ここに画像の説明を挿入
次のコードでは、2つのスレッドT1とT2が作成され、2つの操作を実行します。調整されていない注文getPOrders()とクエリ配信の単一getDOrders()を並列で実行します。メインスレッドで調整操作チェック()および差分書き込み保存()を実行します。ただし、メインスレッドは、スレッドT1とT2の実行が2つの操作check()とsave()を実行するのを待つ必要があることに注意してください。この目的のために、T1.join()とT2.join()を呼び出して待機を実現します。 T1スレッドとT2スレッドが終了すると、T1.join()とT2.join()を呼び出すメインスレッドがブロック状態から呼び起こされ、後続のチェック()と保存()実行されます。

while(存在未对账订单){
  // 查询未对账订单
  Thread T1 = new Thread(()->{
    pos = getPOrders();
  });
  T1.start();
  // 查询派送单
  Thread T2 = new Thread(()->{
    dos = getDOrders();
  });
  T2.start();
  // 等待 T1、T2 结束
  T1.join();
  T2.join();
  // 执行对账操作
  diff = check(pos, dos);
  // 差异写入差异库
  save(diff);
} 

3. CountDownLatchを使用してスレッド待機を実現する

上記のコードの欠点:whileループで毎回新しいスレッドが作成され、スレッドの作成は時間のかかる操作です。したがって、リサイクル可能なスレッドを作成するのが最善であり、スレッドプールでこの問題を解決できます。

スレッドプールが最適化された後、最初に固定サイズ2のスレッドプールを作成し、次にそれをwhileループで再利用します。すべてがスムーズに見えますが、1つの問題は未解決のようです。つまり、2つの操作getPOrders()とgetDOrders()が実行されたときにメインスレッドがそれを知る方法です。以前のメインスレッドは、スレッドT1とT2のjoin()メソッドを呼び出すことによってスレッドT1とT2が終了するのを待っていましたが、スレッドプールスキームでは、スレッドはまったく終了しないため、join()メソッドは無効になりました。

// 创建 2 个线程的线程池
Executor executor =  Executors.newFixedThreadPool(2);
while(存在未对账订单){
  // 查询未对账订单
  executor.execute(()-> {
    pos = getPOrders();
  });
  // 查询派送单
  executor.execute(()-> {
    dos = getDOrders();
  });
  
  /* ??如何实现等待??*/
  
  // 执行对账操作
  diff = check(pos, dos);
  // 差异写入差异库
  save(diff);
}   

解決策は、カウンターを設定することです。電卓が0の場合、スレッドの実行が完了したことを意味します。

// 创建 2 个线程的线程池
Executor executor = Executors.newFixedThreadPool(2);
while(存在未对账订单){
  // 计数器初始化为 2
  CountDownLatch latch = new CountDownLatch(2);
  // 查询未对账订单
  executor.execute(()-> {
    pos = getPOrders();
    latch.countDown();// 计数器减1
  });
  // 查询派送单
  executor.execute(()-> {
    dos = getDOrders();
    latch.countDown(); // 计数器减1
  });
  
  // 等待两个查询操作结束
  latch.await(); // 等待计数器为0
  
  // 执行对账操作
  diff = check(pos, dos);
  // 差异写入差异库
  save(diff);
}

4.パフォーマンスをさらに最適化する

2つのクエリ操作getPOrders()とgetDOrders()を使用して、チェック()操作と保存()操作を並行して調整することもできます。
ここに画像の説明を挿入

プロデューサー-コンシューマー
2つのクエリ操作はプロデューサーであり、調整操作はコンシューマーです。これはプロデューサー/コンシューマーモデルであるため、プロデューサーによって生成されたデータを保持するキューが必要であり、コンシューマーはこのキューからデータを消費します。

注文クエリ操作は注文クエリ結果を注文キューに挿入し、ディスパッチ注文クエリ操作はディスパッチ注文をディスパッチ注文キューに挿入します。2つのキューの要素は1対1で対応しています。2つのキューの利点は、調整操作で注文キューから1つの要素とディスパッチ単一キューから1つの要素を一度に出力し、これらの2つの要素に対して調整操作を実行できるため、データが混乱しないことです。

ここに画像の説明を挿入

スレッドT1とスレッドT2は、1つのデータの生成が終了したとき、つまり、スレッドT1とスレッドT2が互いに待機し、同じペースである必要がある場合にのみ、同時に実行できます。同時に、スレッドT1とT2の両方が生成を終了したとき同時に、スレッドT3に調整操作を実行するよう通知することもできます。
ここに画像の説明を挿入

5. CyclicBarrierを使用してスレッド同期を実現する

上記の方式の難しさ:1つはスレッドT1とT2が同じペースでなければならないことであり、もう1つはスレッドT3に通知できることです。

// 订单队列
Vector<P> pos;
// 派送单队列
Vector<D> dos;
// 执行回调的线程池 
Executor executor = Executors.newFixedThreadPool(1);
final CyclicBarrier barrier =
  new CyclicBarrier(2, ()->{
    executor.execute(()->check());
  });
  
void check(){
  P p = pos.remove(0);
  D d = dos.remove(0);
  // 执行对账操作
  diff = check(p, d);
  // 差异写入差异库
  save(diff);
}
  
void checkAll(){
  // 循环查询订单库
  Thread T1 = new Thread(()->{
    while(存在未对账订单){
      // 查询订单库
      pos.add(getPOrders());
      // 等待
      barrier.await();
    }
  }
  T1.start();  
  // 循环查询运单库
  Thread T2 = new Thread(()->{
    while(存在未对账订单){
      // 查询运单库
      dos.add(getDOrders());
      // 等待
      barrier.await();
    }
  }
  T2.start();
}

最初に、カウンターの初期値が2のCyclicBarrierが作成されます。CyclicBarrierを作成すると、コールバック関数も渡されます。カウンターが0に減少すると、このコールバック関数が呼び出されます。

  • スレッドT1は注文のクエリを担当し、注文を見つけると、それをバリア.await()を呼び出してカウンターをデクリメントし、カウンターが0になるのを待ちます。
  • スレッドT2は、配信順序のクエリを担当します。スレッドT2が見つかった場合は、バリアーを待機し、バリアーが0になるのを待機しながら、バリアーを1だけ減らします。
  • T1とT2の両方がバリア。待機()を呼び出すと、カウンターは0に減少します。この時点で、T1とT2は次のステートメントを実行し、バリアコールバック関数を呼び出して調整操作を実行できます。
  • CyclicBarrierカウンターには自動リセット機能があり、0に下がると、設定した初期値が自動的にリセットされます。この機能はとても便利です。

6.まとめ

  • CountDownLatchは主に、1つのスレッドが複数のスレッドを待機するシナリオを解決するために使用されます。
  • CyclicBarrierは、互いに待機しているスレッドのグループです。
  • CountDownLatchのカウンターはリサイクルできません。つまり、カウンターが0になり、スレッドがawait()を呼び出すと、スレッドは直接渡されます。
  • CyclicBarrierのカウンターはリサイクル可能で、自動リセット機能を備えており、カウンターが0になると、設定した初期値に自動的にリセットされ、コールバック関数を設定することもできます。
97件の元の記事を公開 賞賛3 10,000+ビュー

おすすめ

転載: blog.csdn.net/qq_39530821/article/details/102653592