Unklarheiten/Fragen zum asynchronen Programmieren (IP-Scanner)

  • C#
  • .NET (FX) 4.5–4.8

Es gibt 11 Antworten in diesem Thema. Der letzte Beitrag () ist von Radinator.

    Unklarheiten/Fragen zum asynchronen Programmieren (IP-Scanner)

    Hallo Community,
    ich hoffe das hier ist das richtige Unterforum, ansonsten bitte verschieben.

    Ich versuche einen asynchronen IP-Scanner zu programmieren und Frage mich nach einiger Recherche ob mein Vorgehen richtig ist. Ich habe das ganze vor einiger Zeit mal in PowerShell geschrieben (github.com/BornToBeRoot/PowerShell_IPv4NetworkScanner), hier jedoch mit einem RunspacePool und ohne UI :)

    Der Benutzer hat die Möglichkeit eine IP-Adresse/Range/Subnetz einzugeben. Also z.B. 192.168.178.0/24... Davon erzeuge ich einen List, welche ich zu einem Array konvertiere da dieses ja schneller verarbeitet wird.

    Danach starte ich einen Task (damit der UI Thread nicht blockiert wird) und arbeite mittels einer Parallel.Foreach() die IP-Adressen ab. Da Parallel.Foreach() mit der TPL arbeitet und auf den zugrundeliegenden ThreadPool zurückgreift (korrigiert mich bitte wenn ich hier falsch liege) habe ich die min / max Anzahl der gleichen Thread auf 256 (/24) erhöht.

    Das Ergebis "IPScanResult" ist eine ObservableCollection die an ein DataGrid gebunden ist. Weil die ObseravleCollection nicht Thread Safe ist, habe ich 2 Möglichkeiten gefunden:
    1) Mit Hilfe eines Lock Objects (This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread.)
    2) oder mit dem Dispatcher (wie unten im Code gezeigt)

    Seit .NET 4.5 gibt es ja auch noch den ConcurrentBag, welcher ThreadSafe ist aber von Haus aus kein INotifyPropertyChange unterstützt.

    C#-Quellcode

    1. IPAddress[] ipAddresses = (await IPScanRangeHelper.ConvertIPRangeStringToListAsync(IPRange)).ToArray();
    2. ThreadPool.SetMinThreads(256, 256);
    3. ThreadPool.SetMaxThreads(256, 256);
    4. await Task.Run(() => Parallel.ForEach(ipAddresses, new ParallelOptions() { MaxDegreeOfParallelism = 256 }, ip =>
    5. {
    6. Ping ping = new Ping();
    7. PingReply reply = ping.Send(ip);
    8. if (reply.Status == IPStatus.Success)
    9. {
    10. // Add the items
    11. Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() => IPScanResult.Add(new IPScanInfo(ip, reply.Status))));
    12. }
    13. }));


    Ein Scan dauert gefühlt 4-5 Sekunden (ping timeout usw...) für ein /24 Subnetz.Was ich sehr akzeptabel finde.

    Würded ihr den Code so lassen? Was könnte man optimieren?

    Das Project / den kompletten Code findet ihr auch hier: github.com/BornToBeRoot/NETworkManager/tree/master/Source

    Danke und viele Grüße :thumbsup:
    NETworkManager - A powerful tool for managing networks and troubleshoot network problems!

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „BornToBeRoot“ ()

    etwas unschön ist, dass für jeden einzelnen Ping ein Thread-Transfer erfolgt.
    Dass liesse sich tatsächlich mit einer ConcurrentQueue oder sowas entkoppeln.
    Also dass zB nur alle 300ms Daten ins Gui geleitet werden, dann aber jeweils ein ordentlicher Batzen (die Queue entleeren).

    Aber ist viel Arbeit und lohnt wohl kaum.
    4s Scan-Zeit sind doch auszuhalten.
    Also ich habe meinen Code etwas optimiert, nachdem ich diesen Artikel gelesen habe: albahari.com/threading/

    Da Task.Run() auch einen Thread des ThreadPools beansprucht, habe ich mit dem Beispiel oben nur 255 Threads frei (es werden aber 2^8 = 256 Threads benötigt für eine optimale performance). Mein jetziger Code addiert zu den bestehenden Threads im ThreadPool die erforderlichen und begrenzt dann in den ParallelOptions die gleichzeitig ausgeführten Threads nochmal. Nach dem Scan setzte ich den ThreadPool wieder auf den Ursprungszustand.

    C#-Quellcode

    1. ConcurrentBag<IPAddress> ipAddressBag = await IPScanRangeHelper.ConvertIPRangeToBagAsync(IPRange);
    2. int workerThreads;
    3. int completionPortThreads;
    4. ThreadPool.GetMinThreads(out workerThreads, out completionPortThreads);
    5. ThreadPool.SetMinThreads(workerThreads + ConcurrentThreads, completionPortThreads + ConcurrentThreads);
    6. _cancellationTokenSource = new CancellationTokenSource();
    7. await Task.Run(new Action(delegate ()
    8. {
    9. try
    10. {
    11. ParallelOptions parallelOptions = new ParallelOptions();
    12. parallelOptions.CancellationToken = _cancellationTokenSource.Token;
    13. parallelOptions.MaxDegreeOfParallelism = ConcurrentThreads;
    14. Parallel.ForEach(ipAddressBag, parallelOptions, ip =>
    15. {
    16. // Scan
    17. IPScannerInfo ipScannerInfo = IPScanner.ScanIP(ip, Timeout, new byte[Buffer], PingAttempts, ResolveHostname, GetMACAddressFromARP);
    18. // UI
    19. Application.Current.Dispatcher.BeginInvoke(new Action(delegate ()
    20. {
    21. ProgressBarValue++;
    22. if (ipScannerInfo.PingInfo.Status == IPStatus.Success)
    23. IPScanResult.Add(ipScannerInfo);
    24. }));
    25. });
    26. }
    27. catch (OperationCanceledException) // Cancel token
    28. {
    29. }
    30. }));
    31. ThreadPool.SetMinThreads(workerThreads, completionPortThreads);


    Edit: Unter Labor Bedingungen (500ms timeout im LAN) dauert ein Scan ~1 Sekunde, in realen Umgebungen (2 Pings, 4s Timeout) ~10 Sekunden
    NETworkManager - A powerful tool for managing networks and troubleshoot network problems!
    Standardmäßig sind die MinThreads bei mir auf 4 eingestellt (Das variiert anscheinend je nach CPU / .NET Version). Der ThreadPool soll ja je nach auszuführendem Code die Threads selbst anpassen. Dies wird aber erst im Verlauf des Scnns optimiert. Wenn ich die MinThreads nicht anpasse, dauert der Scan ~ 60-90 Sekunden (Timeout 2000 MS).

    In wenigen seltenen Fällen kann es also hilfreich sein in den ThreadPool einzugreifen und die MinThreads hochzusetzten.

    Wenn ich die MinThreads im ThreadPool auf 256 setzte, aber nur mit 255 Threads scannen kann (weil Thread.Run() ja einen belegt) und mal angenommen in einem Subnetz keine IP Antwortet, müsste 1 Thread der Parallel.ForEach zweimal den Timeout (2 Pings a 4000 Sekunden) abwarten.

    Im schlechtesten Fall würde mein Scan (Code im ersten Beitrag) also 16 Sekunden dauern, so nur maximal 8. Natürlich immer vorausgesetzt man scannt /24.
    NETworkManager - A powerful tool for managing networks and troubleshoot network problems!
    das sind theoretische Überlegungen, aber keine praktischen Tests.

    Interessant ist doch nur der Vergleich der Codes aus Post#1 und aus post#3

    Ist der kompliziertere Code wirklich so viel schneller, dasses sich lohnt?
    Und - wie gesagt - wie testest du das - derlei Tests sind nämlich nicht so einfach, weil man garnet weiß, wo wann was gecacht wird.
    Mal gerade mit einer StopWatch gemassen. Die Zeit verdoppelt sich in einen leeren Subnetz, wenn die Thread weniger als 256 sind. Ohne MinThreads sogar auf ~ 2 Minuten

    Edit: ich hab natürlich nicht getestet welche ip in welchem thread läuft. aber 2x so schnell reicht mir als ergebnis. klar wenn ich jetzt 2000threads laufen lass wirds wohl langsamer weilndie cpu zwischen den threads switchen muss.
    NETworkManager - A powerful tool for managing networks and troubleshoot network problems!

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „BornToBeRoot“ ()

    Jahaa! MinThreads zu setzen verbessert das Ansprechverhalten des ThreadPools.

    Aber das tust du ja in beiden Codes.
    Ist das iwie schwer verständlich, das Anliegen, den Code von post#1 mit dem aus post#3 zu vergleichen?

    Warum redest du immer vonne MinThreads - die doch in beiden Codes gleich behandelt werden?
    im zweiten code addiere ich aber noch die bereits existierenden minthreads dazu.

    wenn ich die fest auf 256 setzt würde, könnten ja z.b 2 andere bereits vergeben sein. der code in #2 macht also:

    GetMinThreads(out x1, out x2)
    SetMinThreads(x1 + 256, x2 + 256 )

    Der Flaschenhals wären beim ersten Code also 2-3 die nicht frei wären, wenn ich sie benötige.

    Sehe ich das falsch?

    Edit: Damit die Parallel.ForEach sich aber nicht mehr Threads nimmt z.B. 258 begrenze ich die mit ParallelOptions. Zudem ist im zweiten code noch ein cancel token gesetzt zum abbrechen
    NETworkManager - A powerful tool for managing networks and troubleshoot network problems!

    BornToBeRoot schrieb:

    Seit .NET 4.5 gibt es ja auch noch den ConcurrentBag, welcher ThreadSafe ist aber von Haus aus kein INotifyPropertyChange unterstützt.
    Warum baust du dir nicht einfach ein ViewModel (ich geh mal davon aus, da du Application.Curren.Dispatcher verwendest, dass es sich um eine WPF App handelt), in welchem du eine Property "veräußerst, welche vom Typ einer Klasse, ist, die du dir selber geschrieben hast, in der du ConcurrentBag beerbst und INotifyPropertyChanged implementierst? Dann wäre zum einen die Threadsafenessproblemeatik (jaaa, ich weiß: tolles Denglisch ;D) und zum anderen das mit dem "Ans GUI melden, die Collection hat sich geändert" erschlagen.
    In general (across programming languages), a pointer is a number that represents a physical location in memory. A nullpointer is (almost always) one that points to 0, and is widely recognized as "not pointing to anything". Since systems have different amounts of supported memory, it doesn't always take the same number of bytes to hold that number, so we call a "native size integer" one that can hold a pointer on any particular system. - Sam Harwell