web-dev-qa-db-ja.com

並列タスクライブラリを介して実行されるアクティブなタスクの数を制限する最良の方法

処理を必要とするジョブのlotを保持するキューを考えてください。キューの制限は一度に1つのジョブしか取得できず、ジョブの数を知る方法がありません。ジョブは完了するまでに10秒かかり、Webサービスからの応答を待つので、CPUに縛られません。

このようなものを使用する場合

while (true)
{
   var job = Queue.PopJob();
   if (job == null)
      break;
   Task.Factory.StartNew(job.Execute); 
}

その後、ジョブを完了するよりもはるかに高速にキューからジョブを猛烈にポップし、メモリを使い果たし、ロバに落ちます。 >。<

使用できません(考えていません) ParallelOptions.MaxDegreeOfParallelism Parallel.InvokeまたはParallel.ForEachを使用できないため

私が見つけた3つの選択肢

  1. Task.Factory.StartNewを置き換えます

    Task task = new Task(job.Execute,TaskCreationOptions.LongRunning)
    task.Start();
    

    これは多少問題を解決するようですが、私は これが何をしているのか明確に ではなく、これが最良の方法であるかどうかはわかりません。

  2. 同時実行の程度を制限するカスタムタスクスケジューラー を作成します

  3. BlockingCollection のようなものを使用して、開始時にジョブをコレクションに追加し、終了時に削除して実行可能な数を制限します。

#1では、正しい決定が自動的に行われることを信頼しなければなりません。#2 /#3自分で実行できるタスクの最大数を計算する必要があります。

これを正しく理解しましたか?これはより良い方法ですか、それとも別の方法がありますか?

[〜#〜] edit [〜#〜]-これは、以下の回答、生産者と消費者のパターンから思いついたものです。

全体的なスループットの目的は、処理可能な速度よりも速くジョブをデキューすることではなく、複数のスレッドポーリングキューを持たないことです(ここには示されていませんが、非ブロッキングopであり、複数の場所から高頻度でポーリングされると膨大なトランザクションコストにつながります) 。

// BlockingCollection<>(1) will block if try to add more than 1 job to queue (no
// point in being greedy!), or is empty on take.
var BlockingCollection<Job> jobs = new BlockingCollection<Job>(1);

// Setup a number of consumer threads.
// Determine MAX_CONSUMER_THREADS empirically, if 4 core CPU and 50% of time
// in job is blocked waiting IO then likely be 8.
for(int numConsumers = 0; numConsumers < MAX_CONSUMER_THREADS; numConsumers++)
{
   Thread consumer = new Thread(() =>
   {
      while (!jobs.IsCompleted)
      {
         var job = jobs.Take();
         job.Execute();
      }
   }
   consumer.Start();
}

// Producer to take items of queue and put in blocking collection ready for processing
while (true)
{
    var job = Queue.PopJob();
    if (job != null)
       jobs.Add(job);
    else
    {
       jobs.CompletedAdding()
       // May need to wait for running jobs to finish
       break;
    }
}
36
Ryan

answer を指定しましたが、これはこの質問に非常に当てはまります。

基本的に、TPL Taskクラスは、CPUにバインドされた作業をスケジュールするために作成されます。作業をブロックするためのものではありません。

CPUではないリソースを使用しています。サービスの応答を待っています。つまり、TPLはCPUの境界をある程度想定しているため、リソースを誤って管理します。

リソースを自分で管理する:固定数のスレッドまたはLongRunningタスクを開始します(基本的に同じです)。経験的にスレッドの数を決定します。

信頼性の低いシステムを実稼働環境に入れることはできません。そのため、#1をお勧めしますが、throttled 。作業項目の数だけスレッドを作成しないでください。リモートサービスを飽和させるのに必要な数のスレッドを作成します。 N個のスレッドを生成し、それらを使用してM個の作業項目を処理するヘルパー関数を作成します。そのようにして、完全に予測可能で信頼できる結果を得ることができます。

22
usr

後でコードまたはサードパーティのライブラリでawaitによって引き起こされる潜在的なフローの分割と継続は、長時間実行されるタスク(またはスレッド)でうまく動作しないため、長時間実行されるタスクを使用しないでください。 async/awaitの世界では、それらは役に立たない。詳細 こちら

ThreadPool.SetMaxThreadsを呼び出すことはできますが、この呼び出しを行う前に、最大スレッド数以下の値を使用して、ThreadPool.SetMinThreadsで最小スレッド数を設定してください。ところで、MSDNのドキュメントは間違っています。少なくとも.NET 4.5および4.6では、これらのメソッド呼び出しでマシンのコア数を下回ることができます。この場合、この手法を使用してメモリ制限32ビットサービスの処理能力を削減しました。

ただし、アプリ全体ではなく、その一部のみを処理する場合は、カスタムタスクスケジューラがジョブを実行します。かなり前に、MSはLimitedConcurrencyLevelTaskSchedulerを含むいくつかのカスタムタスクスケジューラを使用して samples をリリースしました。 Task.Factory.StartNewを使用してメインの処理タスクを手動で生成し、カスタムタスクスケジューラを提供します。それによって生成される他のすべてのタスクは、async/awaitおよびTask.Yieldを含みます。 asyncメソッド内。

ただし、特定のケースでは、両方のソリューションは、ジョブを完了する前にジョブのキューを使い果たすことを止めません。そのキューの実装と目的によっては、これは望ましくない場合があります。それらは、「大量のタスクを起動し、スケジューラーにそれらを実行する時間を見つけさせる」タイプのソリューションに似ています。したがって、おそらくここでもう少し適切なのは、semaphoresを介したジョブの実行をより厳密に制御する方法です。コードは次のようになります。

semaphore = new SemaphoreSlim(max_concurrent_jobs);

while(...){
 job = Queue.PopJob();
 semaphore.Wait();
 ProcessJobAsync(job);
}

async Task ProcessJobAsync(Job job){
 await Task.Yield();
 ... Process the job here...
 semaphore.Release();
}

猫の皮を剥ぐ方法は複数あります。適切だと思われるものを使用してください。

12
MoonStom

Microsoftには、DataFlowと呼ばれる非常にクールなライブラリがあります。詳細 ここ

ActionBlockクラスを使用して、ExecutionDataflowBlockOptionsオブジェクトのMaxDegreeOfParallelismを設定する必要があります。 ActionBlockはasync/awaitで適切に動作するため、外部呼び出しが待機されている場合でも、新しいジョブの処理は開始されません。

ExecutionDataflowBlockOptions actionBlockOptions = new ExecutionDataflowBlockOptions
{
     MaxDegreeOfParallelism = 10
};

this.sendToAzureActionBlock = new ActionBlock<List<Item>>(async items => await ProcessItems(items),
            actionBlockOptions);
...
this.sendToAzureActionBlock.Post(itemsToProcess)
8
Alon Catz

ここでの問題は多すぎないようですrunningTasks、多すぎるTasks。コードは、実行速度に関係なく、できるだけ多くのTasksをスケジュールしようとします。そして、あなたがあまりにも多くの仕事を持っている場合、これはあなたがOOMを取得することを意味します。

このため、提案された解決策のどれも実際に問題を解決しません。単にLongRunningを指定するだけで問題が解決すると思われる場合、新しいThreadの作成(LongRunningが行うこと)に時間がかかり、効果的に新しい仕事。したがって、この解決策は偶然にしか機能せず、後で他の問題につながる可能性が高いでしょう。

解決策に関して、私はほとんどusrに同意します:合理的にうまく機能する最も簡単な解決策は、固定数のLongRunningタスクを作成し、Queue.PopJob()lock(そのメソッドがスレッドセーフでない場合)およびExecute() sジョブ。

PDATE:さらに考えた後、次の試みがひどく動作する可能性が高いことに気付きました。あなたにとって本当にうまくいくと確信している場合にのみ使用してください。


しかし、TPLは、IOにバインドされたTasksであっても、最適な並列度を見つけようとします。それで、あなたはそれをあなたの利益のために使用しようとするかもしれません。 Long Tasksはここでは機能しません。TPLの観点からは、作業が行われず、新しいTasksが何度も開始されるためです。代わりにできることは、各Taskの終わりに新しいTaskを開始することです。このようにして、TPLは何が起こっているかを認識し、そのアルゴリズムがうまく機能する可能性があります。また、TPLに並列度を決定させるには、その行の最初にあるTaskの開始時に、Tasksの別の行を開始します。

このアルゴリズムmayはうまく機能します。しかし、TPLが並列処理の程度に関して悪い決定を下す可能性もあります。実際、このようなことは試していません。

コードでは、次のようになります。

void ProcessJobs(bool isFirst)
{
    var job = Queue.PopJob(); // assumes PopJob() is thread-safe
    if (job == null)
        return;

    if (isFirst)
        Task.Factory.StartNew(() => ProcessJobs(true));

    job.Execute();

    Task.Factory.StartNew(() => ProcessJob(false));
}

で開始

Task.Factory.StartNew(() => ProcessJobs(true));
7
svick

TaskCreationOptions.LongRunningはタスクをブロックするのに便利であり、ここで使用することは正当です。それが行うことは、タスクにスレッドを捧げることをスケジューラに提案することです。スケジューラー自体は、スレッドの数をCPUコアの数と同じレベルに保ち、過度のコンテキスト切り替えを回避しようとします。

Joseph AlbahariによるC#のスレッド化

1
Maciej

これを実現するために、メッセージキュー/メールボックスメカニズムを使用します。それは俳優モデルに似ています。 MailBoxを持つクラスがあります。このクラスを「ワーカー」と呼びます。メッセージを受信できます。これらのメッセージはキューに入れられ、基本的に、ワーカーに実行させるタスクを定義します。ワーカーは、タスクのTask.Wait()を使用して、次のメッセージをデキューして次のタスクを開始する前に終了します。

所有するワーカーの数を制限することにより、実行される同時スレッド/タスクの数を制限できます。

これは、分散コンピューティングエンジンに関する私のブログ投稿で、ソースコードとともに概説されています。 IActorとWorkerNodeのコードを見れば、それが理にかなっていると思います。

https://long2know.com/2016/08/creating-a-distributed-computing-engine-with-the-actor-model-and-net-core/

1
long2know