web-dev-qa-db-ja.com

CPUの「マックスアウト」を防ぐにはどうすればよいですか:複数のワーカーを非同期に呼び出す同期メソッドとSemaphoreSlimを使用したスロットル?

私は現在、既存の非常に遅くタイムアウトする本番アプリケーションを最適化しています。 それを書き換えるオプションはありません

つまり、現在4つの他の「ワーカー」WCFサービスを順番に呼び出すのはWCFサービスです。どのワーカーサービスも他の結果に依存していません。 それで、それらを一度に(順次ではなく)呼び出したいと思います。私たちはそれを書き直す余裕がないと繰り返し言います。

enter image description here

最適化では、すべてのワーカーサービスを一度に呼び出す必要があります。これは、非同期性が頭に浮かんだ場所です。

非同期プログラミングの経験は限られていますが、自分の解決策に関して、このトピックについてできる限り広く読んでいます。

問題は、テストでは機能しますが、CPUを使い果たしてしまうことです。よろしくお願いします

以下は、メインのWCFサービスの必須コードの簡易バージョンです。

// The service operation belonging to main WCF Service
public void ProcessAllPendingWork()
{
    var workerTasks = new List<Task<bool>>();
    foreach(var workerService in _workerServices)
    {
        //DoWorkAsync is the worker method with the following signature:
        // Task<bool> DoWorkAsync()

        var workerTask = workerService.DoWorkAsync()
        workerTasks.Add(workerTask);
    }

    var task = Task.Run(async ()=>
    {
        await RunWorkerTasks(workerTasks);
    });
    task.Wait();


}

private async RunWorkerTasks(IEnumerable<Tast<bool>> workerTasks)
{
    using(var semaphore = new SemaphoreSlim(initialCount:3))
    {

        foreach (var workerTask in workerTasks)
        {
            await semaphore.WaitAsync();
            try
            {
                await workerTask;
            }
            catch (System.Exception)
            {
                //assume 'Log' is a predefined logging service
                Log.Error(ex);
            }
        }
    }
} 

私が読んだもの:

並列タスク処理を制限する複数の方法

同時非同期I/O操作の量を制限する方法?

C#で非同期メソッドを調整するアプローチ

C#での同時スレッドの制約

SemaphoresSlimを使用した同時スレッド数の制限

ChannelFactoryおよびCreateChannelを使用した非同期WCF呼び出し

9
user919426

私が何かを見逃していない限り-あなたのサンプルコードはすべてのワーカーを並行して実行します。 「workerService.DoWorkAsync()」を呼び出すまでに、ワーカーはそのジョブを開始します。 「RunWorkerTasks」は、ワーカータスクが完了するまで待機します。 「DoWorkAsync()」は非同期操作を開始し、「await」は待機中のタスクが完了するまで呼び出しメソッドの実行を一時停止します。

CPU使用率が高いという事実は、workerServiceのアクティビティが原因である可能性が高く、それらを呼び出す方法が原因ではありません。これを確認するには、workerService.DoWorkAsync()Thread.Sleep(..)またはTask.Delay(..)に置き換えてみてください。 CPU使用率が低下した場合、非難されるのはワーカーです。 (workerServiceの機能によっては)並列実行すると、CPU使用量が増加することもあり、予想される場合もあります。

並列実行を制限する方法についての質問に出会います。次のサンプルでは、​​正確に3つのスレッドが使用されているわけではなく、最大3つのスレッドが使用されていることに注意してください。

_    Parallel.ForEach(
        _workerServices,
        new ParallelOptions { MaxDegreeOfParallelism = 3 },
        workerService => workerService.DoWorkAsync()
            .ContinueWith(res => 
            {
                // Handle your result or possible exceptions by consulting res.
            })
            .Wait());
_

以前にコードが順次実行されていたとおっしゃっていましたが、ワーカーにも非同期ではないものがあると思います。おそらくそれらを使用する方が簡単です。非同期メソッドを同期的に呼び出すには、ほとんどの場合面倒です。 DoWorkAsync().Wait()を呼び出すだけで、デッドロックのシナリオさえありました。 非同期のTask <T>メソッドを同期的に実行するにはどうすればよいですか? について多くの議論がありました。本質的に私はそれを避けようとします。それが不可能な場合は、複雑さを増すContinueWithを使用するか、以前のSOディスカッションのAsyncHelperを使用します。

_    var results = new ConcurrentDictionary<WorkerService, bool>();
    Parallel.ForEach(
        _workerServices,
        new ParallelOptions { MaxDegreeOfParallelism = 3 },
        workerService => 
            {
                // Handle possible exceptions via try-catch.
                results.TryAdd(workerService, workerService.DoWork());
            });
    // evaluate results
_

_Parallel.ForEach_は、Thread-またはTaskPoolを利用します。つまり、指定されたパラメーター_Action<TSource> body_のすべての実行を専用スレッドにディスパッチします。次のコードで簡単に確認できます。 _Parallel.ForEach_がすでに別のスレッドで作業をディスパッチしている場合は、単純に「高価な」操作を同期的に実行できます。非同期操作は不要であり、実行時パフォーマンスに悪影響を与えることさえあります。

_    Parallel.ForEach(
        Enumerable.Range(1, 4),
        m => Console.WriteLine(Thread.CurrentThread.ManagedThreadId));
_

これは、workerServiceに依存しないテストに使用したデモプロジェクトです。

_    private static bool DoWork()
    {
        Thread.Sleep(5000);
        Console.WriteLine($"done by {Thread.CurrentThread.ManagedThreadId}.");
        return DateTime.Now.Millisecond % 2 == 0;
    }

    private static Task<bool> DoWorkAsync() => Task.Run(DoWork);

    private static void Main(string[] args)
    {
        var sw = new Stopwatch();
        sw.Start();

        // define a thread-safe dict to store the results of the async operation
        var results = new ConcurrentDictionary<int, bool>();

        Parallel.ForEach(
            Enumerable.Range(1, 4), // this replaces the list of workers
            new ParallelOptions { MaxDegreeOfParallelism = 3 },
            // m => results.TryAdd(m, DoWork()), // this is the alternative synchronous call
            m => DoWorkAsync().ContinueWith(res => results.TryAdd(m, res.Result)).Wait());

        sw.Stop();

        // print results
        foreach (var item in results)
        {
            Console.WriteLine($"{item.Key}={item.Value}");
        }

        Console.WriteLine(sw.Elapsed.ToString());
        Console.ReadLine();
    }
_
3
sa.he

同時呼び出しを制限する方法を説明していませんでした。 30の同時ワーカータスクを実行するか、30のWCF呼び出しですべてのワーカータスクを同時に実行するか、またはそれぞれへの同時WCF呼び出しに独自の同時ワーカータスクの制限を設定するか。各WCF呼び出しには4つのワーカータスクしかなく、サンプルコードを見るとすると、30の同時ワーカータスクのグローバル制限が必要だと思います。

まず、@ mjが暗示するように、SemaphoreSlimを使用してworkerService.DoWorkAsync()への呼び出しを制限する必要があります。あなたのコードは現在それらすべてを開始し、終了するのを待つ数だけをスロットルしようとしました。私はこれがあなたがCPUを最大にする理由だと思います。開始されたワーカータスクの数は無制限のままです。ただし、セマフォを保持している間はワーカータスクを待機する必要があります。そうしないと、タスクの作成速度だけが調整され、同時に実行される数は調整されません。

次に、WCFリクエストごとに新しいSemaphoreSlimを作成します。したがって、私の最初の段落から私の質問。これが何かを調整する唯一の方法は、サンプルで30である初期カウントよりも多くのワーカーサービスがある場合ですが、ワーカーは4つしかないと言いました。 「グローバル」な制限を設けるには、シングルトンのSemaphoreSlimを使用する必要があります。

率直に言って、SemaphoreSlimで.Release()を呼び出すことはないため、シングルトンにした場合、プロセスが開始してから30ワーカーを開始すると、コードがハングします。ワーカーがクラッシュしても解放されるように、必ずtry-finallyブロックで実行してください。

以下は急いで書かれたサンプルコードです。

public async Task ProcessAllPendingWork()
{
    var workerTasks = new List<Task<bool>>();
    foreach(var workerService in _workerServices)
    {
        var workerTask = RunWorker(workerService);
        workerTasks.Add(workerTask);
    }

    await Task.WhenAll(workerTasks);
}

private async Task<bool> RunWorker(Func<bool> workerService)
{
    // use singleton semaphore.
    await _semaphore.WaitAsync();
    try
    {
        return await workerService.DoWorkAsync();
    }
    catch (System.Exception)
    {
        //assume error is a predefined logging service
        Log.Error(ex);
        return false; // ??
    }
    finally
    {
        _semaphore.Release();
    }
}
9
zivkan

TPL(タスク並列ライブラリ)によって提供されるタスクの抽象化は、スレッドの抽象化です。タスクはスレッドプールにエンキューされ、実行者がその要求を管理できるときに実行されます。

言い換えると、いくつかの要因(トラフィック、CPUとIO組み込みおよび展開モデル)によっては、ワーカー関数でマネージタスクを実行しようとしても、まったくメリットがない場合があります(または場合によっては遅くなる)。

つまり、非常に高度な抽象化を使用して同時実行性を管理する Task.WaitAll (.NET 4.0から利用可能)を使用することをお勧めします。特に、次のコードは役に立ちます。

  • それは労働者を作成し、すべてを待ちます
  • 実行に10秒かかります(最長のワーカー)
  • キャッチして例外を管理する機会を与える
  • [最後に重要なことですが]は、何をすべきかではなく何をすべきかにあなたの注意を集中させる落胆的なAPIです。
_public class Q57572902
{
    public void ProcessAllPendingWork()
    {
        var workers = new Action[] {Worker1, Worker2, Worker3};

        try
        {
            Task.WaitAll(workers.Select(Task.Factory.StartNew).ToArray());
            // ok
        }
        catch (AggregateException exceptions)
        {
            foreach (var ex in exceptions.InnerExceptions)
            {
                Log.Error(ex);
            }
            // ko
        }
    }

    public void Worker1() => Thread.Sleep(FromSeconds(5)); // do something

    public void Worker2() => Thread.Sleep(FromSeconds(10)); // do something

    public void Worker3() => throw new NotImplementedException("error to manage"); // something wrong

}
_

コメントから、同時に実行するワーカーは最大3人必要であることがわかりました。この場合、 TaskScheduler ドキュメントからLimitedConcurrencyLevelTaskSchedulerをコピーして貼り付けるだけです。

その後、そのようなonw TaskSchedulerでシグルトンインスタンスTaskFactoryを作成する必要があります。

_public static class WorkerScheduler
{
    public static readonly TaskFactory Factory;

    static WorkerScheduler()
    {
        var scheduler = new LimitedConcurrencyLevelTaskScheduler(3);
        Factory = new TaskFactory(scheduler);
    }
}
_

以前のProcessAllPendingWork()コードは、以下を除いて同じです。

_...workers.Select(Task.Factory.StartNew)...
_

それは

_...workers.Select(WorkerScheduler.Factory.StartNew)...
_

カスタムTaskFactoryに関連付けられたWorkerSchedulerを使用する必要があるためです。

ワーカーが応答するデータを返す必要がある場合は、エラーとデータを次のように別の方法で管理する必要があります。

_public void ProcessAllPendingWork()
{
    var workers = new Func<bool>[] {Worker1, Worker2, Worker3};
    var tasks = workers.Select(WorkerScheduler.Factory.StartNew).ToArray();

    bool[] results = null;

    Task
        .WhenAll(tasks)
        .ContinueWith(x =>
        {
            if (x.Status == TaskStatus.Faulted)
            {
                foreach (var exception in x.Exception.InnerExceptions)
                    Log(exception);

                return;
            }

            results = x.Result; // save data in outer scope
        })
        .Wait();

    // continue execution
    // results is now filled: if results is null, some errors occured
}
_
4
Claudio