380 likes | 468 Views
Qt – Chapter 18: Multithreading. Tworzenie nowego wątku polega na utworzeniu klasy dziedziczącej po QThread i nadpisanie w niej metody run(). class Thread : public QThread { Q_OBJECT public: Thread(); void setMessage(const QString &message); void stop(); protected: void run();
E N D
Qt – Chapter 18: Multithreading
Tworzenie nowego wątku polega na utworzeniu klasy dziedziczącej po QThread i nadpisanie w niej metody run(). class Thread : public QThread { Q_OBJECT public: Thread(); void setMessage(const QString &message); void stop(); protected: void run(); private: QString messageStr; volatile bool stopped; }; Tworzenie wątków
Thread::Thread() { stopped = false; } void Thread::run() { while (!stopped) cerr << qPrintable(messageStr); stopped = false; cerr << endl; } void Thread::stop() { stopped = true; }
Tworzenie prostej aplikacji uruchamiającej wątki. class ThreadDialog : public QDialog { Q_OBJECT public: ThreadDialog(QWidget *parent = 0); protected: void closeEvent(QCloseEvent *event); private slots: void startOrStopThreadA(); void startOrStopThreadB(); private: Thread threadA; Thread threadB; QPushButton *threadAButton; QPushButton *threadBButton; QPushButton *quitButton; };
ThreadDialog::ThreadDialog(QWidget *parent) : QDialog(parent) { threadA.setMessage("A"); threadB.setMessage("B"); threadAButton = new QPushButton(tr("Start A")); threadBButton = new QPushButton(tr("Start B")); quitButton = new QPushButton(tr("Quit")); quitButton->setDefault(true); connect(threadAButton, SIGNAL(clicked()), this, SLOT(startOrStopThreadA())); connect(threadBButton, SIGNAL(clicked()), this, SLOT(startOrStopThreadB())); }
void ThreadDialog::startOrStopThreadA() { if (threadA.isRunning()) { threadA.stop(); threadAButton->setText(tr("Start A")); } else { threadA.start(); threadAButton->setText(tr("Stop A")); } } void ThreadDialog::startOrStopThreadB() { if (threadB.isRunning()) { threadB.stop(); threadBButton->setText(tr("Start B")); } else { threadB.start(); threadBButton->setText(tr("Stop B")); } }
void ThreadDialog::closeEvent(QCloseEvent *event) { threadA.stop(); threadB.stop(); threadA.wait(); threadB.wait(); event->accept(); } Przykładowe działanie: AAAAABBBBBAAAAABBBBBABABABBBBBBBBBBBBBBBBAAAAAAAAAAA...
QMutex QReadWriteLock QSemaphore QWaitCondition Synchronizacja
Obiekty klasy QMutex służą do zablokowania dostępu do danych lub części kodu w momencie gdy tylko jeden wątek może mieć do nich dostęp w tym samym czasie, innymi słowy służy do organizowania sekcji krytycznych. Obiekty typu QMutex posiadają metody: lock() - blokowanie muteksu, ew. jeśli obiekt jest już zablokowany czekanie aż zostanie zwolniony i będzie można go zablokować tryLock() - próba zablokowania muteksu, zwraca true jeśli się udało lub false jeśli obiekt już jest zablokowany unlock() - odblokowanie obiektu QMutex
Modyfikując poprzedni przykład: class Thread : public QThread { ... private: ... QMutex mutex; }; void Thread::stop() { mutex.lock(); stopped = true; mutex.unlock(); }
void Thread::run() { forever { mutex.lock(); if (stopped) { stopped = false; mutex.unlock(); break; } mutex.unlock(); cerr << qPrintable(messageStr); } cerr << endl; }
Jednak aby nie trzeba było pamiętać o odblokowywaniu muteksu wygodniej jest użyć obiektów klasy QMutexLocker. void Thread::run() { forever { { QMutexLocker locker(&mutex); if (stopped) { stopped = false; break; } } cerr << qPrintable(messageStr); } cerr << endl; } void Thread::stop() { QMutexLocker locker(&mutex); stopped = true; }
Obiekty typu QMutex umożliwiają blokowanie obiektów tylko dla jednego wątku. Jeśli chcemy zablokować jakiś obiekt do jednoczesnego czytania dla wielu wątków możemy użyć klasy QReadWriteLock. QReadWriteLock
MyData data; QReadWriteLock lock; void ReaderThread::run() { ... lock.lockForRead(); dostep_do_danej_bez_jej_modyfikacji(&data); lock.unlock(); ... } void WriterThread::run() { ... lock.lockForWrite(); modyfikacja_danych(&data); lock.unlock(); ... }
Analogicznie do QMutexLocker istnieją również klasy QReadLocker i QWriteLocker służące do blokowania obiektów klasy QReadWriteLock odpowiednio do odczytu i do zapisu.
QSemaphore jest kolejną klasą służącą do blokowania dostępu do danych dla wielu wątków. W odróżnieniu od poprzednich w semaforach można ustawi liczbę obiektów jakie mogą jednocześnie być udostępnione dla wątków. QSemaphore semaphore(1); | QMutex mutex; semaphore.acquire(); | mutex.lock(); semaphore.release(); | mutex.unlock(); Semafory najczęściej są wykorzystywane do zażądzania dostępem do wielu danych np. problem Producent-Konsumer. QSemaphore
Problem Producent-Konsumer: const int DataSize = 10; const int BufferSize = 4; int buffer[BufferSize]; Wątek Producer cyklicznie wypełnia tablice buffer, natomiast wątek Consumer odczytuje wpisane znaki.
QSemaphore freeSpace(BufferSize); QSemaphore usedSpace(0); void Producer::run() { for (int i = 0; i < DataSize; ++i) { freeSpace.acquire(); buffer[i % BufferSize]=i; cerr << ”Producer zapisal: ” << i << endl; usedSpace.release(); } } void Consumer::run() { for (int i = 0; i < DataSize; ++i) { usedSpace.acquire(); cerr << ”Consumer odczytal: ” << buffer[i % BufferSize] << endl; freeSpace.release(); } cerr << endl; }
int main() { Producer producer; Consumer consumer; producer.start(); consumer.start(); producer.wait(); consumer.wait(); return 0; }
Przykładowe działanie programu: Producer zapisal: 0 Consumer odczytal: 0 Producer zapisal: 1 Consumer odczytal: 1 Producer zapisal: 2 Consumer odczytal: 2 Producer zapisal: 3 Consumer odczytal: 3 ... lub: Producer zapisal: 0 Producer zapisal: 1 Producer zapisal: 2 Producer zapisal: 3 Consumer odczytal: 0 Consumer odczytal: 1 Consumer odczytal: 2 Consumer odczytal: 3 Producer zapisal: 4 ...
Kolejną klasą umożliwaijącą synchronizację wątków jest QWaitCondition. Obiekty tej klasy umożliwiają pobudzanie innych wątków przy spełnieniu określonych warunków. Aby przybliżyć sposób jej działania zmodyfikujmy kod z programu Producent-Konsument. const int DataSize = 10; const int BufferSize = 4; int buffer[BufferSize]; QWaitCondition bufferIsNotFull; QWaitCondition bufferIsNotEmpty; QMutex mutex; int usedSpace = 0; QWaitCondition
void Producer::run() { for (int i = 0; i < DataSize; ++i) { mutex.lock(); while (usedSpace == BufferSize) bufferIsNotFull.wait(&mutex); buffer[i % BufferSize] = i; ++usedSpace; bufferIsNotEmpty.wakeAll(); mutex.unlock(); } } Jeśli mamy tylko jednego producenta pętle while można zamienić na warunek if. if (usedSpace == BufferSize) { mutex.unlock(); bufferIsNotFull.wait(); mutex.lock(); }
void Consumer::run() { for (int i = 0; i < DataSize; ++i) { mutex.lock(); while (usedSpace == 0) bufferIsNotEmpty.wait(&mutex); cerr << buffer[i % BufferSize]; --usedSpace; bufferIsNotFull.wakeAll(); mutex.unlock(); } cerr << endl; }
Wszystkie wątki w jednym programie mają wspólną przestrzeń adresową, dlatego mogą korzystać z tych samych zmiennych globalnych. Czasem jednak przydatne jest posiadanie kopii niektórych danych osobno dla każdego wątku. Do tego celu służy obszar danych zwany thread-local storage (TLS) lub thread-specific data. Zmienne przechowywane w tym obszarze są duplikowane dla każdego wątku i każdy wątek może je modyfikować bez wpływu na pozostałe wątki. Thread-local storage
W Qt do organizacji obszaru TLS może służyć klasa QThreadStorage<T> QThreadStorage<QHash<int, double> *> cache; void insertIntoCache(int id, double value) { if (!cache.hasLocalData()) cache.setLocalData(new QHash<int, double>); cache.localData()->insert(id, value); } void removeFromCache(int id) { if (cache.hasLocalData()) cache.localData()->remove(id); } QThreadStorage<T>
Komunikacji wątku głównego z innym wątkiem powinna odbywać się przy wykorzystaniu sygnałów i slotów tak aby uniknąć blokowania interfejsu użytkownika. Mechanizm ten zostanie przedstawiony na przykładzie aplikacji Image Pro. Komunikacja z głównym wątkiem
ImageWindow::ImageWindow() { imageLabel = new QLabel; imageLabel->setBackgroundRole(QPalette::Dark); imageLabel->setAutoFillBackground(true); imageLabel->setAlignment(Qt::AlignLeft| Qt::AlignTop); setCentralWidget(imageLabel); createActions(); createMenus(); statusBar()->showMessage(tr("Ready"), 2000); connect(&thread, SIGNAL(transactionStarted( const QString &)), statusBar(), SLOT(showMessage(const QString &))); connect(&thread, SIGNAL(finished()), this, SLOT(allTransactionsDone())); setCurrentFile(""); }
void ImageWindow::flipHorizontally() { addTransaction(new FlipTransaction(Qt::Horizontal)); } void ImageWindow::addTransaction( Transaction *transact) { thread.addTransaction(transact); openAction->setEnabled(false); saveAction->setEnabled(false); saveAsAction->setEnabled(false); }
void ImageWindow::allTransactionsDone() { openAction->setEnabled(true); saveAction->setEnabled(true); saveAsAction->setEnabled(true); imageLabel>setPixmap(QPixmap::fromImage( thread.image())); setWindowModified(true); statusBar()->showMessage(tr("Ready"), 2000); }
class TransactionThread : public QThread { Q_OBJECT public: void addTransaction(Transaction *transact); void setImage(const QImage &image); QImage image(); signals: void transactionStarted( const QString &message); protected: void run(); private: QMutex mutex; QImage currentImage; QQueue<Transaction *> transactions; };
void TransactionThread::addTransaction( Transaction *transact) { QMutexLocker locker(&mutex); transactions.enqueue(transact); if (!isRunning()) start(); } void TransactionThread::setImage(const QImage &image) { QMutexLocker locker(&mutex); currentImage = image; } QImage TransactionThread::image() { QMutexLocker locker(&mutex); return currentImage; }
void TransactionThread::run() { Transaction *transact; forever { mutex.lock(); if (transactions.isEmpty()) { mutex.unlock(); break; } QImage oldImage = currentImage; transact = transactions.dequeue(); mutex.unlock(); emit transactionStarted(transact->message()); QImage newImage = transact->apply(oldImage); delete transact; mutex.lock(); currentImage = newImage; mutex.unlock(); } }
class Transaction { public: virtual ~Transaction() { } virtual QImage apply(const QImage &image) = 0; virtual QString message() = 0; }; class FlipTransaction : public Transaction { public: FlipTransaction(Qt::Orientation orientation); QImage apply(const QImage &image); QString message(); private: Qt::Orientation orientation; };
QImage FlipTransaction::apply(const QImage &image) { return image.mirrored( orientation == Qt::Horizontal, orientation == Qt::Vertical); } QString FlipTransaction::message() { if (orientation == Qt::Horizontal) { return QObject::tr( "Flipping image horizontally..."); } else { return QObject::tr( "Flipping image vertically..."); } }
Ze względu na współbieżność klasy możemy podzielić na: 1) thread-safe – wszystkie metody klasy mogą być wywołane jednocześnie z różnych wątków bez ingerencji ze sobą, nawet wtedy gdy działają na tym samym obiekcie 2) reentrant – różne instancje tego samego obiektu mogą być używane jednocześnie w różnych wątkach Bezpieczeństwo
QObject należy do klas typu reentrant przy czym należy pamiętać o kilku rzeczach: - dzieci klasy QObject muszą być utworzone w ich ojcowskim wątku - należy usuwać wszystkie obiekty QObject utworzone w danym wątku przed jego usunięciem - obiekty QObject muszą być usuwane w wątku, który je stworzył (jeśli nie jest to możliwe należy użyć funkcji QObject::deleteLater()
Komunikacja z obiektami klas, które nie są reentrant musi odbywać się za pomocą mechanizmu slotów i sygnałów lub poprzez wywołanie funkcji QMetaObject::invokeMethod(). void MyThread::run() { ... QMetaObject::invokeMethod(label, SLOT(setText(const QString &)), Q_ARG(QString, "Hello")); ... }