C ++スレッドプールThreadPoolExecutorの実装原則

1.スレッドプールを使用する理由

実際の使用では、スレッドはシステムリソースを消費します。スレッドの管理が不十分だと、システムの問題が発生しやすくなります。したがって、スレッドプールは、ほとんどの同時実行フレームワークでスレッドを管理するために使用さますスレッドプールを使用してスレッドを管理する主な利点は次のとおりです。

  1. リソース消費を削減します既存のスレッドを再利用し、スレッドのシャットダウン回数を減らすことで、システムパフォーマンスの低下を可能な限り減らします。
  2. システムの応答速度を向上させますスレッドを再利用することで、スレッドを作成するプロセスが省略され、システム全体の応答速度が向上します。
  3. スレッドの管理性を向上させます。スレッドは希少なリソースです。無制限に作成すると、システムリソースを消費するだけでなく、システムの安定性も低下します。そのため、スレッドプールを使用してスレッドを管理する必要があります。

2.スレッドプールのしくみ

並行タスクがスレッドプールに送信されると、スレッドプールは、次の図に示すように、タスクを実行するためのスレッドを割り当てます。

 

スレッドプール実行フローチャート.jpg

図からわかるように、スレッドプールは送信されたタスクを次の段階で実行します。

  1. まず、スレッドプールコアスレッドプール内のすべてのスレッドがタスクを実行しているかどうかを確認しますそうでない場合は、送信されたばかりのタスクを実行するための新しいスレッドを作成します。そうでない場合は、コアスレッドプール内のすべてのスレッドがタスクを実行しているので、手順2に進みます。
  2. 現在のブロッキングキューがいっぱいかどうかを判断し、いっぱいでない場合は、送信されたタスクをブロッキングキューに配置します。そうでない場合は、手順3に進みます。
  3. スレッドプール内のすべてのスレッドがタスクを実行しているかどうかを確認します。実行していない場合は、タスクを実行するための新しいスレッドを作成します。そうでない場合は、処理のために飽和戦略に引き渡します。

3.スレッドプールの実装

理解していない友人は、これ、スレッドプールの現実のビデオ説明を見ることができます。クリックしてください:150行のコード、手書きのスレッドプール

4.スレッドプールの作成

スレッドプールの作成は、主にThreadPoolExecutorクラスによって行われます。ThreadPoolExecutorには、多くのオーバーロードされた構築メソッドがあります。最も多くのパラメーターを持つ構築メソッドは、スレッドプールを作成するために構成する必要のあるパラメーターを理解するために使用されます。ThreadPoolExecutorの構築方法は次のとおりです。

ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

パラメータについて以下に説明します。

  1. corePoolSize:コアスレッドプールのサイズを示します。タスクを送信するときに、現在のコアスレッドプールのスレッド数がcorePoolSizeに達しない場合、現在のコアスレッドプールにアイドル状態のスレッドがある場合でも、送信されたタスクを実行するための新しいスレッドが作成されます現在のコアスレッドプール内のスレッド数がcorePoolSizeに達した場合、それ以上スレッドは再作成されません。prestartCoreThread()またはprestartAllCoreThreads()が呼び出されると、スレッドプールの作成時にすべてのコアスレッドが作成され、開始されます。
  2. maximumPoolSize:スレッドプールが作成できるスレッドの最大数を示します。ブロッキングキューがいっぱいで、現在のスレッドプール内のスレッド数がmaximumPoolSizeを超えない場合、タスクを実行するために新しいスレッドが作成されます。
  3. keepAliveTime:アイドルスレッドの存続時間。現在のスレッドプール内のスレッド数がcorePoolSizeを超え、スレッドのアイドル時間がkeepAliveTimeを超えると、これらのアイドルスレッドが破棄され、システムリソースの消費を可能な限り削減できます。
  4. 単位:時間の単位。keepAliveTimeの時間単位を指定します。
  5. workQueue:キューをブロックしています。タスクの保存に使用されるブロッキングキュー。ブロッキングキューに関するこの記事を読むことができます。ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueueを使用できます
  6. threadFactory:スレッドを作成するためのエンジニアリングクラス。スレッドファクトリを指定することで、作成されたスレッドごとにわかりやすい名前を設定できます。同時実行の問題がある場合は、問題の原因を見つけることも便利です。
  7. ハンドラー:飽和戦略。スレッドプールのブロッキングキューがいっぱいで、指定されたスレッドが開かれ、現在のスレッドプールがすでに飽和状態にあることを示している場合、この状況に対処するための戦略が必要です。採用されたいくつかの戦略があります:
    1. AbortPolicy:送信されたタスクを直接拒否し、RejectedExecutionException例外をスローします。
    2. CallerRunsPolicy:タスクを実行するために呼び出し元のスレッドのみを使用します。
    3. DiscardPolicy:処理せずにタスクを直接破棄します。
    4. DiscardOldestPolicy:ブロッキングキューに最も長く保存されているタスクを破棄し、現在のタスクを実行します

Linux、Nginx、ZeroMQ、MySQL、 Redis、スレッドプール、MongoDBを含む学習資料、完全なテクノロジースタック、コンテンツ知識を強化するための、知識と学習ネットワークの基礎となる原則のC / C ++ Linuxバックエンド開発についてもっと共有する、ZK、ストリーミングメディア、オーディオとビデオ、Linuxカーネル、CDN、P2P、epoll、Docker、TCP / IP、coroutine、DPDKなど。

スレッドプール実行ロジック

ThreadPoolExecutorを介してスレッドプールが作成された後、タスクが送信された後の実行プロセスです。ソースコードを見てみましょう。executeメソッドのソースコードは次のとおりです。

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    /*
     * Proceed in 3 steps:
     *
     * 1. If fewer than corePoolSize threads are running, try to
     * start a new thread with the given command as its first
     * task.  The call to addWorker atomically checks runState and
     * workerCount, and so prevents false alarms that would add
     * threads when it shouldn't, by returning false.
     *
     * 2. If a task can be successfully queued, then we still need
     * to double-check whether we should have added a thread
     * (because existing ones died since last checking) or that
     * the pool shut down since entry into this method. So we
     * recheck state and if necessary roll back the enqueuing if
     * stopped, or start a new thread if there are none.
     *
     * 3. If we cannot queue task, then we try to add a new
     * thread.  If it fails, we know we are shut down or saturated
     * and so reject the task.
     */
    int c = ctl.get();
	//如果线程池的线程个数少于corePoolSize则创建新线程执行当前任务
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
	//如果线程个数大于corePoolSize或者创建线程失败,则将任务存放在阻塞队列workQueue中
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
	//如果当前任务无法放进阻塞队列中,则创建新的线程来执行任务
    else if (!addWorker(command, false))
        reject(command);
}

ThreadPoolExecutorのexecuteメソッドの実行ロジックについては注意事項を参照してください。次の図は、ThreadPoolExecutorのexecuteメソッドの実行図を示しています。

 

実行実行の概略図process.jpg

実行メソッドの実行ロジックには、いくつかの状況があります。

  1. 現在実行中のスレッドがcorePoolSizeより小さい場合、新しいタスクを実行するために新しいスレッドが作成されます。
  2. 実行中のスレッドの数がcorePoolSize以上の場合、送信されたタスクはブロッキングキューworkQueueに保存されます。
  3. 現在のworkQueueキューがいっぱいになると、タスクを実行するための新しいスレッドが作成されます。
  4. スレッドの数がmaximumPoolSizeを超えた場合、飽和戦略RejectedExecutionHandlerが処理に使用されます。

スレッドプールの設計アイデアは、コアスレッドプールcorePoolSize、ブロッキングキューworkQueue、およびスレッドプールmaximumPoolSizeを使用することであり、タスクを処理するためのキャッシュ戦略などであることに注意してください。実際、この設計アイデアが使用されます。フレームワークで。

5.スレッドプールを閉じる

スレッドプールを閉じるには、shutdownメソッドとshutdownNowメソッドを使用できます。それらの原則は、スレッドプール内のすべてのスレッドをトラバースし、次にスレッドを順番に中断することです。shutdownとshutdownNowにはまだ違いがあります。

  1. shutdownNowは、最初にスレッドプールの状態をSTOPに設定し、次にタスクを実行および非実行しいるすべてのスレッド停止しようとし、実行を待機しているタスクのリストを返します。
  2. Shutdownは、スレッドプールの状態をSHUTDOWN状態に設定するだけで、タスクを実行していないすべてのスレッドを中断します。

shutdownメソッドは実行中のタスクの実行を継続し、shutdownNowは実行中のタスクを直接中断することがわかります。これら2つのメソッドのいずれかが呼び出されると、isShutdownメソッドはtrueを返します。すべてのスレッドが正常にシャットダウンされると、スレッドプールが正常にシャットダウンされたことを意味します。このとき、isTerminatedメソッドはtrueを返します。

5.スレッドプールパラメータを合理的に設定する方法は?

スレッドプールを合理的に構成する場合は、最初にタスクの特性を分析する必要があります。これは、次の観点から分析できます。

  1. タスクの性質:CPUを集中的に使用するタスク、IOを集中的に使用するタスク、および混合タスク。
  2. タスクの優先度:高、中、低。
  3. タスクの実行時間:長、中、短。
  4. タスクの依存関係:データベース接続などの他のシステムリソースに依存しているかどうか。

タスクの性質が異なるタスクは、サイズの異なるスレッドプールによって個別に処理できます。Ncpu + 1スレッドでスレッドプールを構成するなど、CPUを集中的に使用するタスクをできるだけ少ないスレッドで構成します。IOを集中的に使用するタスクは、IO操作を待機する必要があり、スレッドは常にタスクを実行するとは限らないため、2xNcpuなどのできるだけ多くのスレッドを構成します混合タスクの場合、分割できる場合は、CPU集約型タスクとIO集約型タスクに分割します.2つのタスクの実行の時間差が大きすぎない限り、分解後のスループット率はシリアル実行のスループット率により、これら2つのタスクの実行時間が大きく異なる場合は、分解する必要はありません。現在のデバイスのCPU数は、Runtime.getRuntime()。availableProcessors()メソッドを介して取得できます。

優先度の異なるタスクは、優先キューPriorityBlockingQueueを使用して処理できます。これにより、優先度の高いタスクを最初に実行できます。優先度の高いタスクが常にキューに送信されている場合、優先度の低いタスクは実行されない可能性があることに注意してください。

実行時間が異なるタスクを異なるサイズのスレッドプールに渡して処理したり、優先度付きキューを使用して、実行時間が短いタスクを最初に実行したりできます。

SQLを送信した後、スレッドはデータベースが結果を返すのを待つ必要があるため、データベース接続プールのタスクに依存します。待機時間が長くなるほど、CPUアイドル時間が長くなり、スレッドの数を増やす必要があります。 CPUをより有効に活用できるように設定します。

さらに、ブロックキューは制限付きキューを使用するの最適です。制限なしキューを使用すると、タスクのバックログがブロックキューに入ると、メモリリソースを大量に消費し、システムがクラッシュすることさえあります。

おすすめ

転載: blog.csdn.net/Linuxhus/article/details/115030132