Ein paar Fragen zu Thread Pools und IOCPs



  • Hallo.

    Ich habe so allerlei über I/O Completion Ports und Thread-Pools gelesen und habe ein paar Fragen.

    1. Es wird an verschiedenen Stellen behauptet, dass man einen Pool aus I/O-Threads und einen aus non-I/O-Threads benötigt. Der Grund dafür lautet, dass ein I/O-Thread nicht blockieren darf, ansonsten würde er nicht mehr zeitnah GetQueuedCompletionStatus aufrufen, sodass die Warteschlange immer größer wird. Die Lösung soll dann sein, dass ein I/O-Thread nur asynchron die Verarbeitung übernimmt, d.h. er delegiert die Arbeit an einen Thread im non-I/O-Pool, sodass er selbst nie blockiert und schnell wieder bei GetQueuedCompletionStatus ist. Dafür benutzt er dann z.B. einen zweiten IOCP und PostQueuedCompletionStatus .
      Das klingt für mich ziemlich funky - bzw. m.E. löst es doch gar nicht das Problem?! Dann läuft eben nicht die erste, sondern die zweite Warteschlange voll, wenn alle non-I/O-Threads zu langsam sind/zu lange blockieren.

    Die Lösung sollte doch sein, dass immer genug Threads frei sind, um Arbeit zu erledigen, falls andere Threads blockieren. Nur um klarzustellen: Man gibt, während man CreateIoCompletionPort aufruft, als Concurrency-Wert idealerweise die Anzahl an Hardware-Threads an (weniger, wenn man nicht die volle Power will, niemals mehr, da sonst nur mehr Context-Switches und kleinere Time-Slices anfallen). Wenn mehr Threads auf GetQueuedCompletionStatus warten als der angegebene Concurrency-Wert, dann werden die überschüssigen inaktiven Threads so lange blockieren, bis einer der aktiven Worker-Threads tatsächlich während der Arbeit blockiert, sofern kein anderer der designierten Threads gerade auf GetQueuedCompletionStatus auf Arbeit wartet. Man muss also einfach darauf achten, dass der Pool freier Threads immer groß genug ist. Wozu braucht man jetzt noch die Unterscheidung von I/O-Threads und non-I/O bzw. Worker-Threads? Ein Pool reicht doch aus, solange er groß genug ist. Ich würde sogar erwarten, dass dieses Zwei-IOCP-Modell richtig lahmarschig ist, da durch das zusätzliche PostQueuedCompletionStatus auf jeden Fall ein Extra-Kontextwechsel für jede Completion stattfindet. Benutzt man FILE_SKIP_COMPLETION_PORT_ON_SUCCESS , kann man eine Completion mit 0 bis 1 Kontextwechseln bearbeiten (bei einem IOCP), bei zweien wäre man dann schon bei 1-2 pro Completion.

    1. Die einzigen anderen Gründe, die ich fand, warum man zwei Thread-Pools haben könnte, lauten wie folgt:

    Wenn man seinen Thread-Pool je nach Auslastung dynamisch wachsen/schrumpfen lassen möchte, so könnte ein Thread beendet werden, der eine asynchrone Operation gestartet hat. Auf Systemen vor Vista soll das dazu führen, dass die Operation gecancelt wird. Hat man hingegen einen reinen I/O-Pool, in dem nur asynchrone Operationen gestartet werden, so wird der auch nie blockieren, sodass er eine feste Größe haben kann, während man den Pool an non-I/O-Threads gefahrlos wachsen und schrumpfen lassen kann, da sie nie direkt I/O initiieren.

    Um jetzt messen zu können, wie voll die Warteschlange ist, kann man einfach etwas Statistik machen und die durchschnittliche Dauer zwischen einem PostQueuedCompletionStatus eines I/O-Threads und dem zugehörigen GetQueuedCompletionStatus eines Worker-Threads ausrechnen und den Pool vergrößern/verkleinern, bis die Zeit innerhalb von gewünschten Grenzen liegt. Aber was mache ich, wenn ich nur einen Pool habe, also niemals PostQueuedCompletionStatus aufrufe? In dem Falle kann ich die Zeit nicht mehr messen, es gibt auch keine API-Funktion, um mitzubekommen, wie voll die Warteschlange ist. Ich könnte alle bis auf eine minimale Anzahl an Threads beenden, sofern sie aus GetQueuedCompletionStatus mit einem Timeout zurückkehren. Aber wie weiß ich, wann ich den Pool vergrößern muss? Etwa, wenn man ein mal mit GetQueuedCompletionStatus pollt, bevor man einen mit INFINITE macht? Nach dem Motto, wenn man beim ersten Versuch kein Timeout hat, war schon was in der Warteschlange, also ist da potentiell ein Stau, sodass mehr Threads ran müssen.

    1. Letzte, vielleicht wichtigste Frage: Es das Spektakel um die Größe des Thread-Pools überhaupt wichtig? Ich meine, man muss dem Thread-Pool sowieso eine obere Schranke geben. Gibt es einen guten Grund, nicht gleich die Höchstzahl an Threads zu erzeugen und mit GetQueuedCompletionStatus schlafen zu legen? Der IOCP garantiert mir ja, dass sie nur dann Zeit bekommen, wenn die anderen Threads blockieren, also kosten sie doch eigentlich nur wenig. Ich las immer, dass man einen Thread-Pool nicht zu groß haben will, da er nur irgendwelche nützlichen Ressourcen/non-paged Pool frisst, auch wenn er keine Zeit stielt. Ist das denkbar?

    Ich würde mich sehr freuen, wenn jemand dies gelesen und ein paar nette Anregungen/Antworten hätte. 🙂



  • Irgendwie klingt das wie Du das beschreibst ja seltsam, dass man den Pool der Threads die die IO Completion Packets empfangen auf die Anzahl der logischen Threads im System begrenzen, aber die empfangenen Daten dann an weitere Threads weiterreichen soll. Das macht nämlich mehr Kontextwechsel als würde man gleich so viele Threads am IOCP-Port lauschen lassen wie nötig sind um die Daten dann gleich in den selben weiter zu verarbeiten.
    Eine Queue wird übrigens so gut wie nie voll wenn man z.B. std::list verwendet.


Log in to reply