Wykonanie funkcji współbieżnie, tak aby nie blokował się wątek

0

Witam, mam klasę w Qt, która wygląda tak:

class Board : public QWidget
{
    Q_OBJECT
public:
signals:

public slots:
     void start();
private:
     void toDo();
}

Chciałbym wykonać współbierznie funkcję toDo tak aby nie blokował mi się wątek. W tej funkcji znajduje się długa pętla, wykonująca dość zasobożerne obliczenia na różnych obiektach. NIestety mam problem z odpaleniem wątku z biblioteki Qt (QThread). Są dwie możliwości, albo dziedziczenie po QThread, implementacja metody run() i tam wciśnięcie tego kodu. Ale ja już dziedziczę po klasie QWidget więc odpada. Drugim jest takie wykorzystanie metody moveToThread ale ona nie działa. Wyrzuca jakiś błąd (nie będę go przytaczał bo ta metoda mnie nie interesuje). Skoro Qt nie dostarcza mi możliwości stworzenia wątku postanowiłem wykorzystać bibliotekę <thread>. Zrobiłem coś takiego:

void Board::start()
{
    std::thread th(&Board::run, this);
}

Program owszem się skompilował ale przy próbie wystartowania wątku program się wysypuje prawdopodobnie dlatego że nigdzie nie dałem .join(). (debugger pokazał że wykonał się warunek w bibliotece thread

if(joinable())
  std::terminate();

ale gdy dam th.join() to wątek główny czeka na wątek poboczny - a to chcę uniknąć bo blokuje mi interfejs.

Proszę o pomoc.
Pozdrawiam :)

2

Przenieś tą obszerną logikę do innej klasy, którą odpalisz w wybranym wątku. Jeśli masz w tej pętli dużo odniesień do GUI to je usuń. @MarekR22 ledwo co podlinkował fajny opis użycia klasy QThread:

MarekR22 napisał(a):

Z moim kodem, stosując mechanizm slotów i sygnałów, nie musisz stosować wątków. Na dodatek jeśli się uprzesz stosować wątki, to będzie to o wiele łatwiejsze.
Tu jest mój opis jak używać wątków w qt: http://4programmers.net/Forum/C_i_C++/210633-qt_creator_-_watki?p=918099#id918099
A tu ten z nowszej dokumentacji: http://qt-project.org/doc/qt-5.0/qtcore/qthread.html#details

http://blog.qt.digia.com/blog/2010/06/17/youre-doing-it-wrong/

0

Dzięki, to pomogło, choć wolałem tego uniknąć ale chyba nie ma wyjścia. Jeszcze jedno pytanie. Chcę aby moja klasa, która jest gdzieś tam głęboko i nie ma dostępu do GUI miała możliwość zmiany wartości w QLabel (coś się dzieje w tej klasie i ona aktualizuje go). Trzeba jakoś przekazać wskaźnik do tej klasy lub gdzieś go przechwywać globalnie. Jak to najwydajniej zrobić?

0

Najwydajniej? Przekazać wskaźnik do tego obiektu QLabel i zmienić jego wartość kiedy chcesz (pod warunkiem, że wszystko to dzieje się w wątku głównym aplikacji). Natomiast sposobem znacznie wygodniejszym i bezpieczniejszym byłoby emitowanie sygnału z odpowiednimi danymi i połączenie go z odpowiednim slotem w GUI.

0

Ok ale uzywajac np. connect jako 3 parametr musze podac obiekt do ktorego chce wyslac te dane a aktualnie w tej klasie wysylajacej nie przechowuje wskaznika do takiego obiektu bo jest tam niepotrzebny (ba, nawet jak probowalem przekazac do klasy wysylajacej wskaznik do UI to sypalo bledami)

1

QObject::connect możesz wywołać z dowolnego miejsca w kodzie. Najczęściej jest to jedna z dwóch łączonych stron, ale czasem możesz potrzebować zrobić to "z zewnątrz". Zamiast przekazywać wskaźnik do UI, spróbuj czegoś takiego:

auto * nieui = create();
QObject::connect(nieui, SIGNAL(stateChanged(QString)), ui, SLOT(setText(QString)));
0

Dzieki, a jakbym nie chcial korzystac z dobrodziejstw Qt?

0

Wtedy byś nie używał QLabel ;) Jeśli nie chcesz używać sygnałów i slotów to musisz w jakiś sposób przekazać wskaźnik do odpowiedniego QWidgetu od którego możesz się odnieść do tego elementu do twojej klasy i wywołać funkcję setText(). Z tym, że musisz wykonać to z głównego wątku aplikacji, więc nie możesz zrobić tego bezpośrednio z kodu wykonywanego w innym wątku.

0

To trochę nie dobrze, bo ja mam w metodzie run() wątku, pętlę i chcę aby w QLabelu wyświetlała mi się aktualna iteracja tej pętli. Czyli muszę po prostu zapisać aktualną iterację do pola w klasie wątku, zrobić metodę getIteration() zwracającą wskaźnik na int'a z numerem a potem przypisać to w głównym wątku do labela (tak żeby label "wyświetlał" wskaźnik (a raczej to na co wskazuje) i dzieki temu będzie automatycznie aktualizowany. Tak?

1

W tym przypadku wysłanie sygnału iterationChanged(int) wydaje się idealnym rozwiązaniem. Jeśli koniecznie chcesz zapisać do pola klasy, które będzie odczytywane z innego wątku, musisz zapewnić synchronizację (Qt robi to za Ciebie) - najlepiej przez zmienne typu atomic, ale możesz się też bawić w muteksy i inne takie.

0

Tzn, jeżeli pobiorę wskaźnik tej zmiennej zanim wystartuję wątek to chyba nie muszę mutexować go? (bo nie pobieram zmiennej cały czas (z wątku do wątku) a jedynie na samym początku).

0

Jeśli dokonujesz dereferencji wskaźnika to musisz to jakoś synchronizować. W przypadku wartości liczbowej użyj typu std::atomic<int> zamiast mutexów - jest on bezpieczny oraz prawie tak samo wydajny jak zwykły int.

0

Hmm mógłbyś pokazać jak dobrze z tego korzystać? Czym się róznią od mutexów w praktyce? W uproszczeniu mój kod wygląda tak:

http://wklej.org/id/1191203/

class B : public QThread
{
    private:
    int iteracji;
         
    void run() {
        for(int i=0; i<1000; i++) {
            iteracji = i+1;
        }
    }
    
    int * pobierzIteracje() {
        return & iteracji;
    }
}

class GUI 
{
    QLabel label;
    void ustawB()
    {
        B b;
        label.setText(intToStr(*(b.pobierzIteracje())));
        b.start();
    }
}

Dodam, że niestety nie bardzo zmienia się wraz z iteracjami zawartość Labela ;(

wstawienie kodu do posta - fp

1
 
#include <atomic>
class B : public QThread
{
    private:
    std::atomic<int> iteracji;
         
    void run() {
        for(int i=0; i<1000; i++) {
            iteracji = i+1;
        }
    }
    
    int pobierzIteracje() {
        return iteracji.load();
    }
}

Wymaga kompilatora zgodnego z C++11.

Ale w tym kodzie masz dwa większe problemy:

  1. Wartość iteracji pobierasz raz, zamiast robić to wielokrotnie - dlatego label będzie zawsze pokazywał wartość początkową. Wrzucanie pętli sprawdzającej tutaj jest bezcelowe, ponieważ zablokujesz tylko UI, dlatego powinieneś użyć QTimera, który by był odpalany co pewien czas (np. do momentu złapania sygnału finished() Twojej klasy), albo jednak emitować sygnał przy zmianie wartości zmiennej iteracji - wtedy nie musisz się bawić w atomicsy czy muteksy. Tak czy inaczej UI bez sygnałów i slotów łatwo nie zmienisz.
  2. B jest zmienną o automatycznym czasie istnienia - po wyjściu z funkcji ustawB() jest niszczone, a dokumentacja Qt mówi:

Note that deleting a QThread object will not stop the execution of the thread it manages. Deleting a running QThread (i.e. isFinished() returns false) will probably result in a program crash. Wait for the finished() signal before deleting the QThread.

Przy okazji QString ma metodę statyczną number, więc możesz jej użyć zamiast własnej: QString::number(42)

0

To był taki bardziej pseudokod, bo ten mój jest trochę bardziej rozbudowany a chciałem aby to było bardziej przejrzyste.

Ad.1

Wartość iteracji pobierasz raz, zamiast robić to wielokrotnie - dlatego label będzie zawsze pokazywał wartość początkową

Hm myślałem że skoro przekazuje wskaźnik a nie wartość to będzie się aktualizowało automatycznie.

Ad.2.
Trzymam wskaźnik do obiektu B, jako pole klasy i wątek działa dobrze. :)

Timer to nie jest zły pomysł ale skoro jest możliwość emitowania sygnału to chyba się na to zdecyduje skoro uważasz że jest to najlepszy pomysł i nie będę musiał korzystać z std::atomic. Mam wykorzystać konstrukcję connect(ob, singal(), ob2, slot()) ?

1

Hm myślałem że skoro przekazuje wskaźnik a nie wartość to będzie się aktualizowało automatycznie.

  1. Przekazujesz wskaźnik, ale od razu dokonujesz dereferencji, więc równie dobrze mógłbyś przekazać wartość.
  2. Zamieniasz wartość na string - więc nawet jeśli by ten wskaźnik jakoś magicznie sam się update'ował, to string już nie

Mam wykorzystać konstrukcję connect(ob, singal(), ob2, slot()) ?

Mniej więcej. Na przykładzie B:

class B : public QThread
{
    Q_OBJECT
    private:
    int iteracji;
 
    void run() {
        for(int i=0; i<1000; i++) {
            
            iteracji = i+1;
            emit zmienilaSieIloscIteracji(iteracji);
        }
    }
 
    int pobierzIteracje() {
        return iteracji.load();
    }
    
    signals:
    void zmienilaSieIloscIteracji(int);
}

Sygnału nie musisz implementować - Qt zrobi to za Ciebie.

Wtedy w definicji GUI wystarczy dodać:

slots:
void gdyZmienilaSieIloscIteracji(int);

Ustawienie wartości jest trywialne.

W powyższym przypadku połączenie nawiążesz w ten sposób: connect(b,SIGNAL(zmienilaSieWartoscIteracji(int)),gui,SLOT(gdyZmienilaSieWartoscIteracji(int)));

PS: zakładam, że GUI oczywiście dziedziczy po QWidget i ma makro Q_OBJECT, tylko zapomniałeś tego wrzucić do pseudokodu.

0

Tak. Dzieki! Niesamowity mechanizm. Czy mozna w ten sposob przekazywac rozne parametry programu, opcje itp? Tzn moc pewnie mozna ale czy to nie jest naduzycie?

PS troche sie za bardzo ucieszylem bo widze ze musze miec zawsze dostep przeciez do obu obiektow obu klas. Gdy bede mial klase ktorej obiektu nie bedzie w gui to ten mechanizm nie zadziala.

0

Qt tego praktycznie wszędzie używa. Sygnały i sloty Qt są wygodnym rozwiązaniem do zapewnienia bezpieczeństwa w aplikacjach wielowątkowych, obsługi zdarzeń (eventów) oraz do minimalizacji zależności (połączone klasy nie muszą nic o sobie wiedzieć, a i tak mogą się w ten sposób porozumiewać). Jeśli nic z tego nie potrzebujesz to używaj zwykłych funkcji. W opisanym powyżej przypadku wszystkie trzy powody mogą być prawdziwe ;)

0

No niestety chyba nie, bo klasa w ktorej wywolujemy connect musi znac obiekty B oraz GUI. Jezeli wywolamy connect w Gui to musi znac B. Jezeli w B to musi znac Gui. Jezeli klasa B siedziala y w jeszcze innej klasie, ktora siedzialaby w jeszcze innej to chyba to nie zadziala.

1

Teoretycznie możesz wołać connect podając wyłącznie wskaźniki do QObject, ale nawet w pierwszym podanym przez Ciebie przypadku B oraz GUI nie muszą się znać. Tak czy inaczej, zostaje obsługa zdarzeń oraz bezpieczne przesyłanie wiadomości między różnymi wątkami.

0

Niestety zaprezentowany przez Ciebie sposób nie działa. Zrobiłem dokładnie jak napisałeś i wyrzuca błąd:

błąd:undefined reference to `vtable for B'
In function `B::run()':
błąd:undefined reference to `B::iterationChanged(int)'
:-1: błąd:collect2: error: ld returned 1 exit status

connect w moim przypadku (slot w GUI i signal w B nazwalem tak samo ale to chyba nie ma znaczenia)

connect(B, SIGNAL(iterationChanged(int)), this, SLOT(iterationChanged(int)));
0

Próbujesz z klasy B wysłać sygnał klasy Simulation ?

W każdym razie, każda klasa, która ma mieć własne sygnały/sloty musi mieć makro Q_OBJECT w definicji oraz dziedziczyć po klasie QObject (nie musi być bezpośrednio).

Po zdefiniowaniu nowych sygnałów/slotów musisz ponownie uruchomić QMake, aby wygenerować pliki moc dla tych klas (klasy bez makra Q_OBJECT zostaną zignorowane).

Sloty musisz zdefiniować jak funkcje, ciała sygnałów zostaną utworzone automatycznie.

0

Zapomniałem poprawić po prostu w poście Simulation na B.
Klasa GUI dziedziczy po QWidget, klasa B (u mnie nazywa się SImulation) dziedziczy po QThread. Reszta jest identycznie jak u Ciebie w poście, jest makro Q_OBJECT, jest zadeklarowany signal

class Simulation : public QThread
{
    Q_OBJECT
public:
    Simulation(Board * b);
    void pause();
    void resume();
    void stop();

signals:
    void iterationChanged(int);
public slots:

private:
protected:
    void run();
};

Natomiast emituję go tak jak pokazałeś w metodzie run()

1

Czy connect masz do nazwy klasy, czy do wskaźnika na jej instancję? W poprzednim poście masz connect(B,/.../) i klasę o nazwie B.

Definicja klasy Simulation wygląda ok. Sprawdź czy plik nagłówkowy z nią znajduje się w pliku .pro projektu i odpal QMake ponownie.

edit: jeśli masz tą klasę zdefiniowaną w pliku .cpp to QMake może nie wygenerować metainformacji o niej.

0

Faktycznie wystarczyło Qmake od nowa uruchomić. Teraz mam tylko problem taki że nie wypisuje mi iteracji:

void GUI::iterationChanged(int i)
{
    setWindowTitle(QString("Iteracja: %1").arg(i));
}

Co moze byc zle?

1

Może niepoprawny connect?

0

Wydaje się ok, to nie jedyny connect jakiego tam używam, inne działają

connect(simulation, SIGNAL(iterationChanged(int)), this, SLOT(iterationChanged(int)));

//edit, ok pomoglo zmienie lokalizacji connecta. Dzieki wielkie, bardzo mi pomogles. Pzdr

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