私は現在、既存の非常に遅くタイムアウトする本番アプリケーションを最適化しています。 それを書き換えるオプションはありません。
つまり、現在4つの他の「ワーカー」WCFサービスを順番に呼び出すのはWCFサービスです。どのワーカーサービスも他の結果に依存していません。 それで、それらを一度に(順次ではなく)呼び出したいと思います。私たちはそれを書き直す余裕がないと繰り返し言います。
最適化では、すべてのワーカーサービスを一度に呼び出す必要があります。これは、非同期性が頭に浮かんだ場所です。
非同期プログラミングの経験は限られていますが、自分の解決策に関して、このトピックについてできる限り広く読んでいます。
問題は、テストでは機能しますが、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);
}
}
}
}
私が読んだもの:
私が何かを見逃していない限り-あなたのサンプルコードはすべてのワーカーを並行して実行します。 「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();
}
_
同時呼び出しを制限する方法を説明していませんでした。 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();
}
}
TPL(タスク並列ライブラリ)によって提供されるタスクの抽象化は、スレッドの抽象化です。タスクはスレッドプールにエンキューされ、実行者がその要求を管理できるときに実行されます。
言い換えると、いくつかの要因(トラフィック、CPUとIO組み込みおよび展開モデル)によっては、ワーカー関数でマネージタスクを実行しようとしても、まったくメリットがない場合があります(または場合によっては遅くなる)。
つまり、非常に高度な抽象化を使用して同時実行性を管理する Task.WaitAll (.NET 4.0から利用可能)を使用することをお勧めします。特に、次のコードは役に立ちます。
_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
}
_