Im vorherigen Artikel (To parallel or not to parallel) habe ich eine Aufgabe gezeigt die sich, durch den intensiven Zugriff auf die Festplatte, nicht sonderlich gut zur Parallelisierung eignet. In diesem Artikel will ich eine Methode zeigen wie trotz intensiver E/A-Zugriffe, allerdings auf die Netzwerkkarte, sehr gut Parallelisiert werden kann. Gerade das "pingen" von ganzen LAN-Segmenten lässt sich gut Parallelisieren, da der Thread welcher den Ping ausführt solange wartet bis er Antwort erhält oder der Timeout abgelaufen ist. Während der Thread, oder Ping, auf Antwort wartet kann der nächste Ping schon wieder abgesetzt werden. Hier gilt es die ideale Anzahl von gleichzeitig initialisierten Ping-Objekten zu bestimmen. Während verschiedener Tests habe ich den Wert von 36 Thread's pro Kern als besten ermittelt. Für jene unter euch die's nicht glauben ist hier ein ScreenShot mit der Zeit:

Und als Nachweis dass die Threads auch wirklich etwas tun, außer Thread.Sleep(), hier ein ScreenShot mit dem Ergebnis:

Wie aus dem ScreenShot ersichtlich wird der Hostnamen aufgelöst und der Online-Status ermittelt. Ich finde ein ganzes LAN-Segment mit 254 möglichen Adressen in rund 2 Sekunden zu überprüfen und auszuwerten ist richtig schnell, oder etwa nicht?
Ich verwende dieses mal nicht die Parallel-Klasse, sondern die Task- und TaskManager-Klasse aus der TPL. Die TaskManager-Klasse bietet durch Verwendung einer TaskManagerPolicy die Möglichkeit, sehr genau das Verhalten der Thread-Erzeugung und -Verwendung zu steuern. In dieser Policy wird festgelegt:
- Die minimale Anzahl der CPU's die verwendet werden soll.
- Die maximale Anzahl der CPU's die verwendet werden soll.
- Die ideale Anzahl an Thread's pro CPU, die verwendet werden soll.
- Die maximale Größe des Stack.
- Die Priorität der verwendeten Thread's.
Klingt ja ganz nett. Doch ich wollte wissen, ob die Policy tatsächlich auch so umgesetzt wird. Also habe ich mir eine Methode geschrieben um das zu überprüfen. In dieser Methode wird die verwaltete ID des jeweiligen Thread ermittelt und in eine Hashtable eingefügt, wenn die ID noch nicht in dieser vorhanden ist. Hier der Code der Test-Methode:
[SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.ControlThread)]
private static void SimulateCheck(object obj)
{
// ThreadAffinität wird verwendet um das gleiche Verhalten
// wie in der OriginalMethode zu simulieren
Thread.BeginThreadAffinity();
// die managed ThreadId holen
int tId = Thread.CurrentThread.ManagedThreadId;
// prüfen ob die ThreadID bereits in der Tabelle vorhanden ist.
if (!hashTable.ContainsKey(tId))
{
// nicht vorhanden, jetzt hinzufügen
hashTable.Add(tId, string.Empty);
}
// Pause, in etwa so lange wie die Ping-Verarbeitung
// in der OriginalMethode benötigt.
Thread.Sleep(600);
// ThreadAffinität beenden.
Thread.EndThreadAffinity();
}Die TaskManagerPolicy soll also gewährleisten das bei einer Anzahl von 36 Thread's, als idealThreadCount, auf einem DualCore System nie mehr als 72 Thread's erzeugt und verwendet werden. In der Hauptmethode wird eine Liste von 253 Thread,s erzeugt und abgearbeitet. Anschließend wird diese Liste durchlaufen und jeder Thread auf seine Beendigung überprüft. Nachfolgend der verwendetet Code in der Hauptmethode:
static void Main(string[] args)
{
//ipHelper = new IPHelper(IPHelper.GetLocalIPAdress(false));
netSegment = "192.168.1."; // ipHelper.BaseAdress;
// ClientList initialisieren
clientList = new ClientList();
hashTable = new Hashtable();
// neue Stopwatch-Instanz initialisieren
Stopwatch sw = new Stopwatch();
#region using Tasks
// Initialisiert eine neue TaskManager-Instanz mit der Policy
// nutze min. 1 CPU,
// nutze max. alle CPU's,
// ideale Anzahl der Threads (ermittelt 36)
// maxStackSize Default,
// ThreadPriorität grösser als Normal
TaskManager manager = new TaskManager(
new TaskManagerPolicy(1,Environment.ProcessorCount,threadCount,0,ThreadPriority.AboveNormal));
Console.WriteLine("starte parallele Überprüfung. Verwende PFX Tasks.");
sw.Start();
// generische Liste vom Typ Task
// mit 253 Elementen initialisieren
List<Task> taskList = new List<Task>(253);
// hostAdress mit 1 initialisieren
int hostAdress = 1;
// true, bis hostAdress 254 erreicht
for (int i = 0; hostAdress < 255; i++)
{
// eine neue Instanz der Task-Klasse mit TaskManager
task = Task.Create( SimulateCheck, hostAdress, manager, TaskCreationOptions.None);
// den Task zur Taskliste hinzufügen.
// der Task wird sofort gestartet
taskList.Add(task);
// hostAdress erhöhen
hostAdress++;
}
// durch die taskList iterieren um zu warten bis alle
// Tasks beendet sind.
foreach (Task task in taskList)
{
task.Wait();
// prüfen ob der Task beendet ist
if (task.IsCompleted)
{
// das Task-Objekt entsorgen
task.Dispose();
}
}
// TaskManager entsorgen
manager.Dispose();
// benötigte Zeit abfragen
elapsedTime = sw.Elapsed;
sw.Reset();
Console.WriteLine("Benötigte Zeit: " + elapsedTime.ToString());
Console.WriteLine();
#endregion
// StringBuilder wird verwendet um das Ergebnis in einer Reihe auszugeben
StringBuilder sb = new StringBuilder(hashTable.Count);
Console.WriteLine("Thread-Verwendung:");
Console.WriteLine(string.Format("Anzahl der Threads erwartet {0}",
threadCount * Environment.ProcessorCount));
Console.WriteLine("Anzahl der verwendeten Threads: " + hashTable.Count);
Console.WriteLine();
Console.WriteLine("Verwendete ThreadIDs:");
foreach (DictionaryEntry de in hashTable)
{
sb.Append(de.Key.ToString()).Append(", ");
}
// letztes Komma und Leerzeichen entfernen
Console.WriteLine(sb.ToString().Trim(',', ' '));
Console.WriteLine();
Console.WriteLine("Zum beenden eine Taste drücken.");
Console.ReadKey();
}Der untere Bereich dient nur zur Ausgabe der Tests in der Konsole. Hier die Ausgabe der obigen Methode:

Tatsächlich wird, wie im obigen ScreenShot zu sehen, die TaskManagerPolicy umgesetzt und nur 72 Thread's werden verwendet obwohl 253 Task's erzeugt werden.
Tatsächlich ist das wirklich nützliche an diesem Verfahren, dass je nach Anwendungsfall die Anzahl der verwendetet Thread's gesteuert werden kann. In einer Windows-Anwendung kann die Anzahl hoch gesetzt werden, da die Anwendung mit Sicherheit im Vordergrund läuft und der Anwender auf das Ergebnis wartet. Soll die Lösung in einem Dienst Verwendung finden, kann sowohl die Anzahl als auch die Priorität der erzeugten Thread's dem entsprechend niedrig gewählt werden. Denkbar ist auch eine variable Größe der Thread-Anzahl und -Priorität, je nach momentaner Auslastung des Systems. Gerade in der Verwendung als Dienst könnte das eine sauber Lösung sein um die Ressourcen des Systems nicht über Gebühr zu belasten.
Soweit zum Grundgerüst und dem Test der parallelen Verarbeitung. Das nächste mal werde ich auf den eigentlichen LAN-Scan und das threadsichere einfügen bzw. ersetzen des Ping-Ergebnis in der ClientList-Klasse eingehen.
Wenn ihnen der Artikel gefallen hat oder er für sie hilfreich war, bitte "kicken" sie ihn.
