Samstag, 4. Juni 2011

Von Polling, futures und Paralleler Ausführung

Eines der großen Problemfelder im modernen Computerwesen ist Stromsparen. Es hat besonders Gewicht in tragbaren Geräten (Laptops, Tablets, Handhelds). Eine moderne CPU ist in der Lage in viele stromsparende Modi zu wechseln, wenn sie nichts zu tun hat. Je länger sie untätig ist, desto tiefer der stromsparende Modus und desto weniger Energie wird verbraucht und damit hält der Akku eines Geräts umso länger mit einer einfachen Aufladung.

Stromspar-Modi haben einen Feind: Polling. Weckt ein Prozess die CPU periodisch auf, auch für so etwas triviales wie das Lesen einer Speicherstelle, um eventuelle Änderungen zu erkennen, verlässt die CPU den Stromspar-Modus, weckt alle seine internen Strukturen und wird erst wieder in den Stromspar-Modus wechseln, wenn der kleine periodischer Prozess seine beabsichtigte Arbeit schon lange beendet hat. Intel selbst ist besorgt.

Python 3.2 kommt mit einem neuen Standardmodul, um nebenläufige Arbeiten anzustoßen und auf ihre Beendigung zu warten: concurrent.futures. Während ich den Code durchsah, bemerkte ich, dass es in manchen seiner Arbeitsthreads und -prozesse Polling nutzte. "Manche", weil sich die Implementierung von ThreadPoolExecutor und von ProcessPoolExecutor unterscheidet. Erste pollt in jedem seiner Arbeitsthreads, während die Zweite es nur in einem einzigen Thread, dem "queue management thread", tut, der zur Kommunikation zwischen den Arbeitsprozessen genutzt wird.

Polling wurde hier nur für eine Sache genutzt: Um zu erkennen, wann die Abschaltprozedur ausgeführt werden soll. Für andere Aufgaben, wie das Einreihen von Callables oder das Holen von Ergebnissen, vorher eingereihter Callables, werden synchronisierte Queue-Objekte benutzt. Diese Queue-Objekte kommen entweder aus dem threading- oder dem multiprocessing-Modul, je nachdem welche Executor-Implementierung genutzt wird.

Also entwickelte ich eine einfache Lösung: Ich ersetzte das Polling mit einem Sentinel, dem eingebauten Sentinel None. Bekommt eine Queue ein None, dann wacht ein wartender Arbeiter natürlicherweise auf und überprüft, ob er sich Abschalten sollte oder nicht. Im ProcessPoolExecutor gibt es eine kleine Komplikation, da man neben dem einen "queue managing thread" N Arbeitsprozesse aufwecken muss.

Im ersten Patch gab es immernoch einen Polling-Timeout, wenn auch einen sehr großen (10 Minuten), sodass die Arbeiter an irgendeinem Zeitpunkt aufwachen würden. Der große Timeout existierte, für den Fall, dass der Code fehlerhaft ist und keine Benachrichtigung zum Abschalten durch den schon genannten Sentinel verteilt würde. Aus Neugier tauchte ich in den multiprocessing-Code und machte eine weitere interessante Entdeckung: Unter Windows benutzt multiprocessing.Queue.get() , mit einem von Null verschiedenen, nicht-unendlichen Timeout, ... Polling (weshalb ich Issue 11668 öffnete). Es benutzt eine interessante hochfrequente Art des Pollings, da es mit einem Millisekunden Timeout beginnt, der mit jedem Durchgang erhöht wird.

Natürlich macht die Benutzung eines Timeouts, egal wie groß, meinen Patch unter Windows zwecklos, da die Art der Implementierung ein Aufwachen jede Millisekunde bedeutet. Also hab ich die bittere Pille geschluckt und den riesigen Timeout entfernt. Der letzte Patch kommt komplett ohne Timeout aus und sollte damit, egal auf welcher Plattform, kein periodisches Aufwachen verursachen.

Historisch gesprochen, nutzte vor Python 3.2 jede Timeout-Einrichtung des threading-Moduls - und damit ein Großteil von multiprocessing, da multiprocessing selbst Arbeitsthreads für verschiedene Aufgaben nutzt - Polling. Dies wurde mit Issue 7316 behoben.

Englische Version

Keine Kommentare:

Kommentar posten