Jak wydajniej ?

0

Mam problem dotyczący wydajności programu.
W programie po przyjściu wiadomości na port rs232 wyrysowuje odpowiednie wykresy. Wykresów jest kilka (zależy od wersji). Wszystko ma się rysować na bieżąco i robić jeszcze kilka innych rzeczy dlatego ważna jest wydajność. Robię to następująco: w momencie przyjścia na port danych następuje wykonanie na każdym z pictureBoxów metody BeginInvoke(funkcja,parametry) gdzie funkcja wykonuje odpowiednie malowania/usuwania zgodnie z parametrami. W kodzie obsługującym zdarzenie pojawienia się danej mam następujący fragment:

for (int i = 0; i < ilośc_PictureBoxów; i++)
PictureBox[i].BeginInvvoke(funkcja,parametry)

Tutaj moje pierwsze pytanie: czy można to zrobić lepiej - efektywniej już na tym poziomie ?
Może jakieś inne podejście wielowątkowe ? (Choć z tego co wiem używając BeginInvoke tworzy się
nowy wątek wykonujący daną funkcję). Aha - pod koniec programu wywala mi błąd apropos tego:
"Elementów Invoke lub BeginInvoke nie można wywołać w formancie do czasu utworzenia uchwytu okna"

Potem kwestia malowania(czyli to co jest robione w "funkcja"): robię to malując po pictureBoxie (CreateGraphics) oraz po bitmapie (Graphics.FromImage) tak, że normalnie się rysuje po PictureBoxie a jak sobie program wywoła invalidate to ciągnie Image tego pictureBoxa z bitmapy. No i znowu pytanie - czy da się zrobić lepiej to rysowanie (wszystko jest jak wiadomo real-time).

Będę wdzięczny za wszelką pomoc ! :)

PS. Tak zbudowana aplikacja patrząc na użycie procka wygląda tak że przez chwile jest ok 9%
potem zaś skacze do 20-30 potem znowu mniej i znowu 20-30 i tak w kółko. Jak przypuszczam wiąże
się to z jakimiś automatycznym procesami .NET ale pytanie brzmi jak się tego pozbyć żeby cały
czas mogło być 9% :)

0

Co zlego w 30%? W 100% nie ma nic zlego, pod warunkiem ze program cos robi. Proc musi byc zajety, bo przeciez wlasnie cos robi (czyli np. rysuje wykres).

Ja osobiscie rysowalbym na swoich bitmapach i tylko odswiezal w razie potrzeby pictureboxa, zeby odmalowal nowa bitmape.
Co do wyjatku - prawdopodobnie starasz sie wywolac BeginInvoke na zamknietym oknie (klikasz zamknij, okno jest niszczone, po czy cos jeszcze wywoluje BeginInvoke).

0

Wykres i tak musi być wyświetlony w kontekście wątku głównego więc właśnie tam go pasuje malować, zresztą jakby nie było jest to część GUI.

Pamiętaj ostatnie w pełni wczytane zestawy danych. Możesz zrobić sobie listę, na jednym końcu wątek roboczy dopina zestaw odczytanych danych, na drugim końcu wątek główny ten z GUI odpina zestaw do namalowania. Operacje odczepiania, dopinania i sprawdzania ilości muszą być zsynchronizowane, np. lock'iem (wszystkie trzy jednym lock'iem). Operacje te są szybkie (zaledwie zapis/odczyt kilku zmiennych) więc wątki nie będą się blokować na wzajem.

Wątek roboczy w pętli

  • odczytuje i interpretuje zestaw danych z RS'a
  • w lock
    • jeśli na liście jest mniej jak 2 zestawy to doczepia ten odczytany

Wątek główny podczas tyknięcia timerka

  • w lock
    • jeśli na liście są 2 zestawy to zapisuje referencje do nich (starszy i młodszy) oraz odpina ten ostatni (starszy).
  • jeśli są zapisane referencje do zestawów
    • wykorzystuje stary zestaw do zamalowania wykresu (maluje wykres w kolorze tła), będzie to szybsze niż wyczyszczenie całego obrazka
    • maluje nowy zestaw

Jak johny_bravo wspomniał warto malować po własnej natywnej bitmapie w unsafe (bezpośrednio na pamięci bitmapy). Wtedy możesz normalnie czyścić bitmapę (zamalowując całą powierzchnię) i wystarczy, że zapamiętasz o jeden zestaw mniej (czyli jeden lub wcale) co uprości sytuację do jednej zsynchronizowanej referencji:

Wątek roboczy w pętli

  • odczytuje i interpretuje zestaw danych z RS'a
  • w lock
    • jeśli synchronizowana referencja jest null'em to zapisuje pod nią odczytany zestaw

Wątek główny podczas tyknięcia timerka

  • w lock
    • jeśli synchronizowana referencja nie jest null'em to zapisuje ją w innej zmiennej i zeruje
  • jeśli zapisano referencje to maluje wskazany przez nią zestaw na bitmapie i wyświetla ją.

Aby dobrać się do bufora bitmapy pobierz jej uchwyt (GetHBitmap) i skorzystaj z natywnego api windows'a - GetObject wypełni ci strukturę BITMAP, która posiada wskaźnik na bufor bitmapy).

Co do szczegółów to możesz pytać jeśli coś jest nie jasne, podpowiem więcej.

0

Cóż - rozwiązałem to na razie w sposób następujący:

  1. każdy z PictureBoxów (jest ich obecnie 6 i kazdy musi malować trzy przebiegi - różne!) jest tworzony w oddzielnym wątku - to tak na początku
  2. W trakcie przychodzenia danych jest uruchamiane warunkowo PictureBox[i].BeginInvoke(funkcja,parametry)
  3. w "funkcji" rysuje zaledwie jedną linię (DrawLine w zakresie punkt poprzedni do punkt obecny) i to samo robie po bitmapie tego PictureBoxa (też drawLine punkt porzedni-punkt obecny), ale nie robie żadnego Invalidate !
  4. "Funkcja" ma swoją zawartość objętą lockiem żeby się nie zdarzyło, że może nie zdąży się skończyć a przyjście kolejnej danej na rs'a wywoła BeginInvoke z tą funkcją.

Nie wiem czy dobrze rozumiem ale Lock działa tak jak operacje monitora (Enter i Exit).

Nie używam żadnego odświeżania typu Invalidate bo jest to po prostu w .NET 2 biedne :( (Nie korzysta z DirectX) i trzeba to robić innymi często nieładnymi sposobami - właśnie tak jak ja to robię po prostu malując po PictureBoxie.

W efekcie kilku przekształceń użycie procka spadło do 10%.

Zastanawiam się tylko czy w przypadku wolniejszego kompa nie wystąpi jakiś problem z tymi wątkami. Czy objęcie lock'iem zawartości funkcji malującej załatwia mi sprawę. Oraz czy nie będzie tak, że zbyt szybki rs względem możliwości kompa spowoduje że te BeginInvoke(funkcja,parametry) będzie wywoływać się tak szybko że będzie jakieś nakładanie czy coś takiego... :P

0

Aha mam pytanie do przedmówcy: chodzi o ten pomysł z dopinaniem w wątku głównym a odpinaniem w roboczym. Jak w roboczym to zrobić ? Czyli w roboczym byłby while(true) { .... } tylko tam chyba jakieś opóźnienie dać czy coś takiego ? Bo w końcu on ma tak działać w kółko oczekując jak zestaw będzie się składał z dwóch i wtedy wykona malowanie i odepnie zestaw, przez co wątek główny przy pojawieniu się danych będzie wiedział że może dopiąć kolejną daną. I nawet jakby coś działało za wolno to się nie pogubi i będzie ciągle na czasie.

0

Tak. Za pomocą ronda można to napisać - wątek roboczy będzie umieszczać dane w rondzie, wątek z GUI zabierać. Jeśli w rondzie zabraknie miejsca (malowanie nie nadąża za danymi) to nadpisane zostaną najstarsze porcje danych. Jeśli w rondzie zabraknie danych (dane nie nadążają za malowaniem) to nic nie będzie malowane. Tak może to wyglądać:

public class Data { /*...*/ } //klasa reprezentująca pojedynczą porcję danych

//kolejka (rondo) referencji do Data, w przykładzie bufor ma 6 elementów, im większy bufor tym większe opóźnienie, ale za to tym większa odporność na nadmiar i niedomiar danych 
private CircularFiFo<Data> fifo = new CircularFiFo<Data>(6); 

Data currData = null; //obecnie wyświetlana porcja danych

//metoda dla wątku głównego umieszczona np. w timerku
private void TimerGUI()
{
    Data data = fifo.Pop(); //bierzesz z kolejki najstarszą porcję danych
    if(data != null) //jeśli braknie danych niestety nic nie malujemy ani mażemy
    {
        if (currData != null) Erase(currData); //mazanie tego co było namalowane w poprzednim kroku
        currData = data; //zapisujemy aktualną porcje danych
        Draw(data); //domalowywanie tego co ma być w aktualnym kroku
    }
}

//główna metoda dla wątku roboczego
private void Watek()
{
    while(...) //jakaś główna pętla
    {
        data = Pobierz(); //pobieramy kolejną porcję danych
        fifo.Push(data); //i wrzucamy ją do kolejki
        //...
    }
}

//...

    public class CircularFiFo<T>
    {
        private T[] buff;
        private uint size;
        private uint count = 0;
        private uint head = 0;

        public CircularFiFo() : this(1) { }

        public CircularFiFo(uint buffSize)
        {
            size = buffSize;
            buff = new T[size];
        }

        public void Push(T item)
        {
            lock (this)
            {
                buff[(head + count) % size] = item; //wrzucamy element do kolejki
                if (count < size) count++; //jeśli rondo jeszcze jest nie zapełnione tylko zaznaczamy zwiększenie rozmiaru
                else head = (head + 1) % size; //jeśli jest pełno tylko przesuwamy wskaźnik głowy (poprzednia głowa została nadpisana)
            }
        }

        public T Pop()
        {
            lock (this)
            {
                //bierzemy element z kolejki, jeśli kolejka jest pusta odczytana zostanie wartość domyślna (null, 0, false itp.)
                T result = buff[head]; 
                if (count > 0) //jeśli kolejka jest niepusta
                {
                    count--; //zmniejszamy rozmiar
                    buff[head] = default(T); //zastępujemy usuwany element wartością domyślną
                    head = (head + 1) % size; //i przesuwamy wskaźnik głowy na następny element
                }
                return result;
            }
        }
    }

BTW, w DirectX też można to fajnie zrobić.

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