Reference-Counting (implicit-sharing) bei Qt Klassen
Qt verwendet eigentlich seit den Anfängen das sogenannte reference-counting, oder in Qt implicit sharing genannt, um aufwändiges Kopieren von größeren Objekten bei Funktionsaufrufen zu vermeiden. Wer die Technik nicht kennt, kann in Item 29 (“Reference Counting”) bei Scott Meyers “More Effective C++” nachlesen.
Hier ist ein Beispiel, um das reference-counting bei Qt Containerklassen zu verdeutlichen:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
QVector<double> vec1{1,2,3,4};
double * vecData = vec1.data(); // wir merken uns mal die Adresse der Daten im QVector
QVector<double> vec2 = vec1; // das Gleiche wie QVector<double> vec2(vec1);
// vec2 ist nun eine "shallow copy" vom vec1,
// d.h. ein smart-pointer (reference counting object)
qDebug() << "vec1 =" << vec1;
// wir greifen direkt auf den Speicher in vec1 zu und ändern was
vecData[1] = 15;
qDebug() << "vecData[1] = 15;";
qDebug() << "vec1 =" << vec1;
qDebug() << "vec2 =" << vec2; // siehe da, Vector 2 hat sich auch geändert
// ändern wir nun den QVector mittels operator[]
// copy-on-write erzeugt nun 2 separate Kopien des Inhalts der QVector Klassen
vec1[2] = 30;
qDebug() << "vec1[2] = 30;";
qDebug() << "vec1 =" << vec1;
qDebug() << "vec2 =" << vec2; // Vector 2 bleibt unverändert
Ausgabe:
1
2
3
4
5
6
7
vec1 = QVector(1, 2, 3, 4)
vecData[1] = 15;
vec1 = QVector(1, 15, 3, 4)
vec2 = QVector(1, 15, 3, 4)
vec1[2] = 30;
vec1 = QVector(1, 15, 30, 4)
vec2 = QVector(1, 15, 3, 4)
Beim direkten Manipulieren des Speichers in der Zeile vecData[1] = 15;
sieht man, dass der veränderte Speicherbereich von beiden Vektoren verwendet/geteilt wird, und dadurch beide Vektoren die gleiche Ausgabe bringen.
Verwendet man dagegen eine der vielen Memberfunktionen der QVector
-Klasse, so wird eine Veränderung erkannt und entsprechend copy-on-write wird dem modifizierten Vektor nun ein eigener Speicherbereich zugewiesen und nur dieser wird verwendet.
Qt Klassen und Datentransfer zwischen Threads
Beim Datentransfer von Qt Container-Klassen zwischen Threads kann man sich nun fragen, wie das mit dem reference-counting funktioniert und ob das sicher ist… Spoiler: seit Qt 4 ist das kein Problem mehr.
Auszug aus der Qt Dokumentation:
Threads and Implicitly Shared Classes
Qt uses an optimization called implicit sharing for many of its value class, notably QImage and QString. Beginning with Qt 4, implicit shared classes can safely be copied across threads, >like any other value classes. They are fully reentrant. The implicit sharing is really implicit.
In many people’s minds, implicit sharing and multithreading are incompatible concepts, because of the way the reference counting is typically done. Qt, however, uses atomic reference >counting to ensure the integrity of the shared data, avoiding potential corruption of the reference counter.
… We recommend using signals and slots to pass data between threads, as this can be done without the need for any explicit locking.
To sum it up, implicitly shared classes in Qt 4 are really implicitly shared. Even in multithreaded applications, you can safely use them as if they were plain, non-shared, reentrant >value-based classes.
Wenn man einen Qt Container einem QThread
als Datenmember übergibt, so teilt sich der im QThread
befindliche Container zunächst den gleichen Speicherbereich wie der Container im GUI Thread. Sobald aber nun einer der Threads ein Objekt ändert, wird das implicit sharing aufgelöst und die Threads arbeiten auf separaten Speicherbereichen (wie bei SingleThread-Anwendungen).
Aber wie funktioniert das, wenn man innerhalb des Threads den Speicher direkt manipuliert? Wenn man beispielsweise einen WorkerThread schreibt, in dem ein QVector<double>
an eine Berechnungsfunktion übergeben wird, die aber einen double *
Zeiger erwartet, dann könnte man sich den Adresse mittels der data()
-Memberfunktion holen, oder via &vec[0]
:
1
2
3
double * dataPtr = m_data.data();
// data() ruft detach() auf und eine Kopie wird erstellt
someCalculationFunction(dataPtr);
Will man wirklich nur lesend auf den Speicher zugreifen, sollte man constData()
verwenden:
1
2
3
const double * constData = m_data->constData(); // no detach() here!
// constData() macht keine Kopie
someCalculationFunction(dataPtr);
Sobald man eine Memberfunktion in einer Qt Klasse aufruft, die den Inhalt verändern könnte, wird eine Kopie des geteilten Speicherbereichs erzeugt.
Deep-Copy erzwingen
Man kann aber das Erstellen einer tiefen Kopie deep copy einer Qt Klasse mit implicit sharing erzwingen.
Eigentlich braucht man das nicht wirklich. Meistens kommt der Wunsch nach einem deep copy nur aus die Unsicherheit darüber, was denn mit solchen geteilten Klassenobjekten genau passiert, vor allem bei der Verwendung mit Multithreaded-Anwendungen. Wie oben schon geschildert sind die Sorgen aber unbegründet und ich selbst hab das in meinen Quelltexten bisher noch nie gebraucht.
Um copy-on-write zu erzwingen, muss man bei Qt Klassen mit reference-counting lediglich die Funktion detach()
aufrufen.
Zusammenfassung
Qt Klassen verwenden reference-counting oder implicit sharing, um unnötige Kopien von größeren Speicherbereichen zu vermeiden. Seit Qt 4 können solche Klassen ohne Probleme zwischen Threads kopiert werden. Qt kümmert sich dann darum, dass beide Threads auf eigenem Speicherbereich arbeiten.