web-dev-qa-db-fra.com

Comment puis-je empêcher le "maxing out" du CPU: méthode synchrone appelant plusieurs travailleurs de manière asynchrone et étranglant à l'aide de SemaphoreSlim?

J'optimise actuellement une application de production existante, très lente et dépassée. Il n'y a pas d'option pour le réécrire .

En bref, il s'agit d'un service WCF qui appelle actuellement 4 autres services WCF "ouvriers" de manière séquentielle. Aucun des services aux travailleurs ne dépend des résultats de l'autre. Nous aimerions donc qu'il les appelle tous en même temps (pas de manière séquentielle) . Je répète que nous n'avons pas le luxe de le réécrire.

enter image description here

L'optimisation consiste à lui faire appeler tous les services des travailleurs à la fois. C'est là que l'asynchronie est venue à l'esprit.

J'ai une expérience limitée avec la programmation asynchrone, mais j'ai lu aussi largement que possible sur le sujet, en ce qui concerne ma solution.

Le problème est que, lors des tests, cela fonctionne mais optimise mon processeur. J'apprécierais votre aide

Ce qui suit est une version simplifiée du code essentiel dans le service WCF principal

// 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);
            }
        }
    }
} 

Ce que j'ai lu:

Plusieurs façons de limiter le traitement des tâches parallèles

Comment limiter le nombre d'opérations d'E/S asynchrones simultanées?

Approches pour la limitation des méthodes asynchrones en C #

Contraindre les threads simultanés en C #

Limitation du nombre de threads simultanés avec SemaphoresSlim

Appel WCF asynchrone avec ChannelFactory et CreateChannel

9
user919426

Sauf si je manque quelque chose - votre exemple de code exécute TOUS les travailleurs en parallèle. Au moment d'appeler 'workerService.DoWorkAsync ()', le travailleur commence son travail. 'RunWorkerTasks' attend uniquement la fin de la tâche de travail. 'DoWorkAsync ()' démarre l'opération asynchrone tandis que 'wait' suspend l'exécution de la méthode appelante jusqu'à la fin de la tâche attendue.

Le fait d'une utilisation élevée du processeur est probablement dû à l'activité de votre workerService et non à la façon dont vous les appelez. Afin de vérifier cela, essayez de remplacer workerService.DoWorkAsync() par Thread.Sleep(..) ou Task.Delay(..). Si votre utilisation du processeur diminue, ce sont les travailleurs à blâmer. (Selon ce que fait workerService), il peut être correct ou même prévu que la consommation du processeur augmente une fois que vous les exécutez en parallèle.

Venant à votre question de savoir comment limiter l'exécution parallèle. Notez que l'exemple suivant n'utilise pas exactement 3 threads, mais au maximum 3 threads.

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

Comme vous l'avez mentionné précédemment, votre code s'exécutait séquentiellement, je suppose que les travailleurs ont également un équivalent non asynchrone. Il est probablement plus facile de les utiliser. Pour appeler une méthode asynchrone de manière synchrone est principalement un problème. J'ai même eu des scénarios de blocage simplement en appelant DoWorkAsync().Wait(). Il y a eu beaucoup de discussions sur Comment exécuter une méthode asynchrone Task <T> de manière synchrone? . Essentiellement, j'essaye de l'éviter. Si ce n'est pas possible, j'essaie d'utiliser ContinueWith qui augmente la complexité, ou AsyncHelper de la discussion SO précédente.

    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 Tire parti d'un Thread- ou TaskPool. Cela signifie qu'il distribue chaque exécution du paramètre donné Action<TSource> body Sur un thread dédié. Vous pouvez facilement vérifier cela avec le code suivant. Si Parallel.ForEach Distribue déjà le travail sur différents threads, vous pouvez simplement exécuter votre opération "coûteuse" de manière synchrone. Toute opération asynchrone serait inutile ou aurait même un impact négatif sur les performances d'exécution.

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

Il s'agit du projet de démonstration que j'ai utilisé pour les tests et qui ne dépend pas de votre 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

Vous n'avez pas expliqué comment vous vouliez limiter les appels simultanés. Voulez-vous que 30 tâches de travail simultanées soient exécutées ou voulez-vous 30 appels WCF, dont chacune a toutes ses tâches de travail en cours d'exécution, ou voulez-vous que les appels WCF simultanés aient chacun leur propre limite de tâches de travail simultanées? Étant donné que vous avez dit que chaque appel WCF n'a que 4 tâches de travail et que vous regardez votre exemple de code, je suppose que vous voulez une limite globale de 30 tâches de travail simultanées.

Tout d'abord, comme @mjwills l'a laissé entendre, vous devez utiliser SemaphoreSlim pour limiter les appels à workerService.DoWorkAsync(). Votre code démarre actuellement tous, et a seulement essayé de limiter le nombre que vous attendez pour terminer. Je suppose que c'est pourquoi vous maximisez le processeur. Le nombre de tâches ouvrières commencées reste illimité. Notez cependant que vous devrez également attendre la tâche de travail pendant que vous maintenez le sémaphore, sinon vous ne ferez qu'accélérer la vitesse à laquelle vous créez des tâches, pas le nombre qui s'exécutent simultanément.

Deuxièmement, vous créez un nouveau SemaphoreSlim pour chaque demande WCF. D'où ma question de mon premier paragraphe. La seule façon d'étouffer quoi que ce soit est que vous ayez plus de services aux travailleurs que le nombre initial, qui dans votre échantillon est de 30, mais vous avez dit qu'il n'y avait que 4 travailleurs. Pour avoir une limite "globale", vous devez utiliser un SemaphoreSlim singleton.

Troisièmement, vous n'appelez jamais .Release() sur SemaphoreSlim, donc si vous en avez fait un singleton, votre code se bloquera une fois qu'il aura démarré 30 travailleurs depuis le début du processus. Assurez-vous de le faire dans un bloc try-finally, de sorte que si le travailleur tombe en panne, il soit toujours libéré.

Voici un exemple de code écrit à la hâte:

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

L'abstraction de tâche fournie par TPL (bibliothèque parallèle de tâches) est une abstraction de Thread; les tâches sont mises en file d'attente dans un pool de threads, puis exécutées lorsqu'un exécuteur peut gérer cette demande.

Dans d'autres Word, en fonction de certains facteurs (votre trafic, CPU vs IO buound et modèle de déploiement)), essayer d'exécuter une tâche gérée dans votre fonction de travail peut ne générer aucun avantage (ou dans certains cas être plus lent).

Cela dit, je vous suggère d'utiliser Task.WaitAll (disponible à partir de .NET 4.0) qui utilise des abstractions de très haut niveau pour gérer la concurrence; en particulier ce morceau de code pourrait vous être utile:

  • il crée des travailleurs et attend tout
  • l'exécution prend 10 secondes (le plus long travailleur)
  • il attrape et vous donne la possibilité de gérer les exceptions
  • [Last but not least] est une API déclerative qui concentre votre attention sur ce qu'il faut faire et non sur la façon de le faire.
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

}

J'ai vu dans les commentaires que vous avez besoin d'un maximum de 3 ouvriers fonctionnant en même temps; dans ce cas, vous pouvez simplement copier-coller une documentation LimitedConcurrencyLevelTaskScheduler de TaskScheduler .

Après cela, vous devez créer une instance sigleton TaskScheduler avec son onw TaskFactory comme ça:

public static class WorkerScheduler
{
    public static readonly TaskFactory Factory;

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

Le code précédent de ProcessAllPendingWork() reste le même sauf pour

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

cela devient

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

car vous devez utiliser le TaskFactory associé à votre WorkerScheduler personnalisé.

Si votre travailleur doit renvoyer certaines données à la réponse, les erreurs et les données doivent être gérées de manière différente comme suit:

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