2012-07-01 17 views
17

Mam QThread, który generuje dość dużą ilość danych regularnie (kilka megabajtów na sekundę) i musi przesłać go do wątku nadrzędnego (GUI).Wysyłanie dużej ilości danych między wątkami Qt

Obawiam się, że nie jestem pewien w wewnętrznych działaniach QThread, więc chciałbym poprosić o najlepszą praktykę.

Oczywiście, najbardziej bezpośrednim sposobem przesyłania danych jest tylko jedna tablica o numerze emit. Jaka jest jednak skuteczność? Czy Qt wie o tym, gdzie jest używany i unika głębokiego kopiowania go podczas wysyłania i odbierania?

Jeśli nie, mogę po prostu przydzielić pamięć w głównym wątku i podać wskaźnik do wątku podrzędnego, w którym zapisze dane (i tylko emit krótkich wiadomości o postępie). Nie wydaje mi się to najbardziej eleganckie rozwiązanie, dlatego proszę.

Jeśli Qt unika kopiowania danych w wielu buforach podczas emitowania i odbierania, czy jest to gwarantowane we wszystkich systemach? Nie mam zasobów, aby wypróbować testy porównawcze w różnych systemach operacyjnych.

+0

Musimy wiedzieć więcej. Czy wątek główny może zostać utracony? Co tak czy owak robi główny wątek z danymi? Ale bez względu na twoje potrzeby, nie mogę uwierzyć, że emitowanie tablicy jest optymalnym rozwiązaniem. – TonyK

Odpowiedz

32

Nie ma znaczenia, czy pętle zdarzeń działają. Gdy masz emit sygnał z QObject, który żyje w wątku innym niż obiekt gniazda, sygnał zostanie wysłany jako QMetaCallEvent do kolejki zdarzeń odbieranego wątku. Pętla zdarzeń działająca w odbierającym wątku będzie działała na to zdarzenie i wykona wywołanie w gnieździe, które zostało podłączone do emitowanego sygnału.

Tak więc, niezależnie od tego, co się stanie, jakiekolwiek dane, które wyślesz przez sygnał, ostatecznie staną się ładunkiem w instancji klasy pochodnej QEvent.

Mięsem problemu jest moment, w którym QMetaCallEvent osiągnie pętlę zdarzeń, a pojemnik zostanie przekazany do gniazda jako argument. Oczywiście konstruktorzy kopii mogliby się nazywać wiele razy po drodze.Poniżej jest jakiś prosty kod, który pokazuje, ile razy konstruktor kopiujący i domyślne konstruktora są w rzeczywistości nazywa

  • na elementach członków danych o niejawnie wspólnego pojemnika copy-on-write (QVector)

  • na niestandardowej klasie, która zastępuje kontener.

Będziesz mile zaskoczony :)

Ponieważ kontenery Qt są niejawnie udostępniane kopiowanie przy zapisie, ich budowa egzemplarz ma znikomy koszt: wszystko, co się stało, to licznik jest zwiększany odniesienia atomowo na budowie . Na przykład żaden z członków danych nie jest kopiowany.

Niestety, przed 11 C++ pokazuje swoją paskudną stronę: jeśli kod slotu modyfikuje kontener w jakikolwiek sposób, nie ma możliwości przekazania referencji do gniazda w taki sposób, aby kompilator wiedział, że oryginalny pojemnik jest już nie potrzebne. Tak więc: jeśli slot otrzymuje stałe odniesienie do kontenera, masz gwarancję, że nie zostaną wykonane żadne kopie. Jeśli slot otrzyma możliwą do zapisania kopię kontenera i, zostanie zmodyfikowana, zostanie wykonana całkowicie niepotrzebna kopia, ponieważ instancja żywa na stronie wywołania nie jest już potrzebna. W C++ - 11 przekazałbyś wartość rurnu jako parametr. Przekazywanie referencji rvalue w wywołaniu funkcji kończy czas życia przekazanego obiektu w wywołującym.

Przykładowy kod wyjściowy:

"Started" copies: 0 assignments: 0 default instances: 0 
"Created Foo" copies: 0 assignments: 0 default instances: 100 
"Created Bar" copies: 0 assignments: 0 default instances: 100 
"Received signal w/const container" copies: 0 assignments: 0 default instances: 100 
"Received signal w/copy of the container" copies: 0 assignments: 0 default instances: 100 
"Made a copy" copies: 100 assignments: 1 default instances: 101 
"Reset" copies: 0 assignments: 0 default instances: 0 
"Received signal w/const class" copies: 2 assignments: 0 default instances: 1 
"Received signal w/copy of the class" copies: 3 assignments: 0 default instances: 1 
//main.cpp 
#include <QtCore> 

class Class { 
    static QAtomicInt m_copies; 
    static QAtomicInt m_assignments; 
    static QAtomicInt m_instances; 
public: 
    Class() { m_instances.fetchAndAddOrdered(1); } 
    Class(const Class &) { m_copies.fetchAndAddOrdered(1); } 
    Class & operator=(const Class &) { m_assignments.fetchAndAddOrdered(1); return *this; } 
    static void dump(const QString & s = QString()) { 
     qDebug() << s << "copies:" << m_copies << "assignments:" << m_assignments << "default instances:" << m_instances; 
    } 
    static void reset() { 
     m_copies = 0; 
     m_assignments = 0; 
     m_instances = 0; 
    } 
}; 

QAtomicInt Class::m_instances; 
QAtomicInt Class::m_copies; 
QAtomicInt Class::m_assignments; 

typedef QVector<Class> Vector; 

Q_DECLARE_METATYPE(Vector) 

class Foo : public QObject 
{ 
    Q_OBJECT 
    Vector v; 
public: 
    Foo() : v(100) {} 
signals: 
    void containerSignal(const Vector &); 
    void classSignal(const Class &); 
public slots: 
    void sendContainer() { emit containerSignal(v); } 
    void sendClass() { emit classSignal(Class()); } 
}; 

class Bar : public QObject 
{ 
    Q_OBJECT 
public: 
    Bar() {} 
signals: 
    void containerDone(); 
    void classDone(); 
public slots: 
    void containerSlotConst(const Vector &) { 
     Class::dump("Received signal w/const container"); 
    } 
    void containerSlot(Vector v) { 
     Class::dump("Received signal w/copy of the container"); 
     v[99] = Class(); 
     Class::dump("Made a copy"); 
     Class::reset(); 
     Class::dump("Reset"); 
     emit containerDone(); 
    } 
    void classSlotConst(const Class &) { 
     Class::dump("Received signal w/const class"); 
    } 
    void classSlot(Class) { 
     Class::dump("Received signal w/copy of the class"); 
     emit classDone(); 
     //QThread::currentThread()->quit(); 
    } 
}; 

int main(int argc, char ** argv) 
{ 
    QCoreApplication a(argc, argv); 
    qRegisterMetaType<Vector>("Vector"); 
    qRegisterMetaType<Class>("Class"); 

    Class::dump("Started"); 
    QThread thread; 
    Foo foo; 
    Bar bar; 
    Class::dump("Created Foo"); 
    bar.moveToThread(&thread); 
    Class::dump("Created Bar"); 
    QObject::connect(&thread, SIGNAL(started()), &foo, SLOT(sendContainer())); 
    QObject::connect(&foo, SIGNAL(containerSignal(Vector)), &bar, SLOT(containerSlotConst(Vector))); 
    QObject::connect(&foo, SIGNAL(containerSignal(Vector)), &bar, SLOT(containerSlot(Vector))); 
    QObject::connect(&bar, SIGNAL(containerDone()), &foo, SLOT(sendClass())); 
    QObject::connect(&foo, SIGNAL(classSignal(Class)), &bar, SLOT(classSlotConst(Class))); 
    QObject::connect(&foo, SIGNAL(classSignal(Class)), &bar, SLOT(classSlot(Class))); 
    QObject::connect(&bar, SIGNAL(classDone()), &thread, SLOT(quit())); 
    QObject::connect(&thread, SIGNAL(finished()), &a, SLOT(quit())); 
    thread.start(); 
    a.exec(); 
    thread.wait(); 
} 

#include "main.moc" 
+2

Wow - to dość wyczerpująca odpowiedź! –

+0

Świetny przykład sortowania wspólnych kontenerów Qt w połączeniu z 'QThread'. Nawet dostał trochę miłości C++ - 11 jako bonus. Rewizja. –

5

Podczas przesyłania dużych buforów, jest to "tradycyjny" do new() obiektów buforowych w wątku producenta, a po załadowaniu do kolejki/emitowania/bez względu na * bufor do wątku konsumenta i natychmiast nowy() inny, (do tego samego * bufora zmiennego), dla następnego obciążenia danych.

Problem: jeśli wątek GUI nie może nadążyć, otrzymasz pamięć, której nie ma w pamięci, chyba że podejmiesz pewną kontrolę przepływu (np. Wstępne przydzielenie puli buforów * i ich "krążenie").

Zazwyczaj wstępnie alokuję niektóre instancje bufora w pętlę (do tysięcy na dużym serwerze) i przekazuję ich wystąpienia do "puli pool" producenta-konsumenta. Jeśli wątek potomny chce załadować dane z jakiegoś połączenia sieciowego do bufora, musi wyskoczyć z puli i załadować go. Może następnie kolejkować/emitować/niezależnie od bufora do wątku konsumenta i wysuwać inny bufor na więcej danych, które mogą wejść. Wątek konsumenta pobiera bufer, przetwarza dane i przesyła "użyty" bufor z powrotem do kolejki ponowne użycie. Zapewnia to kontrolę przepływu: jeśli wątek potomny ładuje bufory szybciej niż wątek konsumenta może je przetworzyć, to wyszuka pulę pustą i zablokuje ją, dopóki wątek konsumenta nie zwróci niektórych używanych buforów, więc ograniczył wykorzystanie bufora/pamięci (a także unikanie ciągłego nowego/usuwania lub GC w językach, które go wspierają).

Chciałbym zrzucić liczbę kolejek w basenie do paska stanu GUI na 1-sekundowym zegarze - pozwala mi to obserwować użycie bufora (i szybko wykryć wyciek :).

+0

Jaka jest decydująca zaleta w przydzielaniu pamięci wewnątrz wątku potomnego i przekazywaniu wskaźnika do głównej, a przydzielaniu w głównym i przekazywaniu wskaźnika do elementu potomnego podczas tworzenia? – vsz

+1

Wątek podrzędny jest producentem danych - wie, kiedy bufor jest pełny i kiedy należy umieścić w kolejce jego wskaźnik i utworzyć/zdebokować inny bufor *. Wątek GUI, użytkownik, nie musi znać alokacji bufora wątków potomnych ani zarządzać nią - może swobodnie przetwarzać bufory, gdy się one pojawią, wiedząc, że wątek podrzędny całkowicie porzucił jego użycie i jest albo bezczynny, albo zapełnia się inna instancja bufora. Tak długo, jak dziecko natychmiast tworzy/depools nową instancję bufora po kolejkowaniu jednego z nich, nie ma szans, aby te dwa wątki mogły uzyskać dostęp do tej samej instancji bufora. –

+2

Btw prostą metodą uniknięcia przecieku pamięci jest wysyłanie shared_ptr (lub jeśli wolisz Qt APIs, QSharedDataPointer) zamiast surowego wskaźnika C++. W ten sposób, niezależnie od tego, co się stanie, wiesz, że tablica zostanie zwolniona, gdy oba wątki już jej nie używają. –

Powiązane problemy