私はスレッドプールパターンについて読んでいますが、次の問題の通常の解決策を見つけることができないようです。
タスクを連続して実行したいことがあります。たとえば、ファイルからテキストのチャンクを読み取りますが、何らかの理由でその順序でチャンクを処理する必要があります。したがって、基本的には並行性を排除したいです一部のタスク。
タスクが*
は、プッシュされた順序で処理する必要があります。他のタスクは、任意の順序で処理できます。
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つ注意すべき点は、グループ内のすべてのタスクに事前にアクセスできないことです(グループを開始する前にすべてのタスクが到着するのを待つことはできません)。
お時間をいただきありがとうございます。
次のようなものにより、シリアルタスクとパラレルタスクをキューに入れることができます。シリアルタスクは次々に実行され、パラレルタスクは任意の順序で実行されますが、パラレルに実行されます。これにより、必要に応じてタスクをシリアル化したり、並列タスクを使用したりすることができますが、タスクを受け取ったときにこれを行います。つまり、シーケンス全体を事前に知る必要はなく、実行順序は動的に維持されます。
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);
}
}
}
}
スレッドプールは、すべてが完了していれば、タスクの相対的な順序が重要でない場合に適しています。特に、それらがすべて並行して行われても問題はありません。
特定の順序でタスクを実行する必要がある場合、タスクは並列処理に適していないため、スレッドプールは適切ではありません。
これらのシリアルタスクをメインスレッドから移動する場合、タスクキューを備えた単一のバックグラウンドスレッドがそれらのタスクに適しています。並列処理に適した残りのタスクには、引き続きスレッドプールを使用できます。
はい。それは、タスクが順序正しいタスクか「並列化可能」タスクかに応じて、どこにタスクを送信するかを決定する必要があることを意味しますが、これは大したことではありません。
シリアル化する必要があるが、他のタスクと並行して実行できるグループがある場合、複数の選択肢があります。
基本的に、保留中のタスクがいくつかあります。一部のタスクは、1つ以上の他の保留中のタスクの実行が終了したときにのみ実行できます。
保留中のタスクは、依存関係グラフでモデル化できます。
そのため、保留中のタスクの追加/削除に使用される(少なくとも)1つのスレッドがあり、作業スレッドのスレッドプールがあります。
タスクが依存関係グラフに追加されたら、次を確認する必要があります。
パフォーマンス:
仮定:
行間を読んだことがあるかもしれませんが、他のタスクに干渉しないようにタスクを設計する必要があります。また、タスクの優先度を決定する方法が必要です。タスクの優先度には、各タスクで処理されるデータを含める必要があります。 2つのタスクが同じオブジェクトを同時に変更することはできません。代わりに、タスクの1つが他のタスクよりも優先される必要があります。そうでない場合、オブジェクトで実行される操作はスレッドセーフである必要があります。
スレッドプールでやりたいことを行うには、何らかのスケジューラーを作成する必要があるかもしれません。
そんな感じ:
TaskQueue->スケジューラー->キュー-> ThreadPool
スケジューラは独自のスレッドで実行され、ジョブ間の依存関係を追跡します。ジョブを実行する準備ができると、スケジューラはスレッドプールのキューにジョブをプッシュするだけです。
ThreadPoolは、ジョブが完了したことを示す信号をスケジューラに送信して、ジョブがそのジョブに依存するジョブをキューに入れるようにしなければならない場合があります。
あなたの場合、依存関係はおそらくリンクリストに保存できます。
次の依存関係があるとしましょう:3-> 4-> 6-> 8
ジョブ3はスレッドプールで実行されていますが、ジョブ8が存在するという考えはまだありません。
ジョブ3は終了します。リンクリストから3を削除し、ジョブ4をスレッドプールのキューに入れます。
ジョブ8が到着します。リンクリストの最後に配置します。
完全に同期する必要がある唯一の構造は、スケジューラーの前後のキューです。
私が問題を正しく理解している場合、jdk executorにはこの機能はありませんが、独自にロールバックするのは簡単です。基本的に必要です
ExecutorService
)Jdk executorとの違いは、1つのキューにn個のスレッドがありますが、n個のキューとm個のスレッドが必要なことです(nはmに等しい場合とそうでない場合があります)
*各タスクにキーがあることを読み取った後に編集*
もう少し詳しく
key.hashCode() % n
と同じくらい簡単かもしれません。既知のキー値のスレッドへの静的マッピング、または必要なものこのスキームにワーカースレッドの自動再起動を追加する方が簡単です。その後、ワーカースレッドをマネージャーに登録して、「このキューを所有している」と通知し、その周りのハウスキーピングとスレッドでのエラーの検出(つまり、そのキューの所有権を登録解除し、新しいスレッドを起動するトリガーであるキューの空きプールにキューを返します)
この状況では、スレッドプールを効果的に使用できると思います。アイデアは、依存タスクのグループごとに個別のstrand
オブジェクトを使用することです。 strand
オブジェクトを使用してまたはなしでキューにタスクを追加します。依存タスクで同じstrand
オブジェクトを使用します。スケジューラは、次のタスクにstrand
があるかどうか、およびこのstrand
がロックされているかどうかを確認します。そうでない場合-このstrand
をロックして、このタスクを実行します。 strand
がすでにロックされている場合-次のスケジューリングイベントまでこのタスクをキューに保持します。タスクが完了したら、strand
のロックを解除します。
その結果、単一のキューが必要になり、追加のスレッドや複雑なグループなどは必要ありません。strand
オブジェクトは、lock
とunlock
の2つのメソッドで非常に簡単になります。
私はしばしば同じ設計上の問題に出会います。複数の同時セッションを処理する非同期ネットワークサーバーの場合。セッション内のタスクが依存している場合(セッション内部タスクをグループ内の依存タスクにマップする)、セッションは独立しています(これにより、独立タスクと依存タスクのグループにマップされます)。説明したアプローチを使用して、セッション内での明示的な同期を完全に回避します。すべてのセッションには、独自のstrand
オブジェクトがあります。
さらに、このアイデアの既存の(素晴らしい)実装を使用します: Boost Asio library (C++)。 strand
という用語を使用しました。実装はエレガントです。Iwrap非同期タスクをスケジューリングする前に、対応するstrand
オブジェクトにラップします。
スレッドプールを使用しないことを示唆する答えは、タスクの依存関係/実行順序の知識をハードコーディングするようなものです。代わりに、2つのタスク間の開始/終了依存関係を管理するCompositeTask
を作成します。タスクインターフェイスの背後にある依存関係をカプセル化することにより、すべてのタスクを均一に処理し、プールに追加できます。これにより、実行の詳細が非表示になり、スレッドプールを使用するかどうかに影響を与えずにタスクの依存関係を変更できます。
質問は言語を指定していません-私はJavaを使用します。
class CompositeTask implements Task
{
Task firstTask;
Task secondTask;
public void run() {
firstTask.run();
secondTask.run();
}
}
これにより、タスクが連続して同じスレッドで実行されます。多数のCompositeTask
sを連結して、必要な数のシーケンシャルタスクのシーケンスを作成できます。
ここでの欠点は、すべてのタスクが連続して実行されている間、スレッドが拘束されることです。最初のタスクと2番目のタスクの間に実行したい他のタスクがあるかもしれません。したがって、2番目のタスクを直接実行するのではなく、2番目のタスクの実行を複合タスクでスケジュールします。
class CompositeTask implements Runnable
{
Task firstTask;
Task secondTask;
ExecutorService executor;
public void run() {
firstTask.run();
executor.submit(secondTask);
}
}
これにより、最初のタスクが完了するまで2番目のタスクが実行されなくなり、プールが他の(おそらくより緊急の)タスクを実行できるようになります。最初のタスクと2番目のタスクは別々のスレッドで実行される可能性があるため、同時に実行されませんが、タスクで使用される共有データは他のスレッドから見えるようにする必要があります(変数volatile
を作成するなど)
これはシンプルでありながら強力で柔軟なアプローチであり、タスク自体が異なるスレッドプールを使用して実行制約を定義するのではなく、実行制約を定義できます。
連続したジョブがあるため、これらのジョブをチェーンでまとめて、ジョブが完了したらジョブ自体をスレッドプールに再送信させることができます。ジョブのリストがあるとします:
[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
のさらなる作業とさらなる作業の再送信が可能なジョブチェーンを定義できるシステムを作成します。
なぜ私たちは仕事を分割することさえしなければならないのですか?つまり、これらは順番に依存しているため、同じスレッドですべてを実行しても、そのチェーンを取得して複数のスレッドに分散するよりも速くも遅くもなりません。 「十分な」作業負荷を想定すると、どのスレッドも常に作業を行うことができるため、ジョブをまとめるのがおそらく最も簡単です。
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をジョブと組み合わせるでしょう。
2つの アクティブオブジェクト を使用します。つまり、アクティブオブジェクトパターンは、優先キューと、キューからタスクを取得して処理できる1つ以上の作業スレッドで構成されます。
したがって、1つの作業スレッドで1つのアクティブなオブジェクトを使用します。キューに配置されるすべてのタスクは、順番に処理されます。作業スレッドの数が1を超える2番目のアクティブオブジェクトを使用します。この場合、作業スレッドは任意の順序でキューからタスクを取得して処理します。
幸運。
あなたのシナリオを理解している限り、これは達成可能です。基本的に必要なのは、メインスレッドでタスクを調整するためにスマートなことです。 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を使用することで、より複雑なスレッド調整シナリオを実装できます。
コンセプトをミックスしていると思います。スレッド間でいくつかの作業を分散させたい場合は、スレッドプールは問題ありませんが、スレッド間で依存関係を混在させ始めた場合、あまり良い考えではありません。
私のアドバイスは、単純にスレッドプールを使用しないでくださいそれらのタスクのためです。専用のスレッドを作成し、そのスレッドだけで処理する必要のある順次項目の単純なキューを保持するだけです。その後、シーケンシャル要件がない場合はタスクをスレッドプールにプッシュし続け、必要な場合は専用スレッドを使用できます。
明確化:常識を使用して、シリアルタスクのキューは、各タスクを次々に処理する単一のスレッドによって実行されるものとします:)
1つのタスクが完了するのを待ってから依存タスクを開始する必要があるため、最初のタスクで依存タスクをスケジュールできる場合は簡単に実行できます。したがって、2番目の例では、タスク2の最後にタスク7をスケジュールし、タスク3の最後に4-> 6および6-> 8のタスク4などをスケジュールします。
最初は、タスク1、2、5、9 ...をスケジュールするだけで、残りは従う必要があります。
さらに一般的な問題は、依存タスクを開始する前に複数のタスクを待機する必要がある場合です。これを効率的に処理するのは、簡単なことではありません。
これらのタスクを確実に順序付けるにはどうしますか?
Push task1
Push task2
Push task346
Push task5
編集に応じて:
Push task1
Push task27 **
Push task3468 *
Push task5
Push task9
2種類のタスクがあります。単一のキューでそれらを混在させることはかなり奇妙に感じます。 1つのキューの代わりに2つあります。簡単にするために、両方にThreadPoolExecutorを使用することもできます。シリアルタスクの場合は、固定サイズ1を指定するだけで、同時に実行できるタスクの場合はさらに多くを指定します。なぜそれが不器用になるのかはわかりません。シンプルで愚かにしてください。 2つの異なるタスクがあるので、それらを適切に処理してください。
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の実行を完了します。
多くの答えがあり、明らかに受け入れられました。しかし、なぜ継続を使用しないのですか?
既知の「シリアル」条件がある場合、この条件で最初のタスクをキューに入れるとき、タスクを保留します。そして、さらなるタスクのために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);
}
}
順序付きおよび順序なしの実行メソッドを持つスレッドプール:
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(実行可能なタスク)
別のスレッドで呼び出される