web-dev-qa-db-ja.com

スレッドプールでタスクの実行順序を確認する

私はスレッドプールパターンについて読んでいますが、次の問題の通常の解決策を見つけることができないようです。

タスクを連続して実行したいことがあります。たとえば、ファイルからテキストのチャンクを読み取りますが、何らかの理由でその順序でチャンクを処理する必要があります。したがって、基本的には並行性を排除したいです一部のタスク

タスクが*は、プッシュされた順序で処理する必要があります。他のタスクは、任意の順序で処理できます。

Push task1
Push task2
Push task3   *
Push task4   *
Push task5
Push task6   *
....
and so on

この制約がないスレッドプールのコンテキストでは、保留中のタスクの単一のキューは正常に機能しますが、明らかにここでは機能しません。

スレッドの一部がスレッド固有のキューで動作し、他のスレッドが「グローバル」キューで動作することを考えました。次に、いくつかのタスクをシリアルに実行するには、単一のスレッドが見えるキューにそれらをプッシュするだけです。それdoesは少し不器用に聞こえます。

それで、この長い物語の本当の質問:これをどのように解決しますか? これらのタスクが順序付けられていることをどのように確認しますか

編集

より一般的な問題として、上記のシナリオが次のようになると仮定します

Push task1
Push task2   **
Push task3   *
Push task4   *
Push task5
Push task6   *
Push task7   **
Push task8   *
Push task9
....
and so on

つまり、グループ内のタスクは順番に実行する必要がありますが、グループ自体は混在させることができます。したがって、3-2-5-4-7 例えば。

もう1つ注意すべき点は、グループ内のすべてのタスクに事前にアクセスできないことです(グループを開始する前にすべてのタスクが到着するのを待つことはできません)。

お時間をいただきありがとうございます。

48
nc3b

次のようなものにより、シリアルタスクとパラレルタスクをキューに入れることができます。シリアルタスクは次々に実行され、パラレルタスクは任意の順序で実行されますが、パラレルに実行されます。これにより、必要に応じてタスクをシリアル化したり、並列タスクを使用したりすることができますが、タスクを受け取ったときにこれを行います。つまり、シーケンス全体を事前に知る必要はなく、実行順序は動的に維持されます。

internal class TaskQueue
{
    private readonly object _syncObj = new object();
    private readonly Queue<QTask> _tasks = new Queue<QTask>();
    private int _runningTaskCount;

    public void Queue(bool isParallel, Action task)
    {
        lock (_syncObj)
        {
            _tasks.Enqueue(new QTask { IsParallel = isParallel, Task = task });
        }

        ProcessTaskQueue();
    }

    public int Count
    {
        get{lock (_syncObj){return _tasks.Count;}}
    }

    private void ProcessTaskQueue()
    {
        lock (_syncObj)
        {
            if (_runningTaskCount != 0) return;

            while (_tasks.Count > 0 && _tasks.Peek().IsParallel)
            {
                QTask parallelTask = _tasks.Dequeue();

                QueueUserWorkItem(parallelTask);
            }

            if (_tasks.Count > 0 && _runningTaskCount == 0)
            {
                QTask serialTask = _tasks.Dequeue();

                QueueUserWorkItem(serialTask);
            }
        }
    }

    private void QueueUserWorkItem(QTask qTask)
    {
        Action completionTask = () =>
        {
            qTask.Task();

            OnTaskCompleted();
        };

        _runningTaskCount++;

        ThreadPool.QueueUserWorkItem(_ => completionTask());
    }

    private void OnTaskCompleted()
    {
        lock (_syncObj)
        {
            if (--_runningTaskCount == 0)
            {
                ProcessTaskQueue();
            }
        }
    }

    private class QTask
    {
        public Action Task { get; set; }
        public bool IsParallel { get; set; }
    }
}

更新

シリアルタスクとパラレルタスクが混在するタスクグループを処理するために、GroupedTaskQueueは各グループのTaskQueueを管理できます。繰り返しになりますが、グループについて事前に知る必要はありません。グループはすべて、タスクの受信時に動的に管理されます。

internal class GroupedTaskQueue
{
    private readonly object _syncObj = new object();
    private readonly Dictionary<string, TaskQueue> _queues = new Dictionary<string, TaskQueue>();
    private readonly string _defaultGroup = Guid.NewGuid().ToString();

    public void Queue(bool isParallel, Action task)
    {
        Queue(_defaultGroup, isParallel, task);
    }

    public void Queue(string group, bool isParallel, Action task)
    {
        TaskQueue queue;

        lock (_syncObj)
        {
            if (!_queues.TryGetValue(group, out queue))
            {
                queue = new TaskQueue();

                _queues.Add(group, queue);
            }
        }

        Action completionTask = () =>
        {
            task();

            OnTaskCompleted(group, queue);
        };

        queue.Queue(isParallel, completionTask);
    }

    private void OnTaskCompleted(string group, TaskQueue queue)
    {
        lock (_syncObj)
        {
            if (queue.Count == 0)
            {
                _queues.Remove(group);
            }
        }
    }
}
17
Tim Lloyd

スレッドプールは、すべてが完了していれば、タスクの相対的な順序が重要でない場合に適しています。特に、それらがすべて並行して行われても問題はありません。

特定の順序でタスクを実行する必要がある場合、タスクは並列処理に適していないため、スレッドプールは適切ではありません。

これらのシリアルタスクをメインスレッドから移動する場合、タスクキューを備えた単一のバックグラウンドスレッドがそれらのタスクに適しています。並列処理に適した残りのタスクには、引き続きスレッドプールを使用できます。

はい。それは、タスクが順序正しいタスクか「並列化可能」タスクかに応じて、どこにタスクを送信するかを決定する必要があることを意味しますが、これは大したことではありません。

シリアル化する必要があるが、他のタスクと並行して実行できるグループがある場合、複数の選択肢があります。

  1. グループごとに1つのタスクを作成します。これにより、関連するグループタスクが順番に実行され、このタスクがスレッドプールにポストされます。
  2. グループ内の各タスクがグループ内の前のタスクを明示的に待機し、スレッドプールにポストするようにします。これには、スレッドがデッドロックなしでまだスケジュールされていないタスクを待機している場合に、スレッドプールが処理できることが必要です。
  3. 各グループ専用のスレッドを用意し、適切なメッセージキューにグループタスクを投稿します。
14

基本的に、保留中のタスクがいくつかあります。一部のタスクは、1つ以上の他の保留中のタスクの実行が終了したときにのみ実行できます。

保留中のタスクは、依存関係グラフでモデル化できます。

  • 「タスク1->タスク2」は、「タスク2はタスク1が終了した後にのみ実行できる」ことを意味します。矢印は実行順序の方向を指します。
  • タスクの程度(タスクを指しているタスクの数)は、タスクの実行準備ができているかどうかを決定します。 indegreeが0の場合、実行できます。
  • タスクは、複数のタスクが完了するのを待たなければならないことがあります。その場合、次数は> 1です。
  • タスクが他のタスクが終了するのを待つ必要がない場合(indegreeはゼロ)、ワーカースレッドを使用してスレッドプールに送信するか、ワーカースレッドによって取得されるのを待機しているタスクを含むキューに送信できます。タスクは何も待っていないので、送信されたタスクがデッドロックを引き起こさないことを知っています。最適化として、優先度キューを使用できます。依存関係グラフ内のより多くのタスクが依存するタスクが最初に実行されます。また、スレッドプール内のすべてのタスクを実行できるため、デッドロックを引き起こすこともありません。ただし、飢starを引き起こす可能性があります。
  • タスクの実行が終了したら、依存関係グラフから削除して、他のタスクの程度を減らし、作業スレッドのプールに送信できるようにすることができます。

そのため、保留中のタスクの追加/削除に使用される(少なくとも)1つのスレッドがあり、作業スレッドのスレッドプールがあります。

タスクが依存関係グラフに追加されたら、次を確認する必要があります。

  • 依存関係グラフでのタスクの接続方法:どのタスクが完了するまで待機する必要があるか、どのタスクが完了するまで待機する必要があるか?それに応じて、新しいタスクとの接続を作成します。
  • 接続が描画されたら:新しい接続は依存関係グラフにサイクルを引き起こしましたか?その場合、デッドロック状態があります。

パフォーマンス

  • 並列実行が実際にほとんど不可能な場合、このパターンはシーケンシャル実行よりも遅くなります。とにかくほとんどすべてをシーケンシャルに実行するには追加の管理が必要です。
  • 実際に多くのタスクを同時に実行できる場合、このパターンは高速です。

仮定

行間を読んだことがあるかもしれませんが、他のタスクに干渉しないようにタスクを設計する必要があります。また、タスクの優先度を決定する方法が必要です。タスクの優先度には、各タスクで処理されるデータを含める必要があります。 2つのタスクが同じオブジェクトを同時に変更することはできません。代わりに、タスクの1つが他のタスクよりも優先される必要があります。そうでない場合、オブジェクトで実行される操作はスレッドセーフである必要があります。

8
pvoosten

スレッドプールでやりたいことを行うには、何らかのスケジューラーを作成する必要があるかもしれません。

そんな感じ:

TaskQueue->スケジューラー->キュー-> ThreadPool

スケジューラは独自のスレッドで実行され、ジョブ間の依存関係を追跡します。ジョブを実行する準備ができると、スケジューラはスレッドプールのキューにジョブをプッシュするだけです。

ThreadPoolは、ジョブが完了したことを示す信号をスケジューラに送信して、ジョブがそのジョブに依存するジョブをキューに入れるようにしなければならない場合があります。

あなたの場合、依存関係はおそらくリンクリストに保存できます。

次の依存関係があるとしましょう:3-> 4-> 6-> 8

ジョブ3はスレッドプールで実行されていますが、ジョブ8が存在するという考えはまだありません。

ジョブ3は終了します。リンクリストから3を削除し、ジョブ4をスレッドプールのキューに入れます。

ジョブ8が到着します。リンクリストの最後に配置します。

完全に同期する必要がある唯一の構造は、スケジューラーの前後のキューです。

6
Martin

私が問題を正しく理解している場合、jdk executorにはこの機能はありませんが、独自にロールバックするのは簡単です。基本的に必要です

  • それぞれ専用のキューを持つワーカースレッドのプール
  • 仕事を提供するキューの抽象化(c.f. ExecutorService
  • 作業ごとに特定のキューを決定論的に選択するアルゴリズム
  • その後、各作品が適切なキューにオファーを取得するため、適切な順序で処理されます

Jdk executorとの違いは、1つのキューにn個のスレッドがありますが、n個のキューとm個のスレッドが必要なことです(nはmに等しい場合とそうでない場合があります)

*各タスクにキーがあることを読み取った後に編集*

もう少し詳しく

  • キーを特定の範囲(0〜n、nは必要なスレッドの数)のインデックス(int)に変換するコードを記述します。これはkey.hashCode() % nと同じくらい簡単かもしれません。既知のキー値のスレッドへの静的マッピング、または必要なもの
  • 起動時
    • n個のキューを作成し、それらをインデックス付き構造(配列、リストなど)に入れます
    • n個のスレッドを開始します。各スレッドは、キューからブロッキングテイクを行うだけです
    • 何らかの作業を受け取ると、そのタスク/イベントに固有の作業を実行する方法を認識します(異種のイベントがある場合は、明らかにタスクからアクションへのマッピングを行うことができます)
  • 作業項目を受け入れるファサードの後ろにこれを保管します
  • タスクが到着したら、ファサードに渡します
    • ファサードは、キーに基づいてタスクの適切なキューを見つけ、そのキューに提供します

このスキームにワーカースレッドの自動再起動を追加する方が簡単です。その後、ワーカースレッドをマネージャーに登録して、「このキューを所有している」と通知し、その周りのハウスキーピングとスレッドでのエラーの検出(つまり、そのキューの所有権を登録解除し、新しいスレッドを起動するトリガーであるキューの空きプールにキューを返します)

4
Matt

この状況では、スレッドプールを効果的に使用できると思います。アイデアは、依存タスクのグループごとに個別のstrandオブジェクトを使用することです。 strandオブジェクトを使用してまたはなしでキューにタスクを追加します。依存タスクで同じstrandオブジェクトを使用します。スケジューラは、次のタスクにstrandがあるかどうか、およびこのstrandがロックされているかどうかを確認します。そうでない場合-このstrandをロックして、このタスクを実行します。 strandがすでにロックされている場合-次のスケジューリングイベントまでこのタスクをキューに保持します。タスクが完了したら、strandのロックを解除します。

その結果、単一のキューが必要になり、追加のスレッドや複雑なグループなどは必要ありません。strandオブジェクトは、lockunlockの2つのメソッドで非常に簡単になります。

私はしばしば同じ設計上の問題に出会います。複数の同時セッションを処理する非同期ネットワークサーバーの場合。セッション内のタスクが依存している場合(セッション内部タスクをグループ内の依存タスクにマップする)、セッションは独立しています(これにより、独立タスクと依存タスクのグループにマップされます)。説明したアプローチを使用して、セッション内での明示的な同期を完全に回避します。すべてのセッションには、独自のstrandオブジェクトがあります。

さらに、このアイデアの既存の(素晴らしい)実装を使用します: Boost Asio library (C++)。 strandという用語を使用しました。実装はエレガントです。Iwrap非同期タスクをスケジューリングする前に、対応するstrandオブジェクトにラップします。

4
Andriy Tylychko

スレッドプールを使用しないことを示唆する答えは、タスクの依存関係/実行順序の知識をハードコーディングするようなものです。代わりに、2つのタスク間の開始/終了依存関係を管理するCompositeTaskを作成します。タスクインターフェイスの背後にある依存関係をカプセル化することにより、すべてのタスクを均一に処理し、プールに追加できます。これにより、実行の詳細が非表示になり、スレッドプールを使用するかどうかに影響を与えずにタスクの依存関係を変更できます。

質問は言語を指定していません-私はJavaを使用します。

class CompositeTask implements Task
{
    Task firstTask;
    Task secondTask;

    public void run() {
         firstTask.run();
         secondTask.run();
    }
}

これにより、タスクが連続して同じスレッドで実行されます。多数のCompositeTasksを連結して、必要な数のシーケンシャルタスクのシーケンスを作成できます。

ここでの欠点は、すべてのタスクが連続して実行されている間、スレッドが拘束されることです。最初のタスクと2番目のタスクの間に実行したい他のタスクがあるかもしれません。したがって、2番目のタスクを直接実行するのではなく、2番目のタスクの実行を複合タスクでスケジュールします。

class CompositeTask implements Runnable
{
    Task firstTask;
    Task secondTask;
    ExecutorService executor;

    public void run() {
         firstTask.run();
         executor.submit(secondTask);
    }
}

これにより、最初のタスクが完了するまで2番目のタスクが実行されなくなり、プールが他の(おそらくより緊急の)タスクを実行できるようになります。最初のタスクと2番目のタスクは別々のスレッドで実行される可能性があるため、同時に実行されませんが、タスクで使用される共有データは他のスレッドから見えるようにする必要があります(変数volatileを作成するなど)

これはシンプルでありながら強力で柔軟なアプローチであり、タスク自体が異なるスレッドプールを使用して実行制約を定義するのではなく、実行制約を定義できます。

3
mdma

オプション1-複雑なもの

連続したジョブがあるため、これらのジョブをチェーンでまとめて、ジョブが完了したらジョブ自体をスレッドプールに再送信させることができます。ジョブのリストがあるとします:

 [Task1, ..., Task6]

あなたの例のように。 [Task3, Task4, Task6]が依存関係チェーンであるような、順次依存関係があります。ジョブを作成します(Erlang擬似コード):

 Task4Job = fun() ->
               Task4(), % Exec the Task4 job
               Push_job(Task6Job)
            end.
 Task3Job = fun() ->
               Task3(), % Execute the Task3 Job
               Push_job(Task4Job)
            end.
 Push_job(Task3Job).

つまり、Task3ジョブをジョブにラップすることで変更します継続としてキュー内の次のジョブをスレッドプールにプッシュします。 Node.jsやPython Twistedフレームワークなどのシステムでも見られる、一般的な継続渡しスタイルと強い類似点があります。

一般化して、deferのさらなる作業とさらなる作業の再送信が可能なジョブチェーンを定義できるシステムを作成します。

オプション2-シンプルなもの

なぜ私たちは仕事を分割することさえしなければならないのですか?つまり、これらは順番に依存しているため、同じスレッドですべてを実行しても、そのチェーンを取得して複数のスレッドに分散するよりも速くも遅くもなりません。 「十分な」作業負荷を想定すると、どのスレッドも常に作業を行うことができるため、ジョブをまとめるのがおそらく最も簡単です。

  Task = fun() ->
            Task3(),
            Task4(), 
            Task6()  % Just build a new job, executing them in the order desired
         end,
  Push_job(Task).

ファーストクラスの市民としての機能がある場合、このようなことを行うのはかなり簡単です。たとえば、任意の関数型プログラミング言語、Python、Rubyブロックなどでできるように、気まぐれに自分の言語で構築できます。 。

「オプション1」のように、キューや継続スタックを作成するという考えは特に好きではありません。間違いなく2番目のオプションを使用します。 Erlangには、Erlang Solutionsによって作成され、オープンソースとしてリリースされたjobsというプログラムもあります。 jobsは、これらのようなジョブ実行を実行およびロード調整するために構築されています。この問題を解決するのであれば、おそらくオプション2をジョブと組み合わせるでしょう。

3

2つの アクティブオブジェクト を使用します。つまり、アクティブオブジェクトパターンは、優先キューと、キューからタスクを取得して処理できる1つ以上の作業スレッドで構成されます。

したがって、1つの作業スレッドで1つのアクティブなオブジェクトを使用します。キューに配置されるすべてのタスクは、順番に処理されます。作業スレッドの数が1を超える2番目のアクティブオブジェクトを使用します。この場合、作業スレッドは任意の順序でキューからタスクを取得して処理します。

幸運。

3
garik

あなたのシナリオを理解している限り、これは達成可能です。基本的に必要なのは、メインスレッドでタスクを調整するためにスマートなことです。 Java必要なAPIは ExecutorCompletionService および Callable です

まず、呼び出し可能なタスクを実装します。

public interface MyAsyncTask extends Callable<MyAsyncTask> {
  // tells if I am a normal or dependent task
  private boolean isDependent;

  public MyAsyncTask call() {
    // do your job here.
    return this;
  }
}

次に、メインスレッドでCompletionServiceを使用して、依存タスクの実行を調整します(つまり、待機メカニズム)。

ExecutorCompletionService<MyAsyncTask> completionExecutor = new 
  ExecutorCompletionService<MyAsyncTask>(Executors.newFixedThreadPool(5));
Future<MyAsyncTask> dependentFutureTask = null;
for (MyAsyncTask task : tasks) {
  if (task.isNormal()) {
    // if it is a normal task, submit it immediately.
    completionExecutor.submit(task);
  } else {
    if (dependentFutureTask == null) {
      // submit the first dependent task, get a reference 
      // of this dependent task for later use.
      dependentFutureTask = completionExecutor.submit(task);
    } else {
      // wait for last one completed, before submit a new one.
      dependentFutureTask.get();
      dependentFutureTask = completionExecutor.submit(task);
    }
  }
}

これにより、単一のエグゼキュータ(スレッドプールサイズ5)を使用して通常タスクと依存タスクの両方を実行し、通常タスクは送信されるとすぐに実行され、依存タスクは1つずつ実行されます(待機はgetを呼び出すことによりメインスレッドで実行されます) ()新しい依存タスクをサブミットする前のFutureで)、任意の時点で、常に複数の通常タスクと単一のスレッドプールで実行されている単一の依存タスク(存在する場合)があります。

これは単なる出発点であり、ExecutorCompletionService、FutureTask、およびSemaphoreを使用することで、より複雑なスレッド調整シナリオを実装できます。

2
yorkw

コンセプトをミックスしていると思います。スレッド間でいくつかの作業を分散させたい場合は、スレッドプールは問題ありませんが、スレッド間で依存関係を混在させ始めた場合、あまり良い考えではありません。

私のアドバイスは、単純にスレッドプールを使用しないでくださいそれらのタスクのためです。専用のスレッドを作成し、そのスレッドだけで処理する必要のある順次項目の単純なキューを保持するだけです。その後、シーケンシャル要件がない場合はタスクをスレッドプールにプッシュし続け、必要な場合は専用スレッドを使用できます。

明確化:常識を使用して、シリアルタスクのキューは、各タスクを次々に処理する単一のスレッドによって実行されるものとします:)

2
Jorge Córdoba

1つのタスクが完了するのを待ってから依存タスクを開始する必要があるため、最初のタスクで依存タスクをスケジュールできる場合は簡単に実行できます。したがって、2番目の例では、タスク2の最後にタスク7をスケジュールし、タスク3の最後に4-> 6および6-> 8のタスク4などをスケジュールします。

最初は、タスク1、2、5、9 ...をスケジュールするだけで、残りは従う必要があります。

さらに一般的な問題は、依存タスクを開始する前に複数のタスクを待機する必要がある場合です。これを効率的に処理するのは、簡単なことではありません。

1
ritesh

これらのタスクを確実に順序付けるにはどうしますか?

Push task1
Push task2
Push task346
Push task5

編集に応じて:

Push task1
Push task27   **
Push task3468   *
Push task5
Push task9
1
Amy B

2種類のタスクがあります。単一のキューでそれらを混在させることはかなり奇妙に感じます。 1つのキューの代わりに2つあります。簡単にするために、両方にThreadPoolExecutorを使用することもできます。シリアルタスクの場合は、固定サイズ1を指定するだけで、同時に実行できるタスクの場合はさらに多くを指定します。なぜそれが不器用になるのかはわかりません。シンプルで愚かにしてください。 2つの異なるタスクがあるので、それらを適切に処理してください。

1
tcurdt

dexecutor と呼ばれるこの目的専用のJavaフレームワークがあります(免責事項:私は所有者です)

DefaultDependentTasksExecutor<String, String> executor = newTaskExecutor();

    executor.addDependency("task1", "task2");
    executor.addDependency("task4", "task6");
    executor.addDependency("task6", "task8");

    executor.addIndependent("task3");
    executor.addIndependent("task5");
    executor.addIndependent("task7");

    executor.execute(ExecutionBehavior.RETRY_ONCE_TERMINATING);

task1、task3、task5、task7は並行して実行されます(スレッドプールサイズに依存)、task1が完了すると、task2が実行され、task2がtask4の実行を完了すると、task4がtask6の実行を完了し、最後にtask6がtask8の実行を完了します。

0
craftsmannadeem

多くの答えがあり、明らかに受け入れられました。しかし、なぜ継続を使用しないのですか?

既知の「シリアル」条件がある場合、この条件で最初のタスクをキューに入れるとき、タスクを保留します。そして、さらなるタスクのためにTask.ContinueWith()を呼び出します。

public class PoolsTasks
{
    private readonly object syncLock = new object();
    private Task serialTask = Task.CompletedTask;


    private bool isSerialTask(Action task) {
        // However you determine what is serial ...
        return true;
    }

    public void RunMyTask(Action myTask) {
        if (isSerialTask(myTask)) {
            lock (syncLock)
                serialTask = serialTask.ContinueWith(_ => myTask());
        } else
            Task.Run(myTask);
    }
}
0
Steven Coco

順序付きおよび順序なしの実行メソッドを持つスレッドプール:

import Java.util.concurrent.ExecutorService;
import Java.util.concurrent.Executors;

public class OrderedExecutor {
    private ExecutorService multiThreadExecutor;
    // for single Thread Executor
    private ThreadLocal<ExecutorService> threadLocal = new ThreadLocal<>();

    public OrderedExecutor(int nThreads) {
        this.multiThreadExecutor = Executors.newFixedThreadPool(nThreads);
    }

    public void executeUnordered(Runnable task) {
        multiThreadExecutor.submit(task);
    }

    public void executeOrdered(Runnable task) {
        multiThreadExecutor.submit(() -> {
            ExecutorService singleThreadExecutor = threadLocal.get();
            if (singleThreadExecutor == null) {
                singleThreadExecutor = Executors.newSingleThreadExecutor();
                threadLocal.set(singleThreadExecutor);
            }
            singleThreadExecutor.submit(task);
        });
    }

    public void clearThreadLocal() {
        threadLocal.remove();
    }

}

すべてのキューを埋めた後、threadLocalをクリアする必要があります。唯一の欠点は、メソッドが実行されるたびにsingleThreadExecutorが作成されることです。

executeOrdered(実行可能なタスク)

別のスレッドで呼び出される

0