none
MVVM - Thread Struktur Optimierung RRS feed

  • Frage

  • Hallo Zusammen,

    im Folgenden seht ihr einige Auszüge aus meinem aktuellen Programm. Da ich mich zu wenig mit sauberer Thread - Programmierung auskenne, wende ich mich nun an Euch. Mit Hilfe von Peter Fleischer ist es mir gelungen mit Hilfe von SynchronisationContext.Post die Weitergabe von Ergebnissen des Threads EP2Schnittstelle ans VIEWMODEL zu realisieren. Allerdings stellt dies eine Art Übergangslösung dar.

    Probleme macht derzeitig noch der SerialPortManager, der mir die Messdaten liefert. Leider brauch ich noch ein Thread.Sleep um auf die Antwortpackete zu warten und auch einen Wartebereich zu haben um einen Messtaster auslösen zu können.

    Ziel sollte es sein, die EP2 Schnittstelle und KeyenceSchnittstelle permament laufen zu lassen, als Zustandsautomat. Das ViewModel empfängt nur die ankommenden Daten und aktualisert das ViewModel.

    Ich stelle mir eine Art Zwischenschicht zwischen ViewModel und Ep2Schnittstelle dar, die mir die Daten bereit stellt. Leider fehlt mir hier, wie schon erwähnt, noch Wissen über die Thread-Architektur.

    In der letzten Folie, ist noch eine Ansteuerung der KeyenceHardware vorgesehen, die so bedauerlichweise auch noch nicht funktioniert. Auf einem Laptop von einem Kollegen läuft es sauber durch. Wenn ich das gleiche Programm auf einem Industriepc laufen lasse, läuft das ganze nicht durch.

    Ich hoffe, ihr könnt mir helfen.

    Vielen Dank!

    Viele Grüße
    Martin


    AddMesswert steuert die aktuelle Prüflingszeile. Der alte Messpunkt wird übernommen und die neue Messpunktzeile im Prüfling wird angewählt.

    Donnerstag, 22. August 2019 09:02

Antworten

  • Hi Martin,
    ohne zu wissen, wie die Zustände im Zustandsautomat geändert werden, kann man nicht viel dazu sagen. Wenn es nur eine Fortschrittsfolge ist (Start -> ReceivedAnzeige1 -> ReceivedAnzeige2 usw.), dann würde await ...SendCommand(… völlig ausreichen.

    Mit Datenprotokoll meine ich die Beschreibung der erforderlichen Anfragen mit den zu erwartenden Antworten, aus der man ein Handshaking mit Fehlerbehandlung aufbauen kann und aus der man auch ersehen kann, ob Teile von Datenpaketen erst zusammenzuführen (anhäufen, puffern) sind.

    Wenn sowohl ein SendCommand als auch der Anwender eine lesende Übertragung auslösen kann, dann ist auch im Datenprokoll zu beschreiben, wie diese Parallelarbeit aussieht. Wenn beides über den gleichen logischen Kanal möglich ist, dann ist ggf. ein Zustandsautomat sinnvoll.

    Bezüglich Zwischenschicht könnte das so aussehen:

    1. Abfrage an Model, um Daten zu bekommen;

    2. Daten kommen an, werden gepuffert  und analysiert;

    3. Zwischenschicht ermittelt, dass Daten nicht vollständig sind und wiederholt ab Schritt 1.

    4. Zwischenschicht erkennt, dass alle gepufferten Daten vollständig sind und teilt der übergeordneten Schicht (z.B. ViewModel) mit, dass neue Daten vorliegen.


    --
    Best Regards / Viele Grüße
    Peter Fleischer (former MVP for Developer Technologies)
    Homepage, Tipps, Tricks

    Freitag, 23. August 2019 11:45

Alle Antworten

  • Hi Martin,
    Deine Bilder sind auch vergrößert mit Lupe und Brille nicht im Detail lesbar.

    Wie viele Schichten Du baust, ist relativ uninteressant. Wichtig ist zu bestimmen (festzulegen), welche Schicht welche Aufgaben zu erfüllen hat. Für das Zusammenfügen der eingelesenen Datenportionen kann es sinnvoll sein, zwischen ViewModel und dem Model eine Zwischenschicht zu legen. Das ist sinnvoll, wenn der Model nur die Basisaufgaben erledigt, wie beispielsweise eine Datenportion holen, und diese Zwischenschicht aus den Datenportionen zusammenhängende Datensätze baut (zusammenfügt). Um das aber zu entscheiden und zu gestalten, muss als Erstes das Datenprotokoll beschrieben werden. Das fehlt mir in allen Deinen Ausführungen.

    Thread.Sleep ist eine denkbar ungünstige Lösung. Wer garantiert, dass bei anderen Bedingungen (z.B. Netzlast) die gesetzte Zeit nicht überschritten wird und es dann zu Problemen kommt. Besser ist in jedem Fall das Datenprotokoll so zu gestalten, dass ein lückenloses Hand-Shaking realisiert wird. Dann braucht man kein Thread.Sleep. Die Anwendung läuft dann flüssiger und ggf. viel schneller. Wenn das Hand-Shaking unterbrochen wird, ist das ein Fehler, der ein Rücksetzen auslöst und die Übertragung wiederholt wird.

    Ich weiß nicht, ob Du die Idee des Zustandsautomaten richtig anwendest. Der Zustandsautomat ist einzusetzen, wenn ein Objekt mehrere Zustände haben kann. In Abhängigkeit vom Ergebnis der Verarbeitung des aktuellen Zustandes wird ausgewählt, in welchen nächsten Zustand das Objekt zu versetzen ist, um dann diesen nächsten Zustand zu verarbeiten. Deine Skizze lässt vermuten, dass es sich bei den einzelnen SenCommand-Aufrufen um mögliche parallele Aufrufe handeln kann. Dann wäre Threading sinnvoll, d.h. jedes SendCommand in einem eigene Thread. Zur Verhinderung eines zu großen Overheads kann man ThreadPooling einsetzen. 


    --
    Best Regards / Viele Grüße
    Peter Fleischer (former MVP for Developer Technologies)
    Homepage, Tipps, Tricks

    Donnerstag, 22. August 2019 12:59
  • Hallo Peter,

    danke für deine Antwort:

    1.) Der Zustsndsautomat sieht aktuell wie folgt aus:

    Die Zustände werden dann im Model_DataArrived und ModelKeyence_DataArrived entsprechend geändert, wenn Datenpakete angekommen sind.

    Nun welches Datenprotokoll meinst du?

    Das Datenprotokoll der Ep2 besteht aus einer Anzeigenadresse,dem Messwert und einer Checksumme. Um zu prüfen ob das ankommende Datenpaket sinnvoll ist. Die angekommenden Datenpakete (entweder von Sendcommand ausgelöst oder der Anwender drückt einen knopf) werden von

    void _spManager_NewSerialDataRecieved(object sender, SerialDataEventArgs e)

    ausgewertet und ans ViewModel mit DataArrived.Invoke übergeben.

    Vielleicht wäre ein Beispiel hilfereich , um dieses Handshaking in Verbindung mit der Zwischenschicht in der Anwendung zu sehen.

    Vielen Dank!

    Freitag, 23. August 2019 09:08
  • Hi Martin,
    ohne zu wissen, wie die Zustände im Zustandsautomat geändert werden, kann man nicht viel dazu sagen. Wenn es nur eine Fortschrittsfolge ist (Start -> ReceivedAnzeige1 -> ReceivedAnzeige2 usw.), dann würde await ...SendCommand(… völlig ausreichen.

    Mit Datenprotokoll meine ich die Beschreibung der erforderlichen Anfragen mit den zu erwartenden Antworten, aus der man ein Handshaking mit Fehlerbehandlung aufbauen kann und aus der man auch ersehen kann, ob Teile von Datenpaketen erst zusammenzuführen (anhäufen, puffern) sind.

    Wenn sowohl ein SendCommand als auch der Anwender eine lesende Übertragung auslösen kann, dann ist auch im Datenprokoll zu beschreiben, wie diese Parallelarbeit aussieht. Wenn beides über den gleichen logischen Kanal möglich ist, dann ist ggf. ein Zustandsautomat sinnvoll.

    Bezüglich Zwischenschicht könnte das so aussehen:

    1. Abfrage an Model, um Daten zu bekommen;

    2. Daten kommen an, werden gepuffert  und analysiert;

    3. Zwischenschicht ermittelt, dass Daten nicht vollständig sind und wiederholt ab Schritt 1.

    4. Zwischenschicht erkennt, dass alle gepufferten Daten vollständig sind und teilt der übergeordneten Schicht (z.B. ViewModel) mit, dass neue Daten vorliegen.


    --
    Best Regards / Viele Grüße
    Peter Fleischer (former MVP for Developer Technologies)
    Homepage, Tipps, Tricks

    Freitag, 23. August 2019 11:45
  • Hallo Peter,

    anbei findest du das Datenprotkoll skizziert.

    Den aktuell bestehenden Zustandsautomat könnte man ja verwenden. Darin sollte ja das Abwarten der Sendcommands abgebildet werden. Wenn die zu Erwartenden Antwort eingegetroffen ist, dann springt er in den nächsten Zustand. Damit könnten die Thread.Sleep befehle wegfallen, richtig? Ein anderer Grund warum die Thread.Sleep befehle noch drin sind, is das genügend "Luft" in der Leitung ist, damit das Messtaster Datenpaket erkannt wird. Wie könnte denn da eine saubere Lösung aussehen?

    Vielen Dank!

    Donnerstag, 29. August 2019 07:50
  • Hi,
    da die Schritte im grünen Kasten nacheinander abzuarbeiten sind, bietet sich async/await an. Da kann dann Thread.Sleep entfallen und es vergeht keine unnötige Wartezeit. Das externe Messtasterauslösen muss ja irgendwie gestartet werden. Und da kann dann separat auf das Auslösen gewartet werden.

    --
    Best Regards / Viele Grüße
    Peter Fleischer (former MVP for Developer Technologies)
    Homepage, Tipps, Tricks



    • Bearbeitet Peter Fleischer Mittwoch, 4. September 2019 07:33 Grammatikfehler
    Donnerstag, 29. August 2019 11:46
  • Okay, ohne es getestet zu haben: Gehe ich richtig der Annahme, dass await SendCommand() auch wirklich auf das Ereignis _spManager_NewSerialDataReceived(...) wartet?

    Dankeschön.

    Donnerstag, 29. August 2019 18:05
  • Hi,
    nein, Deine Annahme ist nur richtig, wenn SendCommand entsprechend aufgebaut ist. Mit await wird auf das Ende eines Task gewartet, wobei das Warten so durchgeführt wird, dass im gleichen Thread während des Wartens auch andere Programmteile abgearbeitet werden können, z.B. ein Ereignis oder die Aktualisierung der Anzeigen der Oberfläche. Damit das möglich wird, muss beim Einsprung in die Methode, in der await genutzt wird, zusätzlich Speicherplatz im Stack reserviert werden, um Zustände der Methode beim Warten zwischenzuspeichern. Damit der Compiler das macht, muss das Schlüsselwort async im Methodenaufruf angegeben werden. Await veranlasst den Thread solange zu warten, bis das Task-Objekt der Methode, auf deren Ausführungsende mit await gewartet wird, ein Ende signalisiert. D.h., die aufgerufene Methode muss ein Task-Objekt als Rückgabewert liefern. 

    Für Dich bedeutet das, dass die synchrone SendCommand-Methode asynchron auszuführen ist und das DataReceived-Ereignis das Ende der asynchronen Ausführung dem Task-Objekt signalisieren muss.


    --
    Best Regards / Viele Grüße
    Peter Fleischer (former MVP for Developer Technologies)
    Homepage, Tipps, Tricks


    • Bearbeitet Peter Fleischer Mittwoch, 4. September 2019 07:33 Grammatikfehler
    Freitag, 30. August 2019 03:56
  • Hi Peter,

    danke für deine Antwort.

    Ich habe die SendCommand Routine umgebaut. Mit deinen Informationen könnte es dann wie folgt aussehen?: Leider kann ich die Änderungen nicht testen, da ich die Hardware aktuell nicht zur Verfügung habe:

    In der Klasse Ep2Schnittstelle:

            // 30.08.2019 Task für um auf Event SerialPortManager.NewSerialDataRecieved zu warten
            public static Task SendCommandComplete(SerialPortManager _sp)
            {
                var tcs = new TaskCompletionSource<object>();
                // Wenn Datenstrom angekommen ist, 
                _sp.NewSerialDataRecieved += (_, args) =>
                {
                    tcs.SetResult(null);
                };
    
                return tcs.Task;
            }
    
            public async void SendCommand(string command, byte AnzeigeAdresse, int threshold)
            {
                Debug.Print($"Zeit: {DateTime.Now.Millisecond.ToString()}" + $"SendCommand: {command}" + $"AnzeigeAdresse: {AnzeigeAdresse}");            
                if (command == "Messwert holen")
                {
                     //Thread.Sleep(200);
                    _spManager.SendCommand(AnzeigeAdresse, 0x03, 0, threshold);
                    await SendCommandComplete(_spManager);
                }
                if (command == "Sensorstatus holen")
                {
                     //Thread.Sleep(200);
                    _spManager.SendCommand(AnzeigeAdresse, 0x55, 0, threshold);
                    await SendCommandComplete(_spManager);
                }
                if (command == "Anzeige Nullen")
                {
                    //Thread.Sleep(500);
                    _spManager.SendCommand(AnzeigeAdresse, 0x02, 0, threshold);
                    await SendCommandComplete(_spManager);
                    //Thread.Sleep(500);
                }
            }
    

    Freitag, 30. August 2019 06:35
  • Hi Martin,
    ich würde Dein Problem viel einfacher lösen.

    In der Model-Klasse würde ich das Abrufen und das Empfangen des Ergebnisses als eine synchrone Methode gestalten. Zur Demo könnte diese Model-Klasse dienen. In der SendCommand-Methode wird auf das Ende der Operation gewartet.

      /// <summary>
      /// Model class
      /// </summary>
      public class Window61M
      {
        SerialPortManager _spManager = new SerialPortManager();
    
        public string SendCommand(string command, byte AnzeigeAdresse, int threshold)
        {
          if (command == "Messwert holen") return _spManager.SendCommand(AnzeigeAdresse, 0x03, 0, threshold);
          if (command == "Sensorstatus holen") return _spManager.SendCommand(AnzeigeAdresse, 0x55, 0, threshold);
          if (command == "Anzeige Nullen") return _spManager.SendCommand(AnzeigeAdresse, 0x02, 0, threshold);
          return string.Empty;
        }
    
        public class SerialPortManager
        {
    
          public string SendCommand(byte AnzeigeAdresse, int par1, int par2, int threshold)
          {
            AutoResetEvent are = new AutoResetEvent(false);
            // Start der Aktivität
            SendCommandComplete(are);
            are.WaitOne();
            return "OK";
          }
    
          void SendCommandComplete(object state)
          {
            Thread.Sleep(1000); // Simulation der Verzögerung
            var are = state as AutoResetEvent;
            are?.Set();
          }
        }
      }

    Im ViewModel wird die Folge der SendCommand's mit Task.Run ausgeführt. Damit wird die Folge asynchron ausgeführt. Die Ausführung von Task.Run dauert nur einige Millisekunden, so dass die Oberfläche bedienbar bleibt. Information aus dieser Folge für die Anzeige in der Oberfläche müssen im Oberflächen-Thread abgearbeitet werden. Dazu kann der Dispatcher dienen.

      /// <summary>
      /// ViewModel
      /// </summary>
      public class Window61VM
      {
        public Window61VM() { cvs.Source = col; }
        public ICommand Cmd { get { return new RelayCommand(CmdExec, CanCmdExec); } }
    
        private void CmdExec(object state)
        {
          Task.Run(() =>
          {
            IsWorking = true;
            Window61M model = new Window61M();
            string res = "Folge gestartet";
            AddLog(res);
            res = model.SendCommand("Messwert holen", 1, 1);
            AddLog(res);
            res = model.SendCommand("Sensorstatus holen", 1, 1);
            AddLog(res);
            res = model.SendCommand("Anzeige Nullen", 1, 1);
            AddLog(res);
            AddLog("Folge beendet");
            IsWorking = false;
          });
        }
    
        private bool IsWorking { get; set; }
        private bool CanCmdExec(object obj) => !IsWorking;
    
        private void AddLog(string msg) => Application.Current.Dispatcher.Invoke(() => { col.Add($"{DateTime.Now.ToString("mm:ss.fff")} {msg}"); });
    
        private CollectionViewSource cvs = new CollectionViewSource();
        private ObservableCollection<string> col = new ObservableCollection<string>();
        public ICollectionView Log { get { return cvs.View; } }
      }

    Die Demo-Oberfläche kann so aussehen:

    <Window x:Class="WpfApp1.Window61"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:WpfApp1"
            mc:Ignorable="d"
            Title="Window61" Height="450" Width="800">
      <Window.DataContext>
        <local:Window61VM/>
      </Window.DataContext>
      <StackPanel>
        <Button Content="Start SendCommand" Command="{Binding Cmd}" Margin="5"/>
        <TextBox Text="zum Test Bedienbarkeit etwas eingeben" Margin="5"/>
        <ListBox ItemsSource="{Binding Log}" Margin="5"/>
      </StackPanel>
    </Window>

    --
    Best Regards / Viele Grüße
    Peter Fleischer (former MVP for Developer Technologies)
    Homepage, Tipps, Tricks


    Samstag, 31. August 2019 09:27