Qt多线程学习

来源:互联网 发布:手机淘宝怎么摇一摇 编辑:程序博客网 时间:2024/06/05 22:59

一、QT多线程

1、Qt线程的创建

Qt线程中有一个公共的抽象类,所有的线程都是从这个QThread抽象类中派生的,要实现QThread中的纯虚函数run(),run()函数是通过start()函数来实现调用的。
 
class  MyThread :  public  QThread {
     public :
         virtual   void  run();
    };


     void  MyThread::run()
    {
         for (  int  count  =   0 ; count  <   20 ; count ++  ) {
           sleep(  1  );
           qDebug(  " Ping! "  );
       }
    }


    int  main()
    {
       MyThread a;
       MyThread b;


       a.start(); // 自动调用run(),否则即使该线程创建,也是一开始就挂起 
        b.start();
                  
        a.wait(); // 要等待线程a,b都退出,两个条件发生就退出:run函数返回或超时(wait函数的参数)
        b.wait();
    }


2、用例子来理解多线程

实例代码1:
class MThread :public QThread  
{  
public:  
    MThread();  
    ~MThread();  
    virtual void run();  
    void foo();  
    ...  
       
};

Point-1:QThread类的实例与普通类的实例没什么不同,只是运行着的run()函数会不同



实例代码2:
 class MDialog :public QDialog  
 {  
     ...  
     MThread *mythread;  
 };  
 MDialog::MDialog()  
 {  
     mythread = new MThread;  
     ...   
 }

需要注意的是,在QT中,QThread对象的实例mythread是属于创建它的线程(线程A,即MDialog所在的线程)的,mythread的所有程序代码与数据都放在与MDialog相同的空间中.这时的mythread,就像任何普通的自己定义的类的实例一样.但是在调用mythread->start()之后,mythread的run()函数中的代码会在新的线程(线程B)中执行.在run()函数中声明的变量\实例化的对象,都属于线程B.但是mythread的所有代码,都还在存储在线程A中,只是run()函数的"执行"是在线程B中.


在MDialog中,使用
 mythread->foo();
foo()是在线程A中执行的.
 
在MDialog中使用
 connect(this, SIGNAL(sigDialogSignal()), mythread, SLOT(slotThreadSlot()));
当emit sigDialogSignal()时,是会在MDialog所在的线程A中执行的.因为mythread与MDialog同属于一个线程, 这时thread可以看做一个普通类的实例.另外,因为connect函数的连接方式默认是自动连接,而对同属于一个纯种的两个对象,自动连接会使用直接连接,即slot在发出signal的线程中立即执行.


实例代码3:
 #include "mthread.h"  
 #include <QDebug>  
 MThread::MThread(QObject *parent)  
     : QThread(parent)  
 {  
     myTimer.start(1);  
     connect(&myTimer, SIGNAL(timeout()), this, SLOT(slotPrint()));  
 }  
   
 MThread::~MThread()  
 {  
   
 }  
 void MThread::run()  
 {     
     for (int i = 0; i < 100; ++i) {  
         for (int j = 0 ; j < 10000; ++j) {  
             qDebug()<<"---------"<<i;  
         }  
     }  
     exec();  
 }  
   
 void MThread::slotPrint()  
 {  
     qDebug()<<"==============================";  
   
 }


运行后出现:
 
... 
... 
---------9 
============================================================== 
---------9 
... 
...
不能误以为:在一个QThread类的派生类中,run()函数中的语句在运行时,可能被本线程定时器超时slot中断. (错误)。事实上,slotPrint()在创建MThread的实例的线程中执行,而run()函数在新的线程中运行。


Point-2:线程B中的对象要想接收线程A中的对象发来的signal, 必须进入exec(), 如在exec()前有死循环, 没有进入exec(), 则线程B中的对象不会收到signal.



实例代码4:

void MThread::run()  
{  
    while(1) {  
        dosomething();  //此循环永不退出  
    }  
    exec();             //如果此事件循环不能进入,刚此线程不会收到任何signal  
}
 
Point-3:线程A中的指针可指向线程B中创建的对象实例,  这个实例属于线程B. 指针仅仅是一个地址, 而对象实例的变量/代码等都属于线程B.


实例代码5:

class MThread : public QThread  
{  
    Q_OBJECT  
  
public:  
    MThread(QObject *parent = 0);  
    ~MThread();  
    void run();  
    MPrint *mprint;  
};  
void MThread::run()  
{  
    mprint = new MPrint;  
    exec();  
}  
//如此声明,mprint所指向的对象属于另一个线程,即原始线程.


实例代码6:
class MThread : public QThread  
{  
    Q_OBJECT  
public:  
    MThread(QObject *parent = 0);  
    ~MThread();  
    void run();  
    MPrint *mprint;  
private:  
    QTimer *myTimer;  
private slots:  
    void slotPrint();     
    void testFoo();  
};  
  
void MThread::run()  
{  
    myTimer = new QTimer;  
    mprint = new MPrint;  
    myTimer->setInterval(100);  
    connect(myTimer, SIGNAL(timeout()), this, SLOT(testFoo()), Qt::     DirectConnection);  
    QTimer::singleShot(0, myTimer,SLOT(start()));  
    exec();  
}
 
以上这样写run(),myTimer在run()中new,即myTimer这个指针属于旧线程,但myTimer所指向的QTimer实例的实体在新的线程中,testFoo()会在新线程中执行.


实例代码7:

void MThread::run()  
{  
    QTimer myTimer;  
    mprint = new MPrint;  
    myTimer.setInterval(100);  
    connect(&myTimer, SIGNAL(timeout()), this, SLOT(testFoo()), Qt::DirectConnection);  
    QTimer::singleShot(0, &myTimer,SLOT(start()));  
    //testFoo();  
    exec();  
}
 
  以上这样写run(),myTimer在run()中声明,即myTimer属于新的线程,testFoo()也会在新线程中执行.


实例代码8:

class MThread : public QThread  
{  
    Q_OBJECT  
public:  
    MThread(QObject *parent = 0);  
    ~MThread();  
    void run();  
    MPrint *mprint;  
private:  
    QTimer myTimer;  
private slots:  
    void slotPrint();     
    void testFoo();  
};  
  
void MThread::run()  
{  
    mprint = new MPrint;  
    myTimer.setInterval(100);  
    connect(&myTimer, SIGNAL(timeout()), this, SLOT(testFoo()));  
    QTimer::singleShot(0, &myTimer,SLOT(start()));  
    //testFoo();  
    exec();  
}
 
  以上这样写run(),testFoo()会在创建myTimer的老线程中执行.因为可以看到,mytimer和this(即mythread),都是在同一个线程中,只是在另一个线程中(run()),做了connect操作.
要注意的是,在线程B中启动线程A中的一个定时器,不能使用myTimer.start(),这样启动不了定时器.而应使用signal来触发start()这个slot.

Point-4:slot不会中断同线程中的slot.


实例代码9:
#include "mthread.h"  
#include <QDebug>  
MThread::MThread(QObject *parent)  
    : QThread(parent)  
{  
    myTimer.start(1);  
    connect(&myTimer, SIGNAL(timeout()), this, SLOT(slotPrint()));  
}  
  
MThread::~MThread()  
{  
}  
void MThread::run()  
{  
    exec();  
}  
void MThread::slotPrint()  
{  
    qDebug()<<"===========================";  
    for (int i = 0; i < 100; ++i) {  
        for (int j = 0 ; j < 10000; ++j) {  
            qDebug()<<"---------"<<i;  
        }  
    }  
}
slotPrint()函数运行完之后才会退出,说明slot不会中断slot,一个slot在执行完之后才会执行下一个slot.
注意:slotPrint()在创建MThread实例的线程中执行.而不是使用thread->start()创建出的那个线程.


实例代码10:
#include "mthread.h"  
#include <QDebug>  
MThread::MThread(QObject *parent)  
    : QThread(parent)  
{  
    myTimer.start(1);  
    connect(&myTimer, SIGNAL(timeout()), this, SLOT(slotPrint()));  
}  
MThread::~MThread()  
{  
}  
void MThread::run()  
{  
    testFoo();  
    exec();  
}  
void MThread::slotPrint()  
{  
    qDebug()<<"=======================";  
  
}  
  
void MThread::testFoo()  
{  
    for (int i = 0; i < 100; ++i) {  
        for (int j = 0 ; j < 10000; ++j) {  
            qDebug()<<"---------"<<i;  
        }  
    }  
}
 
以上代码中,slotPrint()与testFoo()会在两个不同的线程中执行.


二、QT线程同步

Qt 包含下面一些线程相关的类:
QThread 提供了开始一个新线程的方法
QThreadStorage 提供逐线程数据存储
QMutex   提供相互排斥的锁,或互斥量
QMutexLocker 是一个便利类,它可以自动对QMutex加锁与解锁
QReadWriterLock 提供了一个可以同时读操作的锁
QReadLocker 与QWriteLocker是便利类,它自动对QReadWriteLock 加锁与解锁
QSemaphore 提供了一个整型信号量,是互斥量的泛化
QWaitCondition 提供了一种方法,使得线程可以在被另外线程唤醒之前一直休眠。


1.QMutex
QMutex ( bool recursive = FALSE )
virtual ~QMutex ()
void lock ()  //试图锁定互斥量。如果另一个线程已经锁定这个互斥量,那么这次调用将阻塞 直到那个线程把它解锁。
void unlock ()
bool locked ()
bool tryLock () //如果另一个进程已经锁定了这个互斥量,这个函数返回假,而不是一直等到这个锁可用为止,比如,它不是阻塞的。
 // Qt 
   QMutex mutex;
    void  someMethod()
    {
      mutex. lock ();
      qDebug( " Hello " );
      qDebug( " World " );
      mutex.unlock();
    }

不过在Qt中我们可用通过另一个类来简化这种应用,因为如果使用QMutex.lock()而没有对应的使用QMutex.unlcok()的话就会造成死锁,别的线程永远也得不到接触该mutex锁住的共享资源的机会。尽管可以不使用lock()而使用tryLock(timeout)来避免因为死等而造成的死锁( tryLock(负值)==lock()),但是还是很有可能造成错误。
  对于上述的情况MFC中用CSingleLock 或 MultiLock,Boost中用boost::mutex::scoped_lock来进行解决,而在Qt中用QMutexLocker来进行解决。下面是没有采用 QMutexLocker的例子和采用 QMutexLocker的方案。
 


2.QMutexLocker
 
 this complex function (down) locks a QMutex upon entering the function and unlocks the mutex at all the exit points
int  complexFunction( int  flag)
 {
     mutex. lock ();


      int  retVal  =   0 ;


      switch  (flag) {
      case   0 :
      case   1 :
         mutex.unlock();
          return  moreComplexFunction(flag);
      case   2 :
         {
              int  status  =  anotherFunction();
              if  (status  <   0 ) {
                 mutex.unlock();
                  return   - 2 ;
             }
             retVal  =  status  +  flag;
         }
          break ;
      default :
          if  (flag  >   10 ) {
             mutex.unlock();
              return   - 1 ;
         }
          break ;
     }
     mutex.unlock();
      return  retVal;
 } 
This example(up) increases the likelihood that errors will occur.


Using QMutexLocker greatly simplifies the code, and makes it more readable 
int  complexFunction( int  flag)
 {
     QMutexLocker locker( & mutex);


      int  retVal  =   0 ;


      switch  (flag) {
      case   0 :
      case   1 :
          return  moreComplexFunction(flag);
      case   2 :
         {
              int  status  =  anotherFunction();
              if  (status  <   0 )
                  return   - 2 ;
             retVal  =  status  +  flag;
         }
          break ;
      default :
          if  (flag  >   10 )
              return   - 1 ;
          break ;
     }


      return  retVal;
 } 
 
Now, the mutex will always be unlocked when the QMutexLocker object is destroyed (when the function returns since locker is an auto variable) . 即使在抛出异常的情况下也可以使用。


3. QReadWriteLock
 
用mutex进行线程同步有一个问题就是mutex只允许某个时刻只允许一个线程对共享资源进行访问,如果同时有多个线程对共享资源进行读访问,而只有一个写操作线程,那么在这种情况下如果采用mutex就成为程序运行性能的瓶颈了。在这种情况下Qt下采用QReadWriteLock来实现多个线程读,一个线程写。写线程执行的时候会阻塞所有的读线程,而读线程之间的运行不需要进行同步。
MyData data;
QReadWriteLock  lock ;
void  ReaderThread::run()
{
     
     lock .lockForRead();
    access_data_without_modifying_it( & data);
     lock .unlock();
     
}
void  WriterThread::run()
{
     
     lock .lockForWrite();
     modify_data( & data);
     lock .unlock();
     
}

QReadWriterLock 与QMutex相似,除了它对 "read","write"访问进行区别对待。它使得多个读者可以共时访问数据。使用QReadWriteLock而不是QMutex,可以使得多线程程序更具有并发性。
 
4.QReadLocker和QWriteLocker
 

对于QMutex有QMutexLocker来简化使用,而对于QReadWriteLock有 QReadLocker 和 QWriteLocker。
    Here's an example that uses QReadLocker to lock and unlock a read-write lock for reading: 
QReadWriteLock  lock ;


 QByteArray readData()
 {
      QReadLocker locker(&lock); 
      
      return  data;
 } 
 It is equivalent to the following code:
 QReadWriteLock  lock ;


 QByteArray readData()
 {
     lock.lockForRead(); 
      
     lock.unlock(); 
      return  data;
 } 


 5.QSemaphore

QSemaphoreQMutex 的一般化,它可以保护一定数量的相同资源,与此相对,一个mutex只保护一个资源。下面例子中,使用QSemaphore 来控制对环状缓冲区 的访问,此缓冲区被生产者线程和消费者线程共享。生产者不断向缓冲写入数据直到缓冲末端 ,消费者从缓冲不断从缓冲头部 读取数据。
  信号量比互斥量有更好的并发性,假如我们用互斥量来控制对缓冲的访问,那么生产者,消费者不能同时访问缓冲 。然而,我们知道在同一时刻,不同线程访问缓冲的不同部分并没有什么危害。
      QSemaphore semaphore(1); |     QMutex mutex;
      Qsemaphore.acquire();         |     Qmutex.lock();
      Qsemaphore.release();         |     Qmutex.unlock();
 
Public Functions
QSemaphore ( int n = 0 )
~QSemaphore ()
void  acquire ( int n = 1 )
int available () const
void release ( int n = 1 )
bool tryAcquire ( int n = 1 )
bool tryAcquire ( int n , int timeout )
 
      Semaphores support two fundamental operations, acquire () and release ():
acquire(n)    tries to acquire n resources. If there aren't that many resources available, the call will block until this is the case.
release(n )   releases n resources.
tryAcquire ()  returns immediately if it cannot acquire the resources
available ()    returns the number of available resources at any time.
 
 Example:
 QSemaphore sem( 5 );       //  sem.available() == 5 


 sem.acquire( 3 );          //  sem.available() == 2 
 sem.acquire( 2 );          //  sem.available() == 0 
 sem.release( 5 );          //  sem.available() == 5 
 sem.release( 5 );          //  sem.available() == 10 


 sem.tryAcquire( 1 );       //  sem.available() == 9, returns true 
 sem.tryAcquire( 250 );        //  sem.available() == 9, returns false 
 
 
生产者线程写数据到buffer直到缓冲末端,然后重新从buffer的头部开始写。
显然producer线程和consumer线程是需要进行同步的,If the producer generates the data too fast, it will overwrite data that the consumer hasn't yet read; if the consumer reads the data too fast, it will pass the producer and read garbage.   
        A crude way to solve this problem is to have the producer fill the buffer, then wait until the consumer has read the entire buffer, and so on
. 显然这样做效率是比较低的。
 
   const   int  DataSize  =   100000 ;
   const   int  BufferSize  =   8192 ;
   char  buffer[BufferSize];
 
   // When the application starts, the reader thread will start  
 // acquiring "free" bytes and convert them into "used" bytes



  QSemaphore freeBytes(BufferSize);    // producer线程在此区域写入数据 ,初始资源数量为BufferSize
  QSemaphore usedBytes;                  // consumer线程读取此区域的数据,初始资源数量为0
 
 
 // For this example, each byte counts as one resource.
 // In a real-world application, we would probably operate on larger
 // units (for example, 64 or 256 bytes at a time) 





   class  Producer :  public  QThread
  {
   public :
       void  run();
  };
//生产者每acquire一次就使用掉Buffer个资源中的一个,而写入的字符存入到buffer数组中


 //从而消费者可用读取字符,从而消费者获取一个资源

   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 " );
 }
// Finally, in main(), we start the producer and consumer threads. 
    // What happens then is that the producer converts some "free" space
    // into "used" space, and the consumer can then convert it back to  "free" space.



 int  main( int  argc,  char   * argv[])
{
    QCoreApplication app(argc, argv);

    Producer producer;
    Consumer consumer;

    producer.start();
    consumer.start();

    producer.wait();
    consumer.wait();

     return   0 ;
}
 
    producer的run函数:

当producer线程执行run函数,如果buffer中已经满了,而没有consumer线程没有读,这样producer就不能再往buffer中写字符。此时在 freeBytes.acquire 处就阻塞直到 consumer线程读(consume)数据。一旦producer获取到一个字节(资源)就写如一个随机的字符,并调用 usedBytes.release 从而 consumer线程获取一个资源可以读一个字节的数据了。
    consumer的run函数:

当consumer线程执行run函数,如果buffer中没有数据,就是资源=0,则consumer线程在此处阻塞。直到producer线程执行写操作,写入一个字节,并执usedBytes.release 从而使得consumer线程的可用资源数=1。则consumer线程从阻塞状态中退出,并将 usedBytes 资源数-1,当前资源数=0。


6. QWaitCondition

       对生产者和消费者问题的另一个解决办法是使用QWaitCondition,它允许线程在一定条件下唤醒其他线程。QWaitCondition必须与QMutex或者QReadwriteLock一起用。


成员函数:
QWaitCondition ()
virtual ~QWaitCondition ()
bool wait ( QMutex * mutex, unsigned long time = ULONG_MAX )
void wakeOne ()
void wakeAll ()


线程1:

        ....

mutex_print.lock();
        qDebug() << "-- print thread wait --";
        print_start.wait(&mutex_print);
        qDebug() << "-- print thread wait end --";
        mutex_print.unlock();

       ....

线程2:

   .....

    app->thread_print.mutex_print.lock();
    app->thread_print.print_list.append(ch_idx);
    app->thread_print.print_start.wakeAll();
    app->thread_print.mutex_print.unlock();

     .....


(1)bool QWaitCondition::wait ( QMutex  * mutex, unsigned long time = ULONG_MAX )
          1) 释放锁定的mutex
          2) 在线程对象上等待,即阻塞调用线程

这个函数提供的是允许从锁定状态到等待状态的原子转换。


    mutex必须由调用wait 的线程进行初锁定 。注意调用wait的话,wait 会自动调用unlock解锁之前锁住的资源,不然会造成死锁。原因如下:
    线程1等待线程2来改变共享资源,达到一定的条件线程2发出信号,使得线程1从wait中的阻塞状态中被唤醒。但是线程2想改变资源从而唤醒线程1,
却无法办到因为线程1调用lock之后就在wait中阻塞,但是没有及时的unlock,而线程2会调用lock()即阻塞,那么这就构成了死锁的条件。所以说wait函数除了使调用线程切换到内核态之外,还自动unlock(&mutex),避免另外线程中遇到lock会死锁。
    也就是说,mutex 将被解锁,并且调用线程将会阻塞,直到下列条件之一满足时才醒来:
    1)另一个线程使用wakeOne ()或wakeAll ()传输信号给它。在这种情况下,这个函数将返回真(注意,如果在调用线程中互斥量mutext没有被锁住或者是一个递归的互斥量,wait立即返回)。
    2)time 毫秒过去了。如果time 为ULONG_MAX(默认值),那么这个等待将永远不会超时(这个事件必须被传输)。如果等待的事件超时,这个函数将会返回假,互斥量将以同样的锁定状态返回。

 
 (2)void QWaitCondition::wakeAll ()
这将会唤醒所有等待QWaitCondition的线程。这些线程被唤醒的顺序依赖于操组系统的调度策略,并且不能被控制或预知。
 
(3)void  QWaitCondition::wakeOne ()
这将会唤醒所有等待QWaitCondition的线程中的一个线程。这个被唤醒的线程依赖于操组系统的调度策略,并且不能被控制或预知。

      唤醒等待线程后,wait函数会对mutex重新上锁,然后wait函数返回。总的来wait的过程:对mutex解锁(方便其他线程访问共享资源,并改变...)->阻塞当前线程->满足条件->锁定->返回。  

 假定每次用户按下一个键,我们有三个任务要同时执行,每个任务都可以放到一个线程中,每个线程的run()都应该是这样:


forever 
{
     mutex. lock ();
     keyPressed.wait( & mutex);
     do_something();
     mutex.unlock ();
 } 
 
第四个线程回去读键按下并且每当它接收到一个的时候唤醒其它三个线程,就像这样:


 QWaitCondition key_pressed;
   for  (;;) 
   {
     getchar();
     key_pressed.wakeAll();//  在key_pressed中导致引起任何一个线程。wait()将会从这个方法中返回并继续执行 
   }
  
 应用条件变量对前面用信号量进行保护的环状缓冲区的例子进行改进:
 
   下面的例子中:
     1)生产者首先必须检查缓冲是否已满(numUsedBytes==BufferSize),如果是,线程停下来等待bufferNotFull条件。如果不是,在缓冲中生产数据,增加numUsedBytes,激活条件 bufferNotEmpty。
     2)使用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 ();
           //producer线程首先检查缓冲区是否已满
            if  (numUsedBytes  ==  BufferSize)//缓冲区已满,等待consumer来减少numUsedBytes
               // bufferNotFull.wait(&mutex)先调用 mutex.unlock ()然后收到信号时调用 mutex. lock ()
               bufferNotFull.wait( & mutex);//缓冲区已满等待bufferNotFull的条件变量成立变为有信号 
           mutex.unlock ();
         
           buffer[i  %  BufferSize]  =   " ACGT " [( int )qrand()  %   4 ];
  
           mutex. lock ();
            ++ numUsedBytes; //producer用掉一个Bytes,表示producer写入buffer中的字节数 
           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 ;
   } 
另外一个例子:
      #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();
      }
  

三、线程安全类

可重入与线程安全

在Qt文档中,术语“可重入 ”与“线程安全 ”被用来说明一个函数如何用于多线程程序。假如一个类的任何函数在此类的多个不同的实例上,可以被多个线程同时调用,那么这个类被称为是“可重入”的。假如不同的线程作用在同一个实例上仍可以正常工作,那么称之为“线程安全”的。
大多数c++类天生就是可重入的,因为它们典型地仅仅引用成员数据。任何线程可以在类的一个实例上调用这样的成员函数,只要没有别的线程在同一个实例上调用这个成员函数。举例来讲,下面的Counter 类是可重入的:
class Counter
{
  public :
      Counter() {n=0;}
      void increment() {++n;}
      void decrement() {--n;}
      int value() const {return n;}
 private :
      int n;
};
这个类不是线程安全的,因为假如多个线程都试图修改数据成员 n,结果未定义。这是因为c++中的++和--操作符不是原子操作。实际上,它们会被扩展为三个机器指令:
1,把变量值装入寄存器
2,增加或减少寄存器中的值
3,把寄存器中的值写回内存
 
假如线程A与B同时装载变量的旧值,在寄存器中增值,回写。他们写操作重叠了,导致变量值仅增加了一次。很明显,访问应该串行化:A执行123步骤时不应被打断。使这个类成为线程安全的最简单方法是使用QMutex 来保护数据成员:
class Counter
 {
 public :
     Counter() { n = 0; }

     void increment() { QMutexLocker locker(&mutex); ++n; }
     void decrement() { QMutexLocker locker(&mutex); --n; }
     int value() const { QMutexLocker locker(&mutex); return n; }

 private :
     mutable QMutex mutex;
     int n;
 };
QMutexLocker类在构造函数中自动对mutex进行加锁,在析构函数中进行解锁。随便一提的是,mutex使用了mutable 关键字来修饰,因为我们在value()函数中对mutex进行加锁与解锁操作,而value ()是一个const 函数。
大多数Qt类是可重入,非线程安全的。有一些类与函数是线程安全的,它们主要是线程相关的类,如QMutex,QCoreApplication::postEvent ()。

线程与QObjects

QThread 继承自QObject,它发射信号以指示线程执行开始与结束,而且也提供了许多slots。更有趣的是,QObjects可以用于多线程,这是因为每个线程被允许有它自己的事件循环。
QObject 可重入性
QObject 是可重入的。它的大多数非GUI子类,像QTimer ,QTcpSocket ,QUdpSocket ,QHttp ,QFtp ,QProcess 也是可重入的,在多个线程中同时使用这些类是可能的。需要注意的是,这些类被设计成在一个单线程中创建与使用,因此,在一个线程中创建一个对象,而在另外的线程中调用它的函数,这样的行为不能保证工作良好。有三种约束需要注意:
1,QObject的孩子总是应该在它父亲被创建的那个线程中创建 。这意味着,你绝不应该传递QThread对象作为另一个对象的父亲(因为QThread对象本身会在另一个线程中被创建)
2,事件驱动对象仅仅在单线程中使用 。明确地说,这个规则适用于"定时器机制“与”网格模块“,举例来讲,你不应该在一个线程中开始一个定时器或是连接一个套接字,当这个线程不是这些对象所在的线程。
3,你必须保证在线程中创建的所有对象在你删除QThread前被删除 。这很容易做到:你可以run ()函数运行的栈上创建对象。

尽管QObject是可重入的,但GUI类,特别是QWidget与它的所有子类都是不可重入的。它们仅用于主线程。正如前面提到过的,QCoreApplication::exec ()也必须从那个线程中被调用。实践上,不会在别的线程中使用GUI类,它们工作在主线程上,把一些耗时的操作放入独立的工作线程中,当工作线程运行完成,把结果在主线程所拥有的屏幕上显示。

逐线程事件循环

每个线程可以有它的事件循环,初始线程开始它的事件循环需使用QCoreApplication::exec (),别的线程开始它的事件循环需要用QThread::exec ().像QCoreApplication一样,QThreadr提供了exit (int)函数,一个quit () slot。

线程中的事件循环,使得线程可以使用那些需要事件循环的非GUI 类(如,QTimer ,QTcpSocket ,QProcess )。也可以把任何线程的signals连接到特定线程的slots,也就是说信号-槽机制是可以跨线程使用的。对于在QApplication之前创建的对象,QObject::thread ()返回0,这意味着主线程仅为这些对象处理投递事件,不会为没有所属线程的对象处理另外的事件。可以用QObject::moveToThread ()来改变它和它孩子们的线程亲缘关系,假如对象有父亲,它不能移动这种关系。在另一个线程(而不是创建它的那个线程)中delete QObject对象是不安全的。除非你可以保证在同一时刻对象不在处理事件。可以用QObject::deleteLater(),它会投递一个DeferredDelete 事件,这会被对象线程的事件循环最终选取到。
假如没有事件循环运行,事件不会分发给对象。举例来说,假如你在一个线程中创建了一个QTimer对象,但从没有调用过exec (),那么QTimer就不会发射它的timeout ()信号.对deleteLater ()也不会工作。(这同样适用于主线程)。你可以手工使用线程安全的函数QCoreApplication::postEvent (),在任何时候,给任何线程中的任何对象投递一个事件,事件会在那个创建了对象的线程中通过事件循环派发。事件过滤器在所有线程中也被支持,不过它限定被监视对象与监视对象生存在同一线程中。类似地,QCoreApplication::sendEvent (不是postEvent ()),仅用于在调用此函数的线程中向目标对象投递事件。

从别的线程中访问QObject子类 

QObject 和所有它的子类是非线程安全的。这包括整个的事件投递系统。需要牢记的是,当你正从别的线程中访问对象时,事件循环可以向你的QObject子类投递事 件。假如你调用一个不生存在当前线程中的QObject子类的函数时,你必须用mutex来保护QObject子类的内部数据,否则会遭遇灾难或非预期结 果。像其它的对象一样,QThread对象生存在创建它的那个线程中---不是当QThread::run() 被调用时创建的那个线程。一般来讲,在你的QThread子类中提供slots是不安全的,除非你用mutex保护了你的成员变量。
另一方面,你可以安全的从QThread::run ()的实现中发射信号,因为信号发射是线程安全的。

跨线程的信号-槽 

Qt支持三种类型的信号-槽连接:
1,直接连接 ,当signal发射时,slot立即调用。此slot在发射signal的那个线程中被执行(不一定是接收对象生存的那个线程)
2,队列连接 ,当控制权回到对象属于的那个线程的事件循环时,slot被调用。此slot在接收对象生存的那个线程中被执行
3,自动连接(缺省), 假如信号发射与接收者在同一个线程中,其行为如直接连接,否则,其行为如队列连接。
连接类型可能通过以向connect ()传递参数来指定。注意的是,当发送者与接收者生存在不同的线程中,而事件循环正运行于接收者的线程中,使用直接连接是不安全的。同样的道理,调用生存在不同的线程中的对象的函数也是不是安全的。QObject::connect ()本身是线程安全的。

多线程与隐含共享 

Qt 为它的许多值类型使用了所谓的隐含共享(implicit sharing)来优化性能。原理比较简单,共享类包含一个指向共享数据块的指针,这个数据块中包含了真正原数据与一个引用计数。把深拷贝转化为一个浅拷 贝,从而提高了性能。这种机制在幕后发生作用,程序员不需要关心它。如果深入点看,假如对象需要对数据进行修改,而引用计数大于1,那么它应该先 detach()。以使得它修改不会对别的共享者产生影响,既然修改后的数据与原来的那份数据不同了,因此不可能再共享了,于是它先执行深拷贝,把数据取 回来,再在这份数据上进行修改。例如:
void QPen::setStyle(Qt::PenStyle style)
 {
     detach ();           // detach from common data 
     d->style = style;   // set the style member 
 }

 void QPen::detach ()
 {
     if (d->ref != 1 ) {
         ...             // perform a deep copy
     }
 }
一 般认为,隐含共享与多线程不太和谐,因为有引用计数的存在。对引用计数进行保护的方法之一是使用mutex,但它很慢,Qt早期版本没有提供一个满意的解 决方案。从4.0开始,隐含共享类可以安全地跨线程拷贝,如同别的值类型一样。它们是完全可重入的。隐含共享真的是"implicit"。它使用汇编语言 实现了原子性引用计数操作,这比用mutex快多了。
假如你在多个线程中同进访问相同对象,你也需要用mutex来串行化访问顺序,就如同其他可重入对象那样。总的来讲,隐含共享真的给”隐含“掉了,在多线程程序中,你可以把它们看成是一般的,非共享的,可重入的类型,这种做法是安全的。




0 0
原创粉丝点击