Multithreaded Server - Design Guidelines?



  • @Jodocus

    Jodocus schrieb:

    so ziemlich jedes wird dann einen Großteil der Zeit beim Kontextwechsel verbraten.

    Kontextwechsel sind aber gar nicht das Problem.
    Das "Problem" hierbei ist dass der Scheduler jedem aktiven Thread gleich viel Zeit zuweist - statt jedem Prozess (bzw. Prozessgruppe) mit aktiven Threads gleich viel Zeit zuzuweisen.

    Es gibt allerdings auch Scheduler die letzteres machen - mit denen kann man dann ganz normal weiterarbeiten während der Server mit den 10K+ Threads läuft.



  • Ich hab jetzt einen Ansatz gefunden, allerdings ist er suboptimal. Ich koennte pro Welt einen io_service verwenden und in den WorldScheduler ein .poll() als repeated Task reinwerfen. Problem dabei: mit asio's socket funktioniert das nicht, weil man da den io_service nicht neu zuweisen kann, daher muss ich einen posix::stream_descriptor verwenden und den accept() Code mit dem BSD-API selbst schreiben.

    Hat hier jemand ne bessere Idee?



  • Sind die paar tausend Nachrichten pro Sekunde wirklich relevant? Parallelisierung würde sich erst dann lohnen, wenn du die Kapazität von Asio überschreiten würdest.

    Finde heraus, was wirklich langsam ist. Das ist vermutlich die Kernlogik des Spiels mit ihren vielen Suchen in den Datenstrukturen für die Spielwelt. Ich kann mir vorstellen, dass das Original da relativ naiv arbeitet und auch wegen Java nicht auf Cache optimiert ist. Diese Suchen kann man möglicherweise auch parallel durchführen, indem man die Welt auf Prozessorkerne aufteilt.



  • Es geht nicht darum, IO zu parallelisieren, sondern die Simulation. Dass die Clients dem jeweiligen Thread ihrer Welt zugeteilt werden, ist nur eine Folge davon.



  • @Kellerautomat
    Hast du dir mal die ASIO Strands angesehen?



  • strands bringen mir nichts.

    Edit: Vielleicht kann man damit doch was machen, aber schoen ist anders.



  • Kellerautomat schrieb:

    Ich hab kein Problem mit Automaten. Ich muss so oder so Event-Handling irgendwie bauen, da duerfte das relativ egal sein.

    Ich fahre sowas von ab auf kleine Funktionen und Destruktoren und RAII und Exceptions…
    Erzeuge ich einen LoginDialog, dann wird er beim Kunden auch sichtbar. Wenn ich den LoginDialog warumauchimmer delete, dann verschwindet der auch. Erkenne ich einen cheat, dann werfe ich eine Exception. Ganz normales Stack-Unwinding kommt. Der hochgehobene Gegenstand im Rollenspiel verliert seinen Griff/Lock, mit dem ich ihn anpackte (und davor schütze, daß ein Anderer ihn hochebt), und plumpst runter. Ich muss nicht mehr wie beim Automaten jede Zustandsändernde Aktion wie Objektaufheben/Zauberbuchöffnen/Handelsdialogöffnen/Holzhackenbeginnen(spiel Hack-Geräusche bis HHEnde) sauber in jede exit()-ähnliche Sache(CheatErkannt/Quit/Weltwechsel) einpflegen.

    ThreadPerClient zu Threadpool ist wie C++ zu C.
    (Türlich soll man bei einfachen Aufgaben das einfache Werkzeug benutzen, die Lib ist ja umschaltbar zwischen ThreadPerClient und Threadpool und in C++ muss man um 3 Zahlen zu sortieren keine Klassen benutzen.)

    Könnte mir vorstellen, daß sauviele Cheats mit RAII auf dem Server gar nicht möglich wären.

    Wenn mir die 30000 Threads ausgehen, hab ich ja schon 30000 Spieler und ausgesorgt. Im nächsten Spiel plane ich voraus und die Spielerthreads greifen nicht direkt mit mutex/lock->map auf die Weltdaten zu, sondern kommunizieren per Socket/Pipe mit dem Weltserver (fühlt sich aber genauso nach mutex/lock->map an). Und wenns dann wieder mehr als 30000 Kunden gibt, wird umgeschaltet auf Proxyrechner vor dem Weltrechner. 30000 Proxys vor dem Weltrechner, die je 30000 Spieler packen, das reicht erstmal.



  • Ich will auch mal ØMQ in die Runde geworfen haben:
    http://zeromq.org/
    WEIT davon entfernt eine Magic Bullet für dein Problem zu sein, aber definitiv eine sehr interessante Library.



  • Ich hab gehoert, dass zeromq keine ordentlich cancelbare IO hat oder so aehnlich. Irgendwas war damit, was die Lib so ziemlich unbrauchbar macht.



  • Wo ist eigentlich das Problem? Du hast einen Thread-Pool mit I/O-Workern und einen Thread-Pool für deine jeweiligen Welten (also jede Welt sein Thread).
    Jeder Welt-Thread ist mit einer Message-Queue mit den I/O-Threads verbunden, also eine 1-zu-1 Welt <--> Queue-Assoziation. Du assoziierst außerdem beim Verbindungsaufbau mit einem Client jedem Socket eine Welt (und damit auch die korrespondierende Queue) zu. Kommt jetzt eine Nachricht im I/O-Thread auf einem bestimmten Socket an, suchst du dir die Queue heraus und pushst die Nachricht. Der Welt-Thread wacht auf, wenn er merkt, dass was in der Queue ist und arbeitet die Message ab (evtl. sogar einfach lock-free zu gestalten, wenn nur 1 Thread pusht und ein andere popt).
    Wenn in der Welt auch dann was passieren soll, wenn gerad kein Socket was gesendet hat, pushst du einfach selber von einem Scheduler-Thread (oder gar vom I/O-Thread) aus Nachrichten in die verschiedenen Welt-Queues, damit die dann irgendwas machen.
    Das klingt für mich nach einfachem EVA-Prinzip.

    hustbaer schrieb:

    Kontextwechsel sind aber gar nicht das Problem.
    Das "Problem" hierbei ist dass der Scheduler jedem aktiven Thread gleich viel Zeit zuweist - statt jedem Prozess (bzw. Prozessgruppe) mit aktiven Threads gleich viel Zeit zuzuweisen.

    Aber wenn nun jedem Thread gleichviel Zeit zugewiesen wird, wird diese jeweils zugewiesene Zeitspanne doch auch mit wachsener Thread-Zahl immer kürzer (damit alle aktiven Threads mal dran sind); Bei gleichzeitig konstant bleibender Zeit für den context swap wird doch dann netto im Verhältnis zur Ausführungszeit eines Threads viel mehr Zeit mit dem swappen zugebracht, oder nicht? Alternativ wäre nur, ebenfalls die zugewiesenen Zeiten zu verlängern, mit dem Preis, dass manche Threads ewig unterbrochen sind. In beiden Fällen klingt das für mich in Sachen Skalierbarkeit katastrophal.


Anmelden zum Antworten