Um zu implementieren "Arbeit stehlen" ist nicht schwer in der Theorie. Sie benötigen eine Reihe von Warteschlangen, die Aufgaben enthalten, die funktionieren, indem Sie eine Kombination aus Computing und anderen Aufgaben erstellen, um mehr Arbeit zu erledigen. Und Sie benötigen atomaren Zugriff auf die Warteschlangen, um neu generierte Aufgaben in diese Warteschlangen zu platzieren.Schließlich benötigen Sie eine Prozedur, die jede Task am Ende aufruft, um mehr Arbeit für den Thread zu finden, der die Aufgabe ausgeführt hat. Diese Prozedur muss in Arbeitswarteschlangen suchen, um Arbeit zu finden.
Die meisten solchen Work-Stealing-Systeme machen die Annahme, dass es eine kleine Anzahl von Threads gibt (in der Regel durch echte Prozessorkerne gesichert), und dass es pro Thread genau eine Arbeitswarteschlange gibt. Dann versuchen Sie zuerst, Arbeit aus Ihrer eigenen Warteschlange zu stehlen, und wenn es leer ist, versuchen Sie, von anderen zu stehlen. Was schwierig wird, ist zu wissen, in welche Warteschlangen man schauen muss; sie für die Arbeit seriell zu scannen, ist ziemlich teuer und kann eine große Menge an Konflikten zwischen Threads erzeugen, die nach Arbeit suchen.
Bis jetzt ist dies alles ziemlich generische Zeug mit einer zwei wichtigen Ausnahmen: 1) Schaltkontexte (z. B. Festlegen von Prozessor Kontextregistern wie einem "Stapel") kann nicht in reinem C oder C++ angegeben werden. Sie können dies beheben, indem Sie zustimmen, einen Teil Ihres Pakets in zielplattformspezifischen Maschinencode zu schreiben. 2) Der atomare Zugriff auf die Warteschlangen für einen Multiprozessor kann nicht rein in C oder C++ erfolgen (der Dekker-Algorithmus wird ignoriert), und daher müssen Sie diejenigen codieren, die Assembler wie das X86 LOCK XCH oder Compare and Swap verwenden. Nun, der Code in der Aktualisierung der Warteschlange beteiligt, sobald Sie einen sicheren Zugriff haben, ist nicht sehr komplex, und Sie könnten leicht schreiben, dass in ein paar Zeilen von C.
Allerdings denke ich, dass Sie finden wird, ist der Versuch, solche zu codieren Ein Paket in C und C++ mit gemischtem Assembler ist immer noch ziemlich ineffizient und Sie werden schließlich das gesamte Ding in Assembler codieren. Alles was übrig bleibt, sind C/C++ - kompatible Einstiegspunkte: -}
Ich habe dies für unsere parallele Programmiersprache PARLANSE getan, die die Idee einer beliebig großen Anzahl von parallelen Berechnungen live und interagierend (synchronisierend) zu jedem Zeitpunkt bietet. Es ist hinter den Kulissen auf einem X86 genau mit einem Thread pro CPU implementiert, und die Implementierung ist komplett in Assembler. Der Arbeitsstehlingcode ist wahrscheinlich 1000 Zeilen insgesamt und sein kniffliger Code, weil er im Nichtkonkurrenzfall extrem schnell sein soll.
Der echte Wermutstropfen für C und C++ ist, wenn Sie eine Aufgabe erstellen, die Arbeit darstellt, wie viel Stapelspeicherplatz Sie zuweisen? Serielle C/C++ - Programme vermeiden diese Frage, indem sie einfach riesige Mengen (z. B. 10 MB) linearer Stapel überlagern, und niemand kümmert sich sehr darum, wieviel von diesem Stapelspeicherplatz verschwendet wird. Aber wenn Sie Tausende von Aufgaben erstellen können und sie alle zu einem bestimmten Zeitpunkt live haben, können Sie nicht vernünftigerweise jeweils 10 MB zuweisen. Jetzt müssen Sie entweder statisch bestimmen, wie viel Stack-Speicherplatz eine Aufgabe benötigt (Turing-hard), oder Sie müssen Stack-Chunks zuweisen (z. B. pro Funktionsaufruf), was von den weit verbreiteten C/C++ - Compilern nicht getan wird (zB die, die Sie wahrscheinlich verwenden). Der letzte Ausweg ist, die Aufgabenerstellung zu drosseln, um sie auf einige hundert zu begrenzen, und einige hundert wirklich große Stapel zu den Aufgaben, die live sind, zu multiplexen. Sie können die letzten nicht ausführen, wenn die Tasks den Status "Sperren"/"Aussetzen" aktivieren können, da Sie auf Ihren Schwellenwert stoßen. Sie können das also nur tun, wenn die Tasks nur berechnen. Das scheint eine ziemlich strenge Einschränkung zu sein.
Für PARLANSE haben wir einen Compiler erstellt, der Aktivierungsaufzeichnungen für jeden Funktionsaufruf auf dem Heap zuweist.
TBB ist viel massiver und komplexer für meine Bedürfnisse. Ich bin auf der Suche nach einer viel einfacheren, "dedizierten" Implementierung ... wenn es eine gibt –