並行プログラミング 5: タスクを実行するにはどうすればよいですか?

目次

1. タスクをスレッドで実行する方法

2. エグゼキューターフレームワーク

2.1 - スレッドの実行戦略

2.2 - スレッドプール

2.3 - エグゼキュータのライフサイクル

2.4 - 遅延タスクと定期タスク

3. 利用可能な並列処理を確認する - コード例

3.1 - シングルスレッド I/O 操作

3.2 - Callable および Future を運ぶタスクの結果 (重要)

3.3 - Future を使用してページレンダラーを実装する

3.5 - CompletionService:Executor と BlockingQueue


        ほとんどの同時実行アプリケーションは「タスク実行」を中心に構造化されています。タスクは通常、抽象的で個別の作業単位です。アプリケーションの作業を複数のタスクに分解することにより、プログラムの編成が簡素化され、エラー回復プロセスを最適化するためのトランザクション境界が提供され、同時実行性を向上させるための並列作業構造が提供されます。//目的: ジョブを複数のタスクに分解し、同時に実行する方法 -> タスク境界をクリアする (独立したタスクは同時実行に役立ちます)

1. タスクをスレッドで実行する方法

        シリアル実行:サーバー アプリケーションでは、シリアル処理メカニズムは一般に高いスループットや応答性を実現できません。// 一度に実行できるリクエストは 1 つだけです。メインスレッドはブロックされます

        並列実行:応答性を高めるために複数のスレッドを介して処理します。// メインスレッドをブロックすることなくマルチスレッド実行 (タスクをメインスレッドから分離) -> より高速な応答性とより高いスループット

        スレッドのライフサイクルのオーバーヘッドが非常に高いことに注意してください (Java でスレッドを作成するには、カーネル モードのサポートが必要です)。アクティブなスレッドはシステム リソース、特にメモリ (TCB) を消費します。

        したがって、一定の範囲内ではスレッドを追加するとシステムのスループットが向上しますが、この範囲を超えるとスレッドを追加してもプログラムの実行速度が低下するだけであり、スレッドが作成されすぎるとアプリケーション全体のパフォーマンスが低下します。プログラムがクラッシュします。この危険を回避するには、アプリケーションが作成できるスレッドの数を制限し、制限に達したときにリソースが不足しないようにアプリケーションを徹底的にテストする必要があります//適切な数のスレッドを選択し、スレッド リソースを管理する必要があります (スレッドを無限に作成することは避けてください)

2. エグゼキューターフレームワーク

        Executor はプロデューサー/コンシューマー モデルに基づいており、タスクを送信する操作はプロデューサーに相当し、タスクを実行するスレッドはコンシューマーに相当します。プログラムにプロデューサー/コンシューマー設計を実装したい場合、通常、最も簡単な方法は Executor を使用することです。

        TaskExecutionWebServer では、Executor を使用することで、リクエスト処理タスクの送信が実際のタスクの実行から切り離されます。コードは次のとおりです。 // Executor はJava または自分で実装できるインターフェイスです。

public class TaskExecutionWebServer {
    private static final int NTHREADS = 100;
    private static final Executor exec = Executors.newFixedThreadPool(NTHREADS);

    public static void main(String[] args) throws IOException {
        ServerSocket socket = new ServerSocket(80);
        while (true) {
            final Socket connection = socket.accept();
            //任务
            Runnable task = () -> handleRequest(connection);
            //使用Executor执行任务
            exec.execute(task);
        }
    }

    private static void handleRequest(Socket connection) {
        // request-handling logic here
    }
}

2.1 - スレッドの実行戦略

        タスクの送信を実行から切り離すことにより、特定の種類のタスクの実行ポリシーをそれほど困難なく指定および変更できます。「何を、どこで、いつ、どのように」およびタスク実行のその他の側面は、次のような実行ポリシーで定義されます。

  • タスクはどのスレッドで実行されますか? 
  • タスクはどのような順序で実行されますか (FIFO、LIFO、優先順位)? 
  • 同時に実行できるタスクの数はいくつですか?
  • キュー内で実行を待っているタスクはいくつありますか?
  • 過負荷のためシステムがタスクを拒否する必要がある場合、どのタスクを選択する必要がありますか?また、タスクが拒否されたことをアプリケーションに通知するにはどうすればよいですか?
  • タスクの実行前または後にどのようなアクションを実行する必要がありますか? 

        さまざまな実行ポリシーはリソース管理ツールであり、最適なポリシーは利用可能なコンピューティング リソースとサービス品質の要求によって異なります。同時タスクの数を制限することで、リソースの枯渇によってアプリケーションが失敗したり、希少なリソースの競合によってパフォーマンスに重大な影響が及んだりすることがなくなります。タスクの送信をその実行戦略から切り離すことにより、導入段階で利用可能なハードウェア リソースに最も適した実行戦略を選択することができます。//タスクの実行方法の説明

2.2 - スレッドプール

        スレッド プールとは、同形のワーカー スレッドのグループを管理するリソース プールを指します。スレッド プールはワーク キュー (Work Oueue)と密接に関連しており、実行を待機しているすべてのタスクがワーク キューに保存されます。ワーカー スレッド (Worker Thread)のタスクは単純です。ワーク キューからタスクを取得し、そのタスクを実行した後、スレッド プールに戻って次のタスクを待ちます。//スレッドプール -> ストレージスレッド、ワークキュー -> ストレージタスク

        Java クラス ライブラリは、いくつかの便利なデフォルト設定を備えた柔軟なスレッド プールを提供します。スレッド プールは、Executor の静的ファクトリ メソッドのいずれかを呼び出すことで作成できます。

        newFixedThreadPoolnewFixedThreadPool は固定長のスレッド プールを作成し、スレッド プールの最大数に達するまでタスクが送信されるたびにスレッドを作成します。最大数に達すると、スレッド プールのサイズは変更されなくなります (スレッドが期限切れになった場合)予期しない例外が発生した場合、スレッド プールは新しいスレッドを追加します)。// スレッド数を制限する

        newCachedThreadPoolnewCachedThreadPool は、キャッシュ可能なスレッド プールを作成します。スレッド プールの現在のサイズが処理需要を超える場合、アイドル状態のスレッドはリサイクルされます。需要が増加すると、新しいスレッドを追加できます。スレッド プールのサイズに制限はありません。// スレッド数は無制限

        newSingleThreadExecutornewSingleThreadExecutor は、タスクを実行する単一のワーカー スレッドを作成するシングルスレッド Executor です。このスレッドが異常終了した場合、それを置き換えるために別のスレッドが作成されます。newSingleThreadExecutor は、タスクがキュー内の順序 (FIFO、LIFO 優先順位など) に従ってシリアルに実行されることを保証できます。// タスクを順番に実行します

        newScheduledThreadPoolnewScheduledThreadPool は固定長のスレッド プールを作成し、Timer と同様に遅延またはタイミング方式でタスクを実行します。// スケジュールされたタスクを実行する

2.3 - エグゼキュータのライフサイクル

        実行サービスのライフサイクル問題を解決するために、Executor はExecutorService インターフェイスを拡張し、ライフサイクル管理のためのいくつかのメソッドを追加し、タスク送信のための便利なメソッドもいくつか備えています。

/*
 * 该Executor提供管理线程池终止的方法,以及提供用于跟踪一个或多个异步任务进度的Future的方法。
 */
public interface ExecutorService extends Executor {
    //1-关闭执行器(线程池):不再接收新任务,然后等待正在执行的线程执行完毕
    void shutdown();
    //关闭执行器(线程池):停止所有正在执行的任务,并返回等待执行的任务列表
    List<Runnable> shutdownNow();
    
    //2-判断执行器(线程池)是否关闭
    boolean isShutdown();

    //3-判断执行器(线程池)关闭后所有任务是否已完成
    //注意,除非先调用shutdown/shutdownNow,否则isTerminated永远不会为true。
    boolean isTerminated();

    //4-阻塞当前线程,直到所有任务完成或中断或当前等待超时
    boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;

    //5-执行给定的任务,并返回表示该任务的Future。
    <T> Future<T> submit(Callable<T> task);
    <T> Future<T> submit(Runnable task, T result);
    Future<?> submit(Runnable task);

    //6-执行给定的任务,并在所有任务完成后返回保存其状态和结果的future列表。
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException;
    
    //7-执行给定的任务,如果有成功完成的任务,则返回成功完成的任务的结果
    //未完成的任务将被取消
    <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;
    <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}

        ExecutorService のライフサイクルには、実行中シャットダウン、および終了の3 つの状態があります。ExecutorService は、最初に作成されたときに実行されています。

  • shutdown メソッドは正常なシャットダウン プロセスを実行します。つまり、新しいタスクは受け付けられず、同時に、まだ開始されていないタスクも含め、送信されたタスクが完了するまで待機します。
  • shutdownNow メソッドは、強制的なシャットダウンを実行します。実行中のすべてのタスクのキャンセルを試行し、キュー内のまだ実行を開始していないタスクは開始しません。

        ExecutorService が閉じられた後に送信されたタスクは、「Reiected Execution Handler」によって処理されます。これにより、タスクが破棄されるか、execute メソッドが未チェックの ReiectedExecutionException をスローします。すべてのタスクが完了すると、ExecutorService は終了状態になります。

awaitTermination        を呼び出してExecutorService が終了状態に達するのを待つことも、isTerminated を呼び出してExecutorService が終了したかどうかをポーリングすることもできます。通常、shutdown は awaitTermination を呼び出した直後に呼び出されます。これにより、ExecutorService が同期的にシャットダウンされます//ExecutorService を安全に閉じるために 2 つのメソッド (awaitTermination + shutdown) を組み合わせて使用​​します。

2.4 - 遅延タスクと定期タスク

        Timerクラスは、遅延タスクと定期タスクの管理を担当します。ただし、Timer にはいくつかの欠点があるため、代わりにScheduledThreadPoolExecutor の使用を検討する必要があります。このクラスのオブジェクトは、ScheduledThreadPoolExecutor のコンストラクターまたは newScheduledThreadPool ワーカー メソッドを通じて作成できます。

        Timer クラスの問題:

        (1) すべてのタイミング タスクを実行するときに、タイマーは 1 つのスレッドのみを作成します。タスクの実行に時間がかかりすぎると、他の TimerTask のタイミング精度が損なわれます。//タスクの実行時間の重複によって引き起こされる問題は、マルチスレッドを使用することで回避できます

        (2) タイマー スレッドは例外をキャッチしないため、TimerTask がチェックされていない例外をスローすると、タイマー スレッドは終了します。この場合、タイマーはスレッドの実行を再開せず、タイマー全体がキャンセルされたと誤って認識しますそのため、スケジュールされているがまだ実行されていないTimerTaskは再度実行されず、新たなタスクをスケジュールすることができなくなり、この問題を「スレッドリーク」と呼びます。// 例外を処理できず、例外から回復できません

3. 利用可能な並列処理を確認する - コード例

        //例のメソッドを要約します

3.1 - シングルスレッド I/O 操作

        たとえば、以下のページ レンダラー プログラムでは、プログラム内の画像ダウンロード プロセスのほとんどは I/O 操作が完了するのを待機しており、その間 CPU はほとんど作業を行いません。したがって、このシリアル実行方法では CPU が十分に活用されず、最終ページが表示されるまでにユーザーが過度に長い時間を待たされることになります。

import java.util.*;

/**
 * 使用单线的程渲染器
 */
public abstract class SingleThreadRenderer {

    /**
     * 渲染页面
     */
    void renderPage(CharSequence source) {
        //1-加载文本数据
        renderText(source);
        List<ImageData> imageData = new ArrayList<>();
        //下载多个图片资源
        for (ImageInfo imageInfo : scanForImageInfo(source)) {
            //TODO:图像下载大部分时间都是I/O操作
            imageData.add(imageInfo.downloadImage());
        }
        for (ImageData data : imageData) {
            //2-加载图片数据
            renderImage(data);
        }
    }

    interface ImageData {

    }

    interface ImageInfo {

        ImageData downloadImage();
    }

    abstract void renderText(CharSequence s);

    abstract List<ImageInfo> scanForImageInfo(CharSequence s);

    abstract void renderImage(ImageData i);
}

        問題を複数の独立したタスクに分解して同時実行することで、より高い CPU 使用率と応答感度を得ることができます。

3.2 - Callable および Future を運ぶタスクの結果 (重要)

        Executor フレームワークは、基本的なタスク表現として Runnable を使用します。ただし、Runnable には大きな制限があり、run メソッドはログ ファイルに書き込むか、結果を共有データ構造に入れることで実行結果を保存できますが、値を返したり、チェック例外をスローしたりすることはできませ//Runnable には戻り値がありません

        データベース クエリの実行、ネットワークからのリソースの取得、複雑な関数の計算など、実際には多くのタスクで計算が遅延します。これらのタスクでは、Callable の方がより優れた抽象化です。これは、メインのエントリ ポイント (つまり、呼び出し) が値を返し、場合によっては例外をスローすることを前提としています// Callable によって返される Future オブジェクトは、未実行のタスクをキャンセルできます 

        RunnableCallable はどちらも抽象的なコンピューティング タスクを記述します。これらのタスクは通常、範囲が決まっています。つまり、明確な開始点があり、最終的には終了しますExecutor によって実行されるタスクには、 Create、Submit、Start、およびFinishの 4 つのライフサイクル フェーズがあります一部のタスクは実行に時間がかかる場合があるため、多くの場合、これらのタスクをキャンセルできることが望ましいです。Executor フレームワークでは、送信済みだがまだ開始されていないタスクはキャンセルできますが、すでに開始されているタスクは、割り込みに応答できる場合にのみキャンセルできます。完了したタスクをキャンセルしても効果はありません。//タスクは送信またはキャンセルでき、実行タスクにはライフサイクルがあります。

        Future はタスクのライフサイクルを表し、タスクが完了したかキャンセルされたかを判断したり、タスクの結果を取得したりタスクをキャンセルしたりするための対応するメソッドを提供します。Future 仕様に含まれる暗黙の意味は、ExecutorService のライフ サイクルと同様に、タスクのライフ サイクルは前方にのみ進むことができ、後方には進むことができないということです。タスクが完了すると、そのタスクは永久に「完了」状態のままになります。//タスク実行ライフサイクルの順序を乱すことはできません。指定された順序で実行する必要があります

        getメソッドの動作は、タスクの状態(未開始、実行中、完了)によって異なります。

        タスクが完了した場合、get はすぐに戻るか、Exceptionをスローします。タスクが完了していない場合、get はタスクが完了するまでブロックされます。// get メソッドは結果を取得するか、例外をスローする場合があります。

        タスクが例外をスローした場合、get は例外をExecutionExceptionとしてカプセル化し、それを再スローします。タスクがキャンセルされた場合、get はcancelExceptionをスローしますget がExecutionExceptionをスローする 場合getCause を使用してカプセル化された初期例外を取得できます。

//Future接口
public interface Future<V> {

    //尝试取消执行此任务。
    //如果任务已经完成或取消,或者由于其他原因无法取消,则此方法不起作用(返回false)。
    //    否则,如果在调用cancel时该任务尚未启动,则该任务不应运行。
    //如果任务已经启动,那么mayInterruptIfRunning参数决定是否中断正在执行的任务,
    //    true 进行中断,false允许程序执行完成。
    boolean cancel(boolean mayInterruptIfRunning);

    //如果此任务在正常完成之前被取消,则返回true。
    boolean isCancelled();

    //如果此任务完成,则返回true。
    //完成可能是由于正常终止、异常或取消——在所有这些情况下,此方法都将返回true。
    boolean isDone();

    //等待计算完成,然后检索其执行结果。
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}

Future は、        さまざまな方法でタスクを記述するために作成できますExecutorService のすべての submit メソッドは、Runnable または Callable を Executor に送信するために Future を返し、タスクの実行結果を取得するかタスクをキャンセルするために Future を取得します。

特定の Runnable または Callable のFutureTask を        明示的にインスタンス化することもできますFutureTask はRunnable を実装しているため、実行のために Executor に送信するか、その run メソッドを直接呼び出すことができます。

3.3 - Future を使用してページレンダラーを実装する

        ページ レンダラーでより高い同時実行性を実現するために、レンダリング プロセスはまず 2 つのタスクに分解されます。1 つはすべてのテキストをレンダリングするタスク、もう 1 つはすべてのイメージをダウンロードするタスクです一方のタスクは CPU を集中的に使用し、もう一方のタスクは I/O を集中的に使用するため、このアプローチにより、単一 CPU システムでもパフォーマンスが向上します。//アイデア 1: I/O 集中型タスクと CPU 集中型タスクを分離する

import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/**
 * 使用Future的渲染器
 */
public abstract class FutureRenderer {

    private final ExecutorService executor = Executors.newCachedThreadPool();

    void renderPage(CharSequence source) {
        //1-获取图片路径信息
        final List<ImageInfo> imageInfos = scanForImageInfo(source);
        //2-下载图片任务-> I/O密集型
        Callable<List<ImageData>> task = () -> {
            List<ImageData> result = new ArrayList<>();
            for (ImageInfo imageInfo : imageInfos) {
                //下载图片资源
                result.add(imageInfo.downloadImage());
            }
            return result;
        };
        //3-使用线程池执行下载图片任务
        Future<List<ImageData>> future = executor.submit(task);
        //4-加载文本数据-> CPU密集型
        renderText(source);
        //5-加载图片数据
        try {
            //TODO:同步获取所有结果,线程阻塞
            List<ImageData> imageData = future.get();
            for (ImageData data : imageData) {
                renderImage(data);
            }
        } catch (InterruptedException e) {
            // 重新设置线程的中断标记
            Thread.currentThread().interrupt();
            // 我们不需要这个结果,所以也取消这个任务
            future.cancel(true);
        } catch (ExecutionException e) {
            throw launderThrowable(e.getCause());
        }
    }

    interface ImageData {

    }

    interface ImageInfo {

        ImageData downloadImage();
    }

    abstract void renderText(CharSequence s);

    abstract List<ImageInfo> scanForImageInfo(CharSequence s);

    abstract void renderImage(ImageData i);
}

        FutureRenderer を使用すると、テキストをレンダリングするタスクを画像データをダウンロードするタスクと同時に実行できます。すべての画像がダウンロードされると、ページに表示されます。これにより、ユーザー エクスペリエンスが向上し、ユーザーが結果をより速く確認できるだけでなく、並列処理を効率的に利用できるようになりますが、さらに改善できることがあります。ユーザーは、すべての画像がダウンロードされるのを待つのではなく、ダウンロードされたらすぐに画像が表示されることを望んでいます//すべての結果が出るまで待ってレンダリングするのではなく、結果が出たら一つずつレンダリングする必要がある

3.5 - CompletionService:Executor と BlockingQueue

        一連の計算タスクを Executor に送信し、計算の完了後に結果を取得したい場合は、Future を各タスクに関連付けたままにし、get メソッドを繰り返し使用し、パラメーターのタイムアウトを 0 に指定します。タスクが完了したかどうかをポーリングで判断します。この方法は実行可能ではありますが、多少面倒です。幸いなことに、完了サービス ( CompletionService ) という、より良い方法があります。

        CompletionService は、Executor と BlockingQueue の機能を組み合わせたものです。呼び出し可能なタスクを実行のために送信し、take や Paul などのキュー操作に似たメソッドを使用して完了した結果を取得できます。これらの結果は完了時に Future としてカプセル化されます。ExecutorCompletionService はCmpletionService を実装し、計算部分を Executor に委任します。

        ExecutorCompletionService の実装は非常に簡単です。コンストラクターで BlockingQueue を作成し、計算結果を保存します。計算が完了したら、FutureTask の Done メソッドを呼び出します。タスクを送信する場合、タスクはまず FutureTask のサブクラスである QueueingFuture としてパッケージ化され、次にサブクラスの Done メソッドを書き換えて、結果を BlockingQueue に入れます。

        CompletionService を使用してページ レンダラーを実装します。

import java.util.List;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

/**
 * 使用 CompletionService 实现页面渲染器
 */
public abstract class Renderer {

    private final ExecutorService executor;

    Renderer(ExecutorService executor) {
        this.executor = executor;
    }

    void renderPage(CharSequence source) {
        final List<ImageInfo> info = scanForImageInfo(source);
        //1-使用CompletionService执行任务
        CompletionService<ImageData> completionService = new ExecutorCompletionService<>(executor);
        for (final ImageInfo imageInfo : info) {
            completionService.submit(() -> imageInfo.downloadImage());
        }

        renderText(source);

        try {
            //2-从CompletionService中获取执行任务的结果,遍历次数为提交任务的数量
            for (int t = 0, n = info.size(); t < n; t++) {
                Future<ImageData> f = completionService.take();
                ImageData imageData = f.get();
                renderImage(imageData);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } catch (ExecutionException e) {
            throw launderThrowable(e.getCause());
        }
    }

    interface ImageData {

    }

    interface ImageInfo {

        ImageData downloadImage();
    }

    abstract void renderText(CharSequence s);

    abstract List<ImageInfo> scanForImageInfo(CharSequence s);

    abstract void renderImage(ImageData i);
}

        ページ ダイヤのパフォーマンスは、CompletionService を通じて 2 つの方法で改善できます。つまり、合計実行時間の短縮と応答性の向上です。イメージのダウンロードごとに個別のタスクを作成し、スレッド プールで実行します。これにより、シリアル ダウンロード プロセスが並列プロセスに変換され、すべてのイメージをダウンロードする合計時間が短縮されます。さらに、CompletionService から結果を取得し、各画像がダウンロードされるとすぐに表示されるようにすることで、ユーザーはより動的で応答性の高いユーザー インターフェイスを取得できます。

        ここまでで全文は終わりです。

おすすめ

転載: blog.csdn.net/swadian2008/article/details/132109001