Qt多线程编程总结(一)

Qt对线程提供了支持,基本形式有独立于平台的线程类、线程安全方式的事件传递和一个全局Qt库互斥量允许你可以从不同的线程调用Qt方法。

这个文档是提供给那些对多线程编程有丰富的知识和经验的听众的。推荐阅读:

  • Threads Primer: A Guide to Multithreaded Programming
  • Thread Time: The Multithreaded Programming Guide
  • Pthreads Programming: A POSIX Standard for Better Multiprocessing (O'Reilly Nutshell)
  • Win32 Multithreaded Programming

警告:所有的GUI类(比如,QWidget和它的子类),操作系统核心类(比如,QProcess)和网络类都是线程安全的。

QRegExp使用一个静态缓存并且也不是线程安全的,即使通过使用QMutex来保护的QRegExp对象。

线程类

最重要的类是QThread,也就是说要开始一个新的线程,就是开始执行你重新实现的QThread::run()。这和Java的线程类很相似。

为了写线程程序,在两个线程同时希望访问同一个数据时,对数据进行保护是很必要的。因此这里也有一个QMutex类,一个线程可以锁定互斥量,并且在它锁定之后,其它线程就不能再锁定这个互斥量了,试图这样做的线程都会被阻塞直到互斥量被释放。例如:

    class MyClass
    {
    public:
        void doStuff( int );
    	private:
        QMutex mutex;
        int a;
        int b;
    };

    // 这里设置a为c,b为c*2。

    void MyClass::doStuff( int c )
    {
        mutex.lock();
	 a = c;
	 b = c * 2;
	mutex.unlock();
    } 

这保证了同一时间只有一个线程可以进入MyClass::doStuff(),所以b将永远等于c * 2。

另外一个线程也需要在一个给定的条件下等待其它线程的唤醒,QWaitCondition类就被提供了。线程等待的条件QWaitCondition指出发生了什么事情,阻塞将一直持续到这种事情发生。当某种事情发生了,QWaitCondition可以唤醒等待这一事件的线程之一或全部。(这和POSIX线程条件变量是具有相同功能的并且它也是Unix上的一种实现。)例如:

    #include <qapplication.h>
    #include <qpushbutton.h>
    // 全局条件变量
    QWaitCondition mycond;
    // Worker类实现
  class Worker : public QPushButton, public QThread
    {
        Q_OBJECT
	 public:
        Worker(QWidget *parent = 0, const char *name = 0)
            : QPushButton(parent, name)
        {
            setText("Start Working");
            // 连接从QPushButton继承来的信号和我们的slotClicked()方法
            connect(this, SIGNAL(clicked()), SLOT(slotClicked()));
            // 调用从QThread继承来的start()方法……这将立即开始线程的执行
            QThread::start();
        }
	public slots:
        void slotClicked()
        {    // 唤醒等待这个条件变量的一个线程
            mycond.wakeOne();
        }
	protected:
        void run()
        {
            // 这个方法将被新创建的线程调用……
            while ( TRUE ) {
                // 锁定应用程序互斥锁,并且设置窗口标题来表明我们正在等待开始工作
                qApp->lock();
                setCaption( "Waiting" );
                qApp->unlock();
                // 等待直到我们被告知可以继续
                mycond.wait();
                // 如果我们到了这里,我们已经被另一个线程唤醒……让我们来设置标题来表明我们正在工作
                qApp->lock();
                setCaption( "Working!" );
                qApp->unlock();
                // 这可能会占用一些时间,几秒、几分钟或者几小时等等,因为这个一个和GUI线程分开的线程,在处理事件时,GUI线程不会停下来……
                do_complicated_thing();
            }
        }
    };
	// 主线程——所有的GUI事件都由这个线程处理。
    int main( int argc, char **argv )
    {
        QApplication app( argc, argv );
        // 创建一个worker……当我们这样做的时候,这个worker将在一个线程中运行
        Worker firstworker( 0, "worker" );
        app.setMainWidget( &worker );
        worker.show();
        return app.exec();
    }
  

只要你按下按钮,这个程序就会唤醒worker线程,这个线程将会进行并且做一些工作并且然后会回来继续等待被告知做更多的工作。如果当按钮被按下时,worker线程正在工作,那么就什么也不会发生。当线程完成了工作并且再次调用QWaitCondition::wait(),然后它就会被开始。

线程安全的事件传递

在Qt中,一个线程总是一个事件线程——确实是这样的,线程从窗口系统中拉出事件并且把它们分发给窗口部件。静态方法QThread::postEvent从线程中传递事件,而不同于事件线程。事件线程被唤醒并且事件就像一个普通窗口系统事件那样在事件线程中被分发。例如,你可以强制一个窗口部件通过如下这样做的一个不同的线程来进行重绘:

    QWidget *mywidget;
    QThread::postEvent( mywidget, new QPaintEvent( QRect(0, 0, 100, 100) ) );
  

这(异步地)将使mywidget重绘一块100*100的正方形区域。

Qt库互斥量

Qt库互斥量提供了从线程而不是事件线程中调用Qt方法的一种方法。例如:

  QApplication *qApp;
  QWidget *mywidget;
 
 
  qApp->lock();
  mywidget->setGeometry(0,0,100,100);
  QPainter p;
  p.begin(mywidget);
  p.drawLine(0,0,100,100);
  p.end();

  qApp->unlock();
  

在Qt中没有使用互斥量而调用一个函数通常情况下结果将是不可预知的。从另外一个线程中调用Qt的一个GUI相关函数需要使用Qt库互斥量。在这种情况下,所有可能最终访问任何图形或者窗口系统资源的都是GUI相关的。使用容器类,字符串或者输入/输出类,如果对象只被一个线程使用就不需要任何互斥量了。

告诫

当进行线程编程时,需要注意的一些事情:

  • 当使用Qt库互斥量的时候不要做任何阻塞操作。这将会冻结事件循环。

  • 确认你锁定一个递归QMutex的次数和解锁的次数一样,不能多也不能少。

  • 在调用除了Qt容器和工具类的任何东西之前锁定Qt应用程序互斥量。

  • 谨防隐含地共享类,你应该避免在线程之间使用操作符=()来复制它们。这将会在Qt的未来主要的或次要的发行版本中进行改进。

  • 谨防那些没有被设计为线程安全的Qt类,例如,QPtrList的应用程序接口就不是线程安全的并且如果不同的线程需要遍历一个QPtrList,它们应该在调用QPtrList::first()之前锁定并且在到达终点之后解锁,而不是在QPtrList::next()的前后进行锁定和解锁。

  • 确认只在GUI线程中创建的继承和使用了QWidgetQTimerQSocketNotifier的对象。在一些平台上,在某个不是GUI线程的线程中创建这样的对象将永远不会接受到底层窗口系统的事件。

  • 和上面很相似,只在GUI线程中使用QNetwork类。一个经常被问到的问题是一个QSocket是否可以在多线程中使用。这不是必须得,因为所有的QNetwork类都是异步的。

  • 不要在不是GUI线程的线程中试图调用processEvents()函数。这也包括QDialog::exec()、QPopupMenu::exec()、QApplication::processEvents()和其它一些。

  • 在你的应用程序中,不要把普通的Qt库和支持线程的Qt库混合使用。这也就是说如果你的程序使用了支持线程的Qt库,你就不应该连接普通的Qt库、动态的载入普通Qt库或者动态地连接其它依赖普通Qt库的库或者插件。在一些系统上,这样做会导致Qt库中使用的静态数据变得不可靠了。

QT通过三种形式提供了对线程的支持。它们分别是,一、平台无关的线程类,二、线程安全的事件投递,三、跨线程的信号-槽连接。这使得开发轻巧的多线程 Qt程序更为容易,并能充分利用多处理器机器的优势。多线程编程也是一个有用的模式,它用于解决执行较长时间的操作而不至于用户界面失去响应。
Qt 线程类
Qt 包含下面一些线程相关的类:
QThread 提供了开始一个新线程的方法
QThreadStorage 提供逐线程数据存储
QMutex 提供相互排斥的锁,或互斥量
QMutexLocker 是一个便利类,它可以自动对QMutex加锁与解锁
QReadWriteLock 提供了一个可以同时读写操作的锁
QReadLocker与QWriteLocker 是便利类,它自动对QReadWriteLock加锁与解锁
QSemaphore 提供了一个整型信号量,是互斥量的泛化
QWaitCondition 提供了一种方法,使得线程可以在被另外线程唤醒之前一直休眠。

Qt 高级线程类
QtConcurrent 开启线程事务
QFutureWatcher 观测线程状态
QFuture 线程启动类
QThread创建线程
为创建一个线程,子类化QThread并且重写它的run()函数,例如:
class MyThread : public QThread
 {
     Q_OBJECT
 protected:
     void run();
 };
 void MyThread::run()
 {
     ...
 }

之后调用start,Qt即可创建一个线程,并在线程中执行run()函数中代码,注意UI非线程安全的。

QtConcurrent创建线程
QtConcurrent 创建线程的方法比较多, 而且QtConcurrent 本身比较特殊,若系统有空闲线程时,它会调度空闲线程,无空闲线程时将会创建一个线程。
(注意:QtConcurrent 创建线程归QthreadPool管理,若超过最大线程数,将会进入队列等待),QtConcurrent创建线程的方法多种,以下举例map函数:
QImage scale(const QImage &image)
 {
     qDebug() < < "Scaling image in thread" << QThread::currentThread();
     return image.scaled(QSize(100, 100), Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
 }
 
 int main(int argc, char *argv[])
 {
     QApplication app(argc, argv);
 
     const int imageCount = 20;
 
     // Create a list containing imageCount images.
     QList images;
     for (int i = 0; i < imageCount; ++i)
         images.append(QImage(1600, 1200, QImage::Format_ARGB32_Premultiplied));
 
     // Use QtConcurrentBlocking::mapped to apply the scale function to all the
     // images in the list.
     QList thumbnails = QtConcurrent::blockingMapped(images, scale);
 
     return 0;
 }


Qt 线程同步
QMutex, QReadWriteLock, QSemaphore, QWaitCondition 提供了线程同步的手段。使用线程的主要想法是希望它们可以尽可能并发执行,而一些关键点上线程之间需要停止或等待。例如,假如两个线程试图同时访问同一个 全局变量,结果可能不如所愿。

QMutex
QMutex 提供相互排斥的锁,或互斥量。在一个时刻至多一个线程拥有mutex,假如一个线程试图访问已经被锁定的mutex,那么它将休眠,直到拥有mutex的线程对此mutex解锁。Mutexes常用来保护共享数据访问。
QReadWriterLock
QReadWriterLock 与QMutex相似,除了它对 “read”,”write”访问进行区别对待。它使得多个读者可以共时访问数据。使用QReadWriteLock而不是QMutex,可以使得多线程程序更具有并发性。
 QReadWriteLock lock;
 void ReaderThread::run()
 {
     lock.lockForRead();
     read_file();
     lock.unlock();
 }
 void WriterThread::run()
 {
     lock.lockForWrite();
     write_file();
     lock.unlock();
 }

QSemaphore
QSemaphore 是QMutex的一般化,它可以保护一定数量的相同资源,与此相对,一个mutex只保护一个资源。下面例子中,使用QSemaphore来控制对环状缓 冲的访问,此缓冲区被生产者线程和消费者线程共享。生产者不断向缓冲写入数据直到缓冲末端,再从头开始。消费者从缓冲不断读取数据。信号量比互斥量有更好 的并发性,假如我们用互斥量来控制对缓冲的访问,那么生产者,消费者不能同时访问缓冲。然而,我们知道在同一时刻,不同线程访问缓冲的不同部分并没有什么 危害。
 const int DataSize = 100000;
 const int BufferSize = 8192;
 char buffer[BufferSize];
 
 QSemaphore freeBytes(BufferSize);
 QSemaphore usedBytes;
 
 class Producer : public QThread
 {
 public:
     void run();
 };
 
 void Producer::run()
 {
     qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
     for (int i = 0; i < DataSize; ++i) {
         freeBytes.acquire();
         buffer[i % BufferSize] = "ACGT"[(int)qrand() % 4];
         usedBytes.release();
     }
 }
 
 class Consumer : public QThread
 {
 public:
     void run();
 };
 
 void Consumer::run()
 {
     for (int i = 0; i < DataSize; ++i) {
         usedBytes.acquire();
         fprintf(stderr, "%c", buffer[i % BufferSize]);
         freeBytes.release();
     }
     fprintf(stderr, "\n");
 }
 
 int main(int argc, char *argv[])
 {
     QCoreApplication app(argc, argv);
     Producer producer;
     Consumer consumer;
     producer.start();
     consumer.start();
     producer.wait();
     consumer.wait();
     return 0;
 }

QWaitCondition
QWaitCondition 允许线程在某些情况发生时唤醒另外的线程。一个或多个线程可以阻塞等待一QWaitCondition ,用wakeOne()或wakeAll()设置一个条件。wakeOne()随机唤醒一个,wakeAll()唤醒所有。
下面的例子中,生产者首先必须检查缓冲是否已满(numUsedBytes==BufferSize),如果是,线程停下来等待 bufferNotFull条件。如果不是,在缓冲中生产数据,增加numUsedBytes,激活条件 bufferNotEmpty。使用mutex来保护对numUsedBytes的访问。另外,QWaitCondition::wait() 接收一个mutex作为参数,这个mutex应该被调用线程初始化为锁定状态。在线程进入休眠状态之前,mutex会被解锁。而当线程被唤醒 时,mutex会处于锁定状态,而且,从锁定状态到等待状态的转换是原子操作,这阻止了竞争条件的产生。当程序开始运行时,只有生产者可以工作。消费者被 阻塞等待bufferNotEmpty条件,一旦生产者在缓冲中放入一个字节,bufferNotEmpty条件被激发,消费者线程于是被唤醒。
 const int DataSize = 100000;
 const int BufferSize = 8192;
 char buffer[BufferSize];
 
 QWaitCondition bufferNotEmpty;
 QWaitCondition bufferNotFull;
 QMutex mutex;
 int numUsedBytes = 0;
 
 class Producer : public QThread
 {
 public:
     void run();
 };
 
 void Producer::run()
 {
     qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
 
     for (int i = 0; i < DataSize; ++i) {
         mutex.lock();
         if (numUsedBytes == BufferSize)
             bufferNotFull.wait(&mutex);
         mutex.unlock();
 
         buffer[i % BufferSize] = "ACGT"[(int)qrand() % 4];
 
         mutex.lock();
         ++numUsedBytes;
         bufferNotEmpty.wakeAll();
         mutex.unlock();
     }
 }
 
 class Consumer : public QThread
 {
 public:
     void run();
 };
 
 void Consumer::run()
 {
     for (int i = 0; i < DataSize; ++i) {
         mutex.lock();
         if (numUsedBytes == 0)
             bufferNotEmpty.wait(&mutex);
         mutex.unlock();
 
         fprintf(stderr, "%c", buffer[i % BufferSize]);
 
         mutex.lock();
         --numUsedBytes;
         bufferNotFull.wakeAll();
         mutex.unlock();
     }
     fprintf(stderr, "\n");
 }
 
 int main(int argc, char *argv[])
 {
     QCoreApplication app(argc, argv);
     Producer producer;
     Consumer consumer;
     producer.start();
     consumer.start();
     producer.wait();
     consumer.wait();
     return 0;
 }

你可能感兴趣的:(Qt多线程编程总结(一))