TVTower
TVTower copied to clipboard
Threadsicherheit
Ich habe ja vorhin per Telefon was angesprochen ... und zwar ob die Mutexe "global" oder "local" sind...
in game.ai.base.bmx:
?threaded
Field _callLuaFunctionMutex:TMutex = CreateMutex() {nosave}
ist der Mutex ja "von BlitzMax NACH Lua", und soll vermeiden, dass mehrere threads (also multithreaded events oder sowas) gleichzeitig in EIN lua-Skript reinrufen koennen.
Wichtiger ist aber ja nun, dass die 4 KIs nicht zeitgleich auf "not thread-safe"-Funktionen zugreifen - und genau hier fehlts noch. Die Kommunikation LU->Blitzmax ist derzeit noch nicht mit einem Mutex abgesichert ... und das waere "per se" auch nicht noetig, wenn alle aufrufbaren Funktionen "threadsafe" waeren.
Man koennte nun erstmal in base.util.luaengine.bmx bei "_invoke()" einen LockMutex(XY) am Anfang und ein UnlockMutex(XY) am Ende ranpacken ... um zumindest das "von aussen" reingraetschen zu unterbinden. Besser waere es aber wohl, generell mehr Threadsicherheit herbeizufuehren ... auch wenn momentan nur die KI in einem anderen Thread arbeitet (die Musikengine auch, aber die ist glaube schon abgesichert)
Das Problem was wir dann aber immer noch haben ist, dass der "main thread" (die Spiellogik) ja nichts davon weiss, sie wuerde munter parallel in die Funktionen reinrufen Das Problem waere also nur ...verringert.
Wieviel bleibt denn vom Multi-Threading noch übrig, wenn in beide Richtungen synchronisiert wird?
das ist halt die Frage.
Ich hatte gerade mit davecamp einen Discordchat und dort folgende Idee aufgebracht:
base.util.luaengine.bmx _Invoke wird so aufgebaut, dass es temporaer nach Metadaten checkt: "THREADSAFED". Hat eine Methode diese Metadaten NICHT, loggt es das entsprechend.
Alle Log-Eintraege nach ein paar Tagen Testspiel sollten uns aufzeigen, welche Methoden "genutzt" wurden ... und ob die schon angepasst sind. Diese muessten dann eigene Mutexe bekommen und sich somit "schuetzen".
Spaeter koennte der Metadatencheck dann manuell zu aktivieren sein.
Ist vielleicht nicht perfekt, sollte aber einen kompletten Umbau ersparen.
Woher kommen denn die Metadaten? Oder heißt das, wir loggen einfach verwendete Methoden und passen die nach und nach an und schreiben die Metadaten von Hand dran? So was ähnliches habe ich gerade schon in naiver Form gemacht: log bei Start und Ende einer Invoke-Methode. Über große Teile sieht das sehr aufgeräumt aus - immer start und dann Ende. Und dann geht es plötzlich über eine Weile quer durcheinander.
locked OnTick
...
_Invoke() Func: TLuaFunctions.GetRoomBlockedTimeString
invocation done TLuaFunctions.GetRoomBlockedTimeString
_Invoke() Func: TLuaFunctions.TimeToMinutes
invocation done TLuaFunctions.TimeToMinutes
_Invoke() Func: TPlayer.SetAIStringData
invocation done TPlayer.SetAIStringData
locked_ InvokeO(n) TFunc: iTck
Player.SetAIStringData
invocation done TPlayer.SetAIStringData
_Invoke() Func: TLuaFunctions.GetDayMinute
_Invoke() Funcin:v oTcPlaation dyoenr.SetAeIStrin gTLuaFuDnata
ctions.GetDayMinute
invocation done TPlayer.SetAIStringData
_Invoke() Func: _TInvokPel() Funacyer.Se:t TALuaFunIcStringtDiaotan
invocation done TPlayesr.IsRo.oSmBelockedtAIStri
nginvoDcaattaion
Die Ausgaben sind print-Ausgaben von mir. Auffällig ist, dass das locked onTick nicht abgeschlossen war, bevor das nächste locked kam. Gerade läuft ein Dauerspiel auf dem Masterstand, der bislang nicht abgestürzt ist (Tag 27). In meinen Anpassungen des KI-Codes habe ich aber nichts finden können, was "seltsam" aussieht. Die Umbauten des Engine-Codes sollten ja auch nur dazu geführt haben, dass Paramter definiert werden müssen.
Ich werde die Ausgaben nochmal sowohl im Master als auch im Branch machen - im Master um zu sehen, ob es dort auch schon durcheinander geht.
Ja man koennte alle Methoden loggen ... oder aber sowas:
(TProgrammeLicence)
'override
Method GetTitle:string() {_exposeToLua, THREADSAFE}
if not title and GetData() then return GetData().GetTitle()
return Super.GetTitle()
End Method
Und in luaengine _invoke():
If not callable.HasMetaData("THREADSAFE") Then TLogger.Log("THREADSAFE", "Calling method or function " + funcOrMeth.name()+ " not marked threadsafe yet.", LOG_DEBUG)
Local t:Object
?Not bmxng
If func Then t = func.Invoke(obj, args)
?bmxng
If func Then t = func.Invoke(args)
?
Oder man markiert die Methoden, die Sachen "NICHT veraendern" (nur "read" von einfachen Zahlenwerten oder so und kann die somit "whitelisten" ("ungefaehrlich").
Die brauchen dann keinen Mutex - sind also per se "threadsafe", nur dass halt die Bedeutung schaerfer abgegrenzt ist.
Getter die änderbare Objekte zurückgeben könnten zwar thread-safe sein, nicht aber die Änderungen, die dann an diesen Objekten gemacht werden. Da würde dann auch ein Mutex nicht wirklich helfen, oder?
Richtig.
'not threadsafe
Method GetSum:int()
Local sum:Int
For local i:int = eachin self.arr
sum :+ i
Next
return sum
End Method
'not threadsafe
Method GetSum:int()
Local arrCopy:Int[] = self.arr[..] 'copy is not threadsafe
Local sum:Int
For local i:int = eachin arrCopy
sum :+ i
Next
return sum
End Method
'threadsafe
Method GetSum:int()
Return self.precalculatedSum
End Method
@davecamp suggested to think about message queues we could use to pass around requests and responses - fifo approach. This way only the queue handling should need mutexes.
The very same davecamp/col just explained to me that you can LockMutex(mutex)
multiple times in the same thread without requiring UnLockMutex(mutex)
inbetween - as long as the "unlocks" are called as often as the "locks".
This means if we protect eg "Field myArr:object[]` used inside of multiple interchained methods (Get() calling GetX() and both use "myArr" somehow) then we only need a "myArrMutex" not two mutexes (getMutex and getXMutex). This should keep amount of really required mutexes lower than I first feared.
Edit: adding an example (untested, just to show what I was talking about):
Type TBase
Field number:int
Field protectMe:Object[]
Field protectMeMutex:TMutex = CreateMutex()
Method Get:Int()
Local result:Int
LockMutex(protectMeMutex)
For local o:TBase = EachIn protectMe
result :+ GetX() + o.number
Next
UnlockMutex(protectMeMutex)
Return result
End Method
Method GetX:Int()
Return Millisecs()
End Method
End Type
Type TExt Extends TBase
Method GetX:Int() override
Local result:Int
'we do not know if we there was a mutex - was Get() called or raw GetX() here?
LockMutex(protectMeMutex)
For local o:TBase = EachIn protectMe
result :+ o.number
Next
UnlockMutex(protectMeMutex)
Return result
End Method
End Type
As you see - this allows to use a mutex "without" taking care of other methods in need to also lock the mutex. We could call myExt.GetX() ... but also myExt.Get()
Means we should make playercollections and schedule threadsafe ... also checking the TBroadcast / Audience predictor (albeit it should have improved already).
Ich bekomme den Segmentation Fault schon relativ früh in der Kette der Änderungen. Deshalb habe ich jetzt doch mal die LuaEngine-Änderungen zusammen mit den Lua-Aufrufänderungen (zusätzliche Parameter) separiert und keine KI-Performanceänderungen gemacht. Wenn der Segmentation Fault dann immer noch auftritt, sollte irgendwas an dem Engine-Refactoring problematisch sein.
Wie schon gesagt ... alles was von Lua "reinkommt" ist derzeit nicht threadsafe.
Also wenn Lua TVT.KaufeProgramm()
macht, wird derzeit nicht ueberprueft, ob die Spiellogik selbst auch gerade an der Liste der Lizenzen herumspielt.
Es kann da also zu Ueberlappungen und somit Problemen kommen.
Dies laesst sich ohne Refactoring (alles "von mehreren Threads benutzte" muss mit Mutexen geschuetzt werden) nicht beheben.
Neben dem "Lua -> BlitzMax" kann derzeit aber auch der gleichzeitige Zugriff mehrerer Lua-Instanzen (KI1 bis 4) auf ein BlitzMaxobjekt Probleme bereiten (KI1 sorgt fuers hinzufuegen waehrend KI2 auch hinzufuegt). Hier kann allerdings Abhilfe geschaffen werden, in dem der gleichzeitige Zugriff erstmal (bis zum obig beschriebenen "Refactoring") blockiert wird:
base.util.luaengine.bmx
Function Invoke:Int(fromLuaState:Byte Ptr)
Local engine:TLuaEngine = TLuaEngine.FindEngine(fromLuaState)
Return engine._Invoke()
End Function
wird zu
'mutex blocking multiple lua engines from simultaneously invoking
'functions or methods
global invokeMutex:TMutex = CreateMutex()
Function Invoke:Int(fromLuaState:Byte Ptr)
LockMutex(invokeMutex)
Local engine:TLuaEngine = TLuaEngine.FindEngine(fromLuaState)
Local result:Int = engine._Invoke()
UnlockMutex(invokeMutex)
Return result
End Function
Das ist jetzt an sich erstmal die ganz grobe Keule, da nun auch "sicherere" Aufrufe andere Aufrufe von Lua zu Blitzmax blockieren wuerden. Eine Funktion TVT.WarteFuerMich(10 Minuten)
wuerde nun auch andere KIs die 10 Minuten warten lassen.
Zumindest wenn in BlitzMax das so geschrieben waere (oder so):
Function WarteFuerMich(time:Int)
delay(time*60)
End Function
Ich denke weniger "blockierend" waere dann bei sowas der Event-Approach ...
TVT.WeckeMichAufIn(10 Minuten)
isActive = false
und die BlitzMax-Funktion wuerde eine Variable setzen, die dafuer sorgt, dass nach Minuten die LuaEngine einen Funktionsaufruf bekommt (also so ein "onWakeMeUp" gam event oder so).
Ich denke, hier fuehren einige Wege nach Rom, wir muessten nur die passenden raussuchen.
Bei einer höheren Vorspulgeschwindigkeit ist bei mir nun auch der Stand nach Lua-Engine-Änderungen ohne KI-Logik-Anpassungen mit Segmentation Fault abgestürzt. Ich denke neben der Threadsicherheit an sich sollte man auch noch einmal überlegen, ob LUA überhaupt auf die spielinternen Objekte zugreifen können sollte. Eigentlich sollten nur einfache Datenobjekte an die KI gegeben werden. Die schon vorhandenen Objekte zu nutzen macht es einem zwar bequem, aber öffnet eben auch die Tür für solche Probleme.
Ich weiß jetzt nicht, wie wir prinzipiell vorgehen wollen. Ich könnte meine aktuellen KI-Änderungen noch so anpassen, dass die Chance für einen parallelen Zugriff möglichst gering ist. Dann könnte man das in den Master übernehmen und parallel an den Themen KI-Verbesserung und Threadsicherheit weiterarbeiten.
Ich habe nochmal ein Master-Spiel mit hoher Geschwindigkeit laufen lassen - auch hier Absturz mit Segmentation Fault. Es gab also auch vor dem ganzen Umbau schon ein Problem (bei mir und einem Forumsspieler gab es auch in regulären Spielen schon Abstürze). Insofern halte ich es für gerechtfertigt, den KI-Branch (mit Verbesserungen bezüglich der Zugriffe) zu mergen.
Die grob(/ß)e Keule schwingt aber auch in einigen Fällen daneben. Zwar warten die KIs dann aufeinander, aber KI und Spiellogik können weiterhin gleichzeitig auf dasselbe Objekt zugreifen. Das lässt sich auch mit Logging der aufgerufenen KI-Methoden nur bedingt ermitteln, denn der problematische Zugriff kann ja tief versteckt liegen (getter, der weit drinnen Berechnungen auf geteilten Objekten macht).
Ja es waere erstmal nur eine grobe Keule, die das meiste erstmal verhindern koennte. Es passiert ja so schon selten (muss halt kollidieren).
Gemergt werden kann da gerne...
Die KI bekommt ja an sich nur die Objekte um dort bequem auf GetTitle usw zugreifen zu koennen. Fuer wichtige Interaktionen muss sie ja die TVT:-Methoden nutzen
Das Problem da haben wir ja bereits erkannt: lua gteift aus seinen Threads auf die Sachen zu..waehrend der Mainthread das auch macht.
Ergo muessen alle Daten, die Lua "irgendwie" nutzt..abgesichert werden.
Das wird im Groben und Ganzen alle Spielobjekte betreffen.. da wir aber ja oft ueberpruefen ob eine Lizenz auch wirklich nicht null ist...sollte es an den Stellen kein Problem geben. Allerdings sollten listen, maps...trotzdem geschuetzt werden damit beim iterieren nichts an der Liste von "extern" geaendert werden kann...bzw andersherum.
Siehe auch #485
Mein subjektives Gefühl ist, dass die Anzahl der Abstürze zurückgegangen ist. Besonders seit die Anzeige zum Garbagecollector entfallen ist. Früher konnte ich auf meinem Entwicklungsrechner (Linux) bei Tempo 1000 kein KI-Spiel machen, weil sonst nach 2 bis 4 Spieltagen ein Absturz kam. Jetzt geht es manchmal bis Tag 35... Es ist aus den Logs aber weiterhin nicht für mich ersichtlich, was die Ursache sein könnte.
Lässt sich steuern, wieviel Speicher das Spiel zur Verfügung hat? (Simulation Rechner mit wenig Speicher - häufigere segmentation faults...)
Es gibt möglicherweise eine neue Spur. Wenn ich beim Schnellspiel die Maus im Chatbereich parke, laufen die Spiele (mutmaßlich) deutlich länger ohne Absturz. Möglicherweise kommen die Segmentation-Faults (zumindest beim Vorspulen) durch Methoden, die bei Mausposition im Spielbereich nicht schnell genug fertig werden (Raum schon wieder verlassen, Objekte weg?).
(ein Spiel am Tag 100 unterbrochen, zweites Spiel bei Tag 50 ohne Absturz; zuvor habe ich den Mauszeiger immer links knapp oberhalb des Fernsehers stehen lassen)
War vor dem Release noch ein Zaubercommit dabei? Zumindest gab es bei den (noch nicht ganz so zahlreichen Testspielen) noch keinen einzigen Absturz wegen Segmentation-Fault... Und in dem Moment, wo ich die Nachricht abschicken will, ist es dann passiert. Es wäre ja auch zu schön gewesen.
Das wird ne Sache vom Betriebssystem sein.
Aber hier wird es ne um den RAM gehen..eher was anderes.
Ich würde dieses allgemeine Ticket gern zumachen. Es ist in den neueren verlinkt und durch die bereits durchgeführten Anpassungen scheint sich die Anzahl der Abstürze bei stark gestiegener, möglicher Geschwindigkeit schon gesunken zu sein.