Der Messenger in SeeingSharp

In Vorbereitung für ein neues Spiel habe ich heute die Kern-Logik von Seeing# an ein paar Stellen erweitert. Wichtigster Punkt dieser Tage ist der Messenger, welcher in den Klassen von Seeing# als SeeingSharpMessenger bezeichnet wird. Ursprünglich habe ich mich bei dem Konzept etwas vom EventAggregator des Prism-Frameworks inspirieren lassen [1]. Grundsätzlich geht es darum, eine gemeinsame Klasse zu haben, an der sich Ereignis-Empfänger registrieren können, um so Ereignisse von beliebigen Quellen der Anwendung zu empfangen, ohne diese Quellen selbst zu kennen. Bei Seeing# habe ich dieses im Grunde sehr einfache Prinzip hauptsächlich dahingehend erweitert, damit es besser mit verschiedenen Threads innerhalb des Programms umgehen kann.

In jedem Programm mit Benutzeroberfläche gibt es einen UI-Thread, welcher technisch nichts anderes als eine Endlosschleife ist. Wird ein Ereignis empfangen, so kann der UI-Thread in eine eigens dafür bereitgestellte Methode springen und man hat völlige Freiheit, innerhalb der Oberfläche zu manipuliere n. Seeing# geht davon aus, dass es neben dem UI-Thread auch andere solcher Threads gibt. Bestes Beispiel ist hier der Game-Thread, welcher vollständig auf Basis der Update-Logik der 3D-Engine läuft – und diese wird im Hintergrund und eben nicht über den UI-Thread ausgeführt. Grundsätzlich ist der Game-Thread aber auch nichts anderes als eine Hauptschleife. Die Spielelogik wird laufend aktualisiert, bei Ereignissen werden hier und da diverse Logiken ausgeführt und so weiter… nichts Besonderes eben. Bei Seeing# habe ich mich dazu entschieden, jedem dieser Threads einen eigenen Messenger zuzuweisen. Bei Ereignissen innerhalb eines der Threads werden zunächst nur die Listener aufgerufen, welche sich am gleichen Messenger registriert haben – wohlwissend, dass Aufrufe im zugehörigen Thread aufgerufen werden.

Ein Beispiel: Wird nachfolgender Code innerhalb des Game-Threads aufgerufen, werden entsprechend alle Listener benachrichtigt, welche sich auch über den Messenger des Game-Threads registriert haben. Weiterhin stellt der Messenger sicher, dass das Publish selbst bereits vom richtigen Thread aufgerufen wird. Andernfalls wird eine Exception geschmissen, um so auf Threading-Fehler hinzuweisen.

 /// <summary>
 /// Initializes the game in an async way.
 /// </summary>
 public async Task InitializeAsync()
 {
     await m_scene.BuildBackgroundAsync();

     m_initialized = true;
     MessageHandler.Publish<GameInitializedMessage>();
 }

Das Publish wird dabei wie auch bei anderen Frameworks innerhalb des gerade ausgeführten Threads synchron ausgeführt. Bei dem ganzen Prozedere entsteht aber ein kleines Problem: Ich möchte bei obigen Codebeispiel etwa haben, dass der UI-Thread trotzdem auch benachrichtigt wird. Schließlich soll dieser mitkriegen, dass das Spiel erfolgreich initialisiert wurde. Erreichen kann ich das, indem ich an der Nachrichtenklasse selbst (hier GameInitializedMessage) über Attribute eine Routing-Logik konfiguriere. Nachfolgend entsprechend das Coding der GameInitializedMessage.

[MessagePossibleSource(Constants.THREAD_NAME_GAME)]
[MessageAsyncRoutingTargets(SeeingSharpConstants.THREAD_NAME_GUI)]
public class GameInitializedMessage : SeeingSharpMessage
{

}

Hier gebe ich mittels des MessagePossibleSource Attributs zunächst an, welcher Thread die Nachricht auslösen darf. Wird die Message aus einem anderen Thread heraus ausgelöst, würde das Framework direkt mit einer Exception antworten. Weiterhin wird definiert, dass die Nachricht asynchron an den UI-Thread weitergegeben werden soll. Asynchron deswegen, um Deadlock-Situationen zu vermeiden. Würde nämlich an dieser Stelle der UI-Thread schon auf den Game-Thread warten, hätte man es schon so weit.

Auf Seite der UI habe ich für dieses Beispiel nachfolgendes Coding. Nicht wundern… hier handelt es sich um ein klassisches Windows.Forms Programm. Aus Sicht der hier vorgestellten Logik ist es völlig egal, ob an der Oberfläche Windows.Forms, WPF oder WinRT werkelt.

protected override async void OnLoad(EventArgs e)
{
    base.OnLoad(e);

    if (!SeeingSharpApplication.IsInitialized) { return; }

    // Subscribe all handlers to the UIMessenger
    SeeingSharpApplication.Current.UIMessenger
        .SubscribeAllOnControl(this);

    ...
}

...

private void OnMessage_Received(GameInitializedMessage message)
{
    this.UpdateDialogStates();
}

private void OnMessage_Received(LevelLoadedMessage message)
{
    this.UpdateDialogStates();
}

private void OnMessage_Received(LevelUnloadedMessage message)
{
    this.UpdateDialogStates();
}

Die UI kriegt hier von der Synchronisierungs-Logik nicht viel mit. Im Load-Ereignis des Hauptfensters wird hier die Methode Messenger.SubscribeAllOnControl aufgerufen. Diese Methode registriert alle Methoden am MessageHandler, welche entsprechend Messages verarbeiten. Hier betrifft das alle Methoden mit dem Namen OnMessage_Received, der Parameter gibt jeweils an, um welche Message es sich handelt. Alle drei hier abgebildeten Messages werden zwar im Game-Thread ausgelöst, sie werden aber sauber an den UI-Thread weitergeleitet, damit auch hier darauf reagiert werden kann, ohne sich weiter um Thread-Synchronisierung kümmern zu müssen.

Warum gehe ich diesen Weg? Für mich gibt es folgende wichtige Gründe:

  • Bei jeder Methode und bei den meisten Klassen sollte eindeutig klar sein, welcher Thread darin Logik ausführen darf, und welcher nicht. Je weniger Threads an einem gemeinsamen Objekt rum arbeiten, desto einfacher fällt die Synchronisierung.
  • Bereits bei den Message-Klassen (=Ereignisse) soll klar hinterlegt sein, welche Threads diese auslösen und welche diese empfangen dürfen.
  • Wütet ein Thread innerhalb einer Methode rum, bei der eben das nicht vorgesehen ist, werden entsprechende Exceptions geschmissen, um diese Probleme bei der Entwicklung schnell identifizieren zu können.
  • Leichte Verständlichkeit des Messenger-Patterns

Verweise

  1. http://www.codeproject.com/Articles/355473/Prism-EventAggregator-Sample

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.