W jaki sposób zapewnić responsywność wątku interfejsu użytkownika?

0

Cześć, chciałbym napisać program do komunikacji poprzez UART. Dobrym pomysłem wydaje się wykorzystanie wątku tak, aby aplikacja była responsywna podczas komunikacji.
Chciałbym zapytać Was o to czy powinno to tak wyglądać? W Visual studio mam błędy (tylko w postaci informacji, a nie wywalenia programu) System.InvalidOperationException.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading;
using System.Windows.Forms;

namespace ssss
{
    public partial class Form1 : Form
    {
        int x;
        int PortCOMOtwarty;
        string PortSzeregowyCaly;
        static SerialPort serial;
        static bool Continue;

        public Form1()
        {
            InitializeComponent();
        }

        public void UruchomWatek()
        {
            Thread readThread = new Thread(Odczyt);
            Continue = true;
            readThread.Start();
        }

        public void Odczyt()
        {
            while (Continue)
            {
                Thread.Sleep(1000);

                x++;

                try
                {
                    this.Invoke(new Action(() => { label13.Text = "Odebrane dane z UART: " + serial.ReadExisting(); }));
                    this.Invoke(new Action(() => { label14.Text = "Sygnalizacja pracy wątku: " + x.ToString(); }));
                }

                catch 
                {
                    
                }
            }
        }

        private void button3_Click(object sender, EventArgs e)
        {
            try
            {
                Continue = false;
                serial.Close();
                this.Close();
            }

            catch (Exception ex)
            {
                MessageBox.Show("Błąd: " + ex.Message, "Błąd");
                return;
            }

        }

        private void button1_Click(object sender, EventArgs e)
        {
            if (PortCOMOtwarty == 0)
            {
                PortSzeregowyCaly = "COM" + comboBox1.SelectedIndex;

                serial = new SerialPort();                      // Inicjalizacja obiektu
                serial.PortName = PortSzeregowyCaly;            // Nazwa portu COM - DevKit!
                serial.BaudRate = int.Parse(comboBox2.Text);    // Prędkość
                serial.Parity = Parity.None;                    // Brak bitu parzystości
                serial.DataBits = 8;                            // 8 bitów danych
                serial.StopBits = StopBits.One;                 // 1 bit stopu
                serial.Handshake = Handshake.None;

                serial.ReadTimeout = 500;
                serial.WriteTimeout = 500;

                try
                {
                    serial.Open();                              // Otwiera port szeregowy
                }

                catch (Exception ex)
                {
                    MessageBox.Show("Błąd: " + ex.Message, "Błąd");
                    return;
                }

                button1.Text = "Zamknij połączenie";
                PortCOMOtwarty = 1;
                UruchomWatek();
                return;
            }

            if (PortCOMOtwarty == 1)
            {
                Continue = false;

                try
                {
                    serial.Close();                              // Zamyka port COM
                }

                catch (Exception ex)
                {
                    MessageBox.Show("Błąd: " + ex.Message, "Błąd");
                    return;
                }

                button1.Text = "Nawiąż połączenie";
                PortCOMOtwarty = 0;
                return;
            }
        }

        private void button2_Click(object sender, EventArgs e)
        {
            try
            {
                serial.WriteLine(textBox1.Text + "x" + textBox2.Text + "x" + textBox3.Text + "x" + textBox4.Text + "x" + textBox5.Text + "x" + textBox6.Text + "x" + textBox7.Text + "x" + textBox8.Text + "x" + textBox9.Text);
            }

            catch (Exception ex)
            {
                MessageBox.Show("Błąd: " + ex.Message, "Błąd");
                return;
            }
        }
    }
}


1

Wygląda dobrze, ale ja proponuję pewną modyfikację:

  1. Nie próbuj nic robić na GUI z drugiego wątku. Zamiast tego, utwórz pole klasy, najprościej List<string> buf = new List<string>() i w wątku dopisuj do listy odczytane dane.
  2. W interfejsie dodaj timer tykający co 200ms lub rzadziej i wyzwalający zdarzenie, które które składa tekst z całej zawartości listy, wypisuje do pola tekstowego i czyści listę. Oczywiście, jeżeli w chwili tyknięcia lista jest pusta, nic nie robisz. Tykanie timera częściej niż 5 razy na sekundę i tak nie będzie zauważalne, a nie będzie blokować gui i bufora bez potrzeby.
  3. W celu zabezpieczenia przed nieprawidłowościami związanymi z wielowątkowym dostępem do jednego i tego samego obiektu, czyli listy komunikatów obiektu, przed pierwszą czynnością dodaj Monitor.Enter(buf), a po ostatniej czynności w funkcji dodaj Monitor.Exit(buf) przy założeniu, że lista komunikatów ma nazwę buf. Funkcje Monitor gwarantują, że nie będzie możliwa sytuacja, w której dwa wątki będą jednocześnie wykonywać dwa kawałki kodu objęte wejściem i wyjściem monitora na tym samym obiekcie.
1
andrzejlisek napisał(a):

Wygląda dobrze,

Moim zdaniem wręcz przeciwnie. Kod jest totalnie wbrew dobrym praktykom.

0

Wymodziłem coś takiego, czy teraz jest zgodnie z "dobrą praktyką"?

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading;
using System.Windows.Forms;

namespace ssssssss
{
    public partial class Form1 : Form
    {
        int x;
        int PortCOMOtwarty;
        string PortSzeregowyCaly;
        static SerialPort serial;
        static bool Continue;
        List<string> buf = new List<string>();
        private readonly object bufLock = new object();

        public Form1()
        {
            InitializeComponent();
        }

        public void UruchomWatek()
        {
            Thread readThread = new Thread(Odczyt);
            Continue = true;
            readThread.Start();
        }

        public void Odczyt()
        {
            while (Continue)
            {
                Thread.Sleep(1000);

                x++;

                try
                {
                    string receivedData = serial.ReadExisting();
                    lock (bufLock)
                    {
                        Monitor.Enter(buf);
                        buf.Add(receivedData);
                        Monitor.Exit(buf);
                    }
                }

                catch
                {

                }
            }
        }

        private void button3_Click(object sender, EventArgs e)
        {
            try
            {
                Continue = false;
                serial.Close();
                this.Close();
            }

            catch (Exception ex)
            {
                MessageBox.Show("Błąd: " + ex.Message, "Błąd");
                return;
            }
        }

        private void button1_Click(object sender, EventArgs e)
        {
            if (PortCOMOtwarty == 0)
            {
                PortSzeregowyCaly = "COM" + comboBox1.SelectedIndex;

                serial = new SerialPort();                      // Inicjalizacja obiektu
                serial.PortName = PortSzeregowyCaly;            // Nazwa portu COM - DevKit!
                serial.BaudRate = int.Parse(comboBox2.Text);    // Prędkość
                serial.Parity = Parity.None;                    // Brak bitu parzystości
                serial.DataBits = 8;                            // 8 bitów danych
                serial.StopBits = StopBits.One;                 // 1 bit stopu
                serial.Handshake = Handshake.None;

                serial.ReadTimeout = 500;
                serial.WriteTimeout = 500;

                try
                {
                    serial.Open();                              // Otwiera port szeregowy
                }

                catch (Exception ex)
                {
                    MessageBox.Show("Błąd: " + ex.Message, "Błąd");
                    return;
                }

                button1.Text = "Zamknij połączenie";
                PortCOMOtwarty = 1;
                UruchomWatek();
                return;
            }

            if (PortCOMOtwarty == 1)
            {
                Continue = false;

                try
                {
                    serial.Close();                              // Zamyka port COM
                }

                catch (Exception ex)
                {
                    MessageBox.Show("Błąd: " + ex.Message, "Błąd");
                    return;
                }

                button1.Text = "Nawiąż połączenie";
                PortCOMOtwarty = 0;
                return;
            }
        }

        private void button2_Click(object sender, EventArgs e)
        {
            try
            {
                string dataToSend = textBox1.Text + "x" + textBox2.Text + "x" + textBox3.Text + "x" + textBox4.Text + "x" + textBox5.Text + "x" + textBox6.Text + "x" + textBox7.Text + "x" + textBox8.Text + "x" + textBox9.Text;
                serial.WriteLine(dataToSend);
            }

            catch (Exception ex)
            {
                MessageBox.Show("Błąd: " + ex.Message, "Błąd");
                return;
            }
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            lock (bufLock)
            {
                Monitor.Enter(buf);
                try
                {
                    if (buf.Count > 0)
                    {
                        StringBuilder sb = new StringBuilder();
                        foreach (string data in buf)
                        {
                            sb.Append(data);
                        }
                        label13.Text += sb.ToString();
                        buf.Clear();
                    }
                }

                finally
                {
                    Monitor.Exit(buf);
                }
            }
        }
        }
}
0

ak nie chce Ci się analizować zmian to po co piszesz. Dodałem buf i monitor. W gui już zmienia jeden prawilny wątek więc chyba osiągnąłem cel. — bboylubsko 41 minut temu

Kod ze współbieżnością, totalnie poza dobrymi praktykami, a wręcz w metodyce stochastycznej.
MOŻE zadziała w skrajnie prostym przypadku, pewnie tak, ale polegniesz potem

ps. masz rację. Nigdy nie analizuję combo1, czy namespace sssss Form1

0
ZrobieDobrze napisał(a):

ak nie chce Ci się analizować zmian to po co piszesz. Dodałem buf i monitor. W gui już zmienia jeden prawilny wątek więc chyba osiągnąłem cel. — bboylubsko 41 minut temu

Kod ze współbieżnością, totalnie poza dobrymi praktykami, a wręcz w metodyce stochastycznej.
MOŻE zadziała w skrajnie prostym przypadku, pewnie tak, ale polegniesz potem

ps. masz rację. Nigdy nie analizuję combo1, czy namespace sssss Form1

Jejku, jak czasem ciężko się przebić przez ten mur. Toż to po to ten temat założyłem, żeby się czegoś dowiedzieć od fachur, a nie po zaocznych szpargały pisać.

Wygląda to mniej więcej tak:

Hej, mam pytanie, czy jest coś do poprawy?

No hej, tak, jest.

A koledze andrzejlisek bardzo dziękuję za wskazówki.

0
ZrobieDobrze napisał(a):
andrzejlisek napisał(a):

Wygląda dobrze,

Moim zdaniem wręcz przeciwnie. Kod jest totalnie wbrew dobrym praktykom.

Masz prawo mieć odmienne zdanie od mojego, ale wypadałoby powiedzieć, dlaczego tak uważasz i co w tym kodzie jest nie tak.

Moim zdaniem, ma to sens i wygląda logicznie, bo co następuje:

  1. Jest przycisk "start", który przygotowuje działanie (parametry działania portu), uruchamia dodatkowy wątek, a w nim funkcję w pętli, której warunek zawsze jest spełniony.
  2. Jest przycisk "stop", który zmienia wartość zmiennej pętli, po czym warunek pętli przestaje być spełniony i dodatkowy wątek kończy pracę.
  3. Newralgiczne elementy są otoczone try/catch.
  4. Przy uruchamianiu jest sprawdzenie, czy port szeregowy już jest otwarty, żeby dwa razy nie uruchomić wątku.

Co Twoim zdaniem jest nie tak w kontekście wielu wątków? W tym przypadku na OP najprawdopodobniej pisze jakiś program, patrzy jak to działa, bawi się, a potem wszystko pójdzie do kosza. Pomijam taki "drobiazg", że w jednej klasie jest pomieszana obsługa GUI i logiki biznesowej, bo jeżeli robi się próbę po to, żeby napisać, uruchomić i zapomnieć, to nie ma po co rozdzielać tych dwóch elementów.

0

Kurczę, chodzi mi o podłapanie dobrych nawyków, mogę coś brać intuicyjnie, na chłopski rozum, ale jak będę w to brnął dalej to tylko stracę czas, a może akurat ktoś podpowie jak powinno się takie rzeczy robić na poziomie założeń.

Aplikacja ma się komunikować z procesorkiem STM32. Komunikacja w dwie strony. Dane wysyłane przez proca mają być prezentowane na wykresie.

Co chciałbym uzyskać:

  1. Jeden zestaw danych wysyłanych przez STM ma być po prostu wyświetlony w 3 stringach w gui aplikacji.
  2. Drugi zestaw danych ma być prezentowany na wykresie w czasie rzeczywistym.
  3. Trzeci zestaw danych też ma być prezentowany na wykresie w czasie rzeczywistym.
  4. Fajnie jakby był podgląd odebranych danych w jakimś polu diagnostycznym.

Główkuję jak to wszystko przesłać i odebrać, żeby komunikacja była jak najbardziej optymalna.

Moje przemyślenia na dzień dzisiejszy wyglądają tak:
Załóżmy, że wykres chciałbym odświeżać co 1 sekundę.
Aby była pewność że dane będą poprawnie wyświetlone to musiałbym je wysyłać z większą częstotliwością.
Dane powinny być jakoś odseparowane charakterystycznym znakiem, tak aby można było odfiltrować z ciągu to na czym mi zależy w danym momencie
.
Nadawanie przez serial z procka = wartoscString1;wartoscString2;wartoscString3;daneOSy1;daneOSy2.

Pojawia się pytanie o synchronizację czy w wyniku działania komunikacji nie dojdzie do przeskoczenia jeśli będę opierał się na tak prymitywnym filtrowaniu danych.

Zastanawiam się, w jaki sposób to wszystko odebrać, przefiltrować, porozmieszczać po stringach i wykresach.
Dla mnie jest już dużą wskazówką czy mój sposób myślenia jest poprawny.

0

Czy to ustrojstwo z STM jest Twojej konstrukcji? Czy masz możliwość zmiany formatu danych? Aby przemyśleć format wyobraź sobie jedną rzecz. Rejestrujesz dane tak jak lecą ciurkiem,zakladasz,ze nie ma żadnych błędów w samej komunikacji i nie ma przekłaman. Dostajesz po prostu ciąg bajtow czy tam ciąg znaków, co na jedno wychodzi. Skoro dane są wysyłane cyklicznie, to niech ten ciąg jest od połowy cyklu, zawiera trzy pełne cykle i urywa się w połowie kolejnego cyklu. Teraz podejdź do analizy ciągu bazując na założeniu, że nie wiesz w jakiej części cyklu się zaczyna i kończy i ile cykli zawiera, wiesz tylko tyle, że zawiera co najmniej jeden pełny cykl. Czy analizując sam ciąg jesteś w stanie wyodrebnic ten cykl? Jeżeli nie, to rozważ zmianę formatu lub dodanie wyróżnika, który jednoznaczne wskazuje że jest to miejsce między cyklami, nie ma znaczenia czy ten wyróżnik rozpoczyna czy kończy cykl. Co do wątków, to w jakiejś strukturze w jednym wątku zapisujesz dane z najnowszego cyklu, a w innym wątku, konkretnie z wątku GUI co pewien czas odczytujesz dane z tej struktury w celu namalowania wykresu. Aby odczyt i zapis samych nie wchodziły sobie w drogę i nie psuły działania programu, potrzebny jest monitor. Przydadzą się Monitor.Enter, Monitor.TryEnter i Monitor.Exit. Opis tych funkcji wiele wyjaśni po co one są i co robią.

0

Już samo to, że autor uaktualnia gui z wątku to jeden wielki wtf, nigdy nie powinno się tego robić tylko użyć Invoke

1 użytkowników online, w tym zalogowanych: 0, gości: 1