web-dev-qa-db-fra.com

Quelle construction utiliser pour garantir que 100 tâches s'exécutent en parallèle

J'essayais de créer un test d'intégration pour mon service où 100 clients se connecteraient, se connecteraient, enverraient une demande et consignaient toutes les réponses pendant un certain temps configurable.

J'ai construit une classe pour le client en utilisant des sockets asynchrones et cela fonctionne très bien. Je les ai tous démarrés à l'aide de Task et Task.Factory, envoyé la connexion et posté reçoit chaque fois que je reçois des données, jusqu'à l'expiration du délai, puis j'ai appelé l'arrêt sur eux.

Il semble que ceux-ci ne fonctionnent jamais vraiment en parallèle. Parfois, je pourrais courir 35 secondes à la fois, parfois un peu plus. Je suppose que le planificateur de tâches les exécute lorsqu'il semble approprié plutôt que d'un seul coup.

Maintenant, je comprends que je ne peux pas vraiment avoir 100 threads en cours d'exécution simultanée, mais je veux garantir que tous les 100 sont démarrés et que le système d'exploitation change de contexte en essayant de les exécuter tous.

Au final, je souhaite simuler un grand nombre de clients connectés à mon service obtenant tous un flux de données.

Quelle construction dois-je utiliser si la tâche ne fonctionne pas?

Tentative actuelle:

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace IntegrationTests
{
    class Program
    {
        static void Main(string[] args)
        {
            string       server            = ConfigurationManager.AppSettings["server"];
            int          port              = int.Parse(ConfigurationManager.AppSettings["port"]);
            int          numClients        = int.Parse(ConfigurationManager.AppSettings["numberOfClients"]);
            TimeSpan     clientLifetime    = TimeSpan.Parse(ConfigurationManager.AppSettings["clientLifetime"]);
            TimeSpan     timeout           = TimeSpan.Parse(ConfigurationManager.AppSettings["timeout"]);
            TimeSpan     reconnectInterval = TimeSpan.Parse(ConfigurationManager.AppSettings["reconnectInterval"]);
            List<string> clientIds         = ConfigurationManager.GetSection("clientIds") as List<string>;

            try
            {
                // SNIP configure logging

                // Create the specified number of clients, to carry out test operations, each on their own threads
                Task[] tasks = new Task[numClients];
                for(int count = 0; count < numClients; ++count)
                {
                    var index = count;
                    tasks[count] = Task.Factory.StartNew(() =>
                        {
                            try
                            {
                                // Reuse client Ids, if there are more clients then clientIds.
                                // Keep in mind that tasks are not necessarily started in the order they were created in this loop.
                                // We may see client id 1 be assigned client id 2 if another client was started before it, but we
                                // are using all clientIds
                                string clientId = null;
                                if (numClients < clientIds.Count)
                                {
                                    clientId = clientIds[index];
                                }
                                else
                                {
                                    clientId = clientIds[index % clientIds.Count];
                                }

                                // Create the actual client
                                Client client = new Client(server, port, clientId, timeout, reconnectInterval);
                                client.Startup();

                                // Will make an sync request issue a recv.
                                // Everytime we get a reponse, it will be logged and another recv will be posted.
                                // This will continue until shutdown is called
                                client.MakeRequest(symbol);

                                System.Threading.Thread.Sleep(clientLifetime);

                                client.Shutdown();
                            }
                            catch(Exception e)
                            {
                                // SNIP - Log it
                            }
                        });
                }
                Task.WaitAll(tasks);
            }
            catch (Exception e)
            {
                // SNIP - Log it
            }
        }
    }
}
5
Christopher Pisz

Les tâches et les threads existent à des fins différentes. Les tâches sont destinées à être des tâches de courte durée qui doivent être exécutées en arrière-plan. Les threads représentent une ressource opérationnelle pour une exécution simultanée.

En interne, le TaskManager utilise un pool de threads afin de pouvoir réutiliser des threads pour traiter plus de tâches. Les threads sont coûteux à installer et à démonter, de sorte qu'ils ne fonctionnent pas bien dans le but pour lequel les tâches ont été créées. Bien que vous puissiez influencer le nombre de threads disponibles pour le gestionnaire de tâches, il est toujours responsable de distribuer le travail aux threads.

Garantie de X nombre de clients simultanés

La seule façon de garantir cela est d'utiliser Thread au lieu de Task. Si vous deviez restructurer un peu votre code, vous pourriez gérer vos clients simultanés comme ceci:

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Text;
using System.Threading;

namespace IntegrationTests
{
    private static string server;
    private static int port;
    private static TimeSpan clientLifetime;
    private static TimeSpan timeout;
    private static TimeSpan reconnectInterval;
    private static List<string> clientIds;
    private static Barrier barrier;

    class Program
    {
        static void Main(string[] args)
        {
            int          numClients        = int.Parse(ConfigurationManager.AppSettings["numberOfClients"]);
            server            = ConfigurationManager.AppSettings["server"];
            port              = int.Parse(ConfigurationManager.AppSettings["port"]);
            clientLifetime    = TimeSpan.Parse(ConfigurationManager.AppSettings["clientLifetime"]);
            timeout           = TimeSpan.Parse(ConfigurationManager.AppSettings["timeout"]);
            reconnectInterval = TimeSpan.Parse(ConfigurationManager.AppSettings["reconnectInterval"]);
            clientIds         = ConfigurationManager.GetSection("clientIds") as List<string>;
            barrier           = new Barrier(numClients + 1);

            try
            {
                // SNIP configure logging

                // Create the specified number of clients, to carry out test operations, each on their own threads
                Thread[] threads= new Thread[numClients];
                for(int count = 0; count < numClients; ++count)
                {
                    var index = count;
                    threads[count] = new Thread();
                    threads[count].Name = $"Client {count}"; // for debugging
                    threads[count].Start(RunClient);
                }

                // We loose the convenience of awaiting all tasks,
                // but use a thread barrier to block this thread until all the others are done.
                barrier.SignalAndWait();
            }
            catch (Exception e)
            {
                // SNIP - Log it
            }
        }

        private void RunClient()
        {
            try
            {
                // Reuse client Ids, if there are more clients then clientIds.
                // Keep in mind that tasks are not necessarily started in the order they were created in this loop.
                // We may see client id 1 be assigned client id 2 if another client was started before it, but we
                // are using all clientIds
                string clientId = null;
                if (numClients < clientIds.Count)
                {
                    clientId = clientIds[index];
                }
                else
                {
                    clientId = clientIds[index % clientIds.Count];
                }

                // Create the actual client
                Client client = new Client(server, port, clientId, timeout, reconnectInterval);
                client.Startup();

                // Will make an sync request issue a recv.
                // Everytime we get a reponse, it will be logged and another recv will be posted.
                // This will continue until shutdown is called
                client.MakeRequest(symbol);

                System.Threading.Thread.Sleep(clientLifetime);

                client.Shutdown();
            }
            catch(Exception e)
            {
                // SNIP - Log it
            }
            finally
            {
                barrier.SignalAndWait();
            }
        }
    }
}
7
Berin Loritsch

Je ne pense pas qu'il y ait un problème avec votre test.

J'ai utilisé un code similaire pour les tests de charge (de base) et vu bien plus de 100 tâches simultanées.

Je suppose qu'il y a un problème avec la façon dont vous vous connectez. Mesurez-vous simplement le nombre de connexions simultanées que votre serveur peut gérer?

Par exemple, le code ci-dessous comptera jusqu'à 1000.

Cependant, notez la différence si nous remplaçons Task.Delay par Thread.Sleep. Cela casse l'application car plusieurs tâches s'exécutent sur le même thread.

Maintenant, si nous aussi changeons la tâche.

tasks.Add(Task.Factory.StartNew(async () => Work(),TaskCreationOptions.LongRunning));

Le code fonctionne à nouveau, car il sait créer les nouvelles tâches sur de nouveaux threads

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Collections.Generic;

namespace UnitTestProject1
{
    [TestClass]
    public class UnitTest1
    {
        volatile int count = 0;
        [TestMethod]
        public async Task TestMethod1()
        {
            var tasks = new List<Task>();
            for(int i = 0;i<1000;i++)
            {
                tasks.Add(Work());
            }
            await Task.WhenAll(tasks.ToArray());
            Debug.WriteLine("finished");
        }

        async Task Work()
        {
            count++;
            Debug.WriteLine(count);
            await Task.Delay(10000);
            Debug.WriteLine(count);
            count--;
        }
    }
}
1
Ewan