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