Qt多线程编程

1 篇文章 0 订阅

Qt线程类 

Qt  包含下面一些线程相关的类:

QThread  提供了开始一个新线程的方法

QThreadStorage  提供逐线程数据存储

QMutex  提供相互排斥的锁,或互斥量

QMutexLocker  是一个便利类,它可以自动对QMutex  加锁与解锁

QReadWriterLock  提供了一个可以同时读操作的锁

QReadLocker  QWriteLocker  是便利类,它自动对QReadWriteLock  加锁与解锁

QSemaphore  提供了一个整型信号量,是互斥量的泛化

QWaitCondition  提供了一种方法,使得线程可以在被另外线程唤醒之前一直休眠。

Qt线程的创建 

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

1  class  MyThread  public  QThread  {

2  public  :

3  virtual  void  run();

4  };

5 

6  void  MyThread::run()

7  {

8  for  int  count  =  0  count  <  20  count  ++  {

9  sleep(  1  );

10  qDebug(  "  Ping!  "  );

11  }

12  }

13 

14  int  main()

15  {

16  MyThread  a;

17  MyThread  b;

18 

19  a.start();  //  自动调用run(),否则即使该线程创建,也是一开始就挂起 

20  b.start();

21  //  要等待线程a,b都退出 

22  a.wait();

23  b.wait();

24  }

25 

Qt线程同步 

1.QMutex 

QMutex  (  bool  recursive  =  FALSE  )

virtual  ~QMutex  ()

void  lock  ()  //试图锁定互斥量。如果另一个线程已经锁定这个互斥量,那么这次调用将阻塞  直到那个线程把它解锁。 

void  unlock  ()

bool  locked  ()

bool  tryLock  ()  //如果另一个进程已经锁定了这个互斥量,这个函数返回假,而不是一直等到这个锁可用为止,比如,它不是阻塞的。 

1  //  Qt 

2  QMutex  mutex;

3  void  someMethod()

4  {

5  mutex.  lock  ();

6  qDebug(  "  Hello  "  );

7  qDebug(  "  World  "  );

8  mutex.unlock();

9  }

10 

11  //  Java的术语,这段代码应该是: 

12  void  someMethod()

13  {

14  synchronized  {

15  qDebug(  "  Hello  "  );

16  qDebug(  "  World  "  );

17  }

18  } 

不过在Qt中我们可用通过另一个类来简化这种应用,因为如果使用QMutex.lock()而没有对应的使用QMutex.unlcok()的话 

就会造成死锁,别的线程永远也得不到接触该mutex锁住的共享资源的机会。尽管可以不使用lock()而使用tryLock(timeout)

来避免因为死等而造成的死锁tryLock(负值)==lock()),但是还是很有可能造成错误。

对于上述的情况MFC中用CSingleLock    MultiLockBoost中用boost::mutex::scoped_lock来进行解决,而在Qt中用

QMutexLocker来进行解决。下面是没有采用  QMutexLocker的例子和采用  QMutexLocker的方案。 

2.QMutexLocker 

this  complex  function  locks  a  QMutex  upon  entering  the  function  and  unlocks  the  mutex  at  all  the  exit  points 

1  int  complexFunction(  int  flag)

2  {

3  mutex.  lock  ();

4 

5  int  retVal  =  0  ;

6 

7  switch  (flag)  {

8  case  0  :

9  case  1  :

10  mutex.unlock();

11  return  moreComplexFunction(flag);

12  case  2  :

13  {

14  int  status  =  anotherFunction();

15  if  (status  <  0  {

16  mutex.unlock();

17  return  -  2  ;

18  }

19  retVal  =  status  +  flag;

20  }

21  break  ;

22  default  :

23  if  (flag  >  10  {

24  mutex.unlock();

25  return  -  1  ;

26  }

27  break  ;

28  }

29 

30  mutex.unlock();

31  return  retVal;

32 

This  example  increases  the  likelihood  that  errors  will  occur.Using  QMutexLocker  greatly  simplifies  the  code,  and  makes  it  more  readable: 

1  int  complexFunction(  int  flag)

2  {

3  QMutexLocker  locker(  &  mutex);

4 

5  int  retVal  =  0  ;

6 

7  switch  (flag)  {

8  case  0  :

9  case  1  :

10  return  moreComplexFunction(flag);

11  case  2  :

12  {

13  int  status  =  anotherFunction();

14  if  (status  <  0  )

15  return  -  2  ;

16  retVal  =  status  +  flag;

17  }

18  break  ;

19  default  :

20  if  (flag  >  10  )

21  return  -  1  ;

22  break  ;

23  }

24 

25  return  retVal;

26 

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来实现多个线程读,一个线程写。写线程执行的时候会阻塞所有的读线程,而读线程之间的运行不需要进行同步。

1  MyData  data;

2  QReadWriteLock  lock  ;

3  void  ReaderThread::run()

4  {

5 

6  lock  .lockForRead();

7  access_data_without_modifying_it(  &  data);

8  lock  .unlock();

9 

10  }

11  void  WriterThread::run()

12  {

13 

14  lock  .lockForWrite();

15  modify_data(  &  data);

16  lock  .unlock();

17 

18  }

19 

20 

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

4.QReadLockerQWriteLocker 

对于QMutexQMutexLocker来简化使用,而对于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 

QSemaphore  QMutex  的一般化,它可以保护一定数量的相同资源,与此相对,一个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. 

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.  显然这样做效率是比较低的。 

1  const  int  DataSize  =  100000  ;

2  const  int  BufferSize  =  8192  ;

3  char  buffer[BufferSize];

4 

5  //  When  the  application  starts,  the  reader  thread  will  start 

//  acquiring  "free"  bytes  and  convert  them  into  "used"  bytes 

6  QSemaphore  freeBytes(BufferSize);  //  producer线程在此区域写入数据  ,初始资源数量为BufferSize

7  QSemaphore  usedBytes;  //  consumer线程读取此区域的数据,初始资源数量为0

8 

9 

10  //  For  this  example,  each  byte  counts  as  one  resource.

11  //  In  a  real-world  application,  we  would  probably  operate  on  larger 

//  units  (for  example,  64  or  256  bytes  at  a  time) 

12  class  Producer  public  QThread

13  {

14  public  :

15  void  run();

16  };

17  //生产者每acquire一次就,使用掉Buffer个资源中的一个,而写入的字符存入到buffer数组中 

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

18  void  Producer::run()

19  {

20  //qsrand(QTime(0,0,0).secsTo(QTime::currentTime())); 

21  for  (  int  =  0  i  <  DataSize;  ++  i)  {

22  freeBytes.acquire  ();

23  buffer[i  %  BufferSize]  =  "  ACGT  "  [(  int  )qrand()  %  4  ];

24  usedBytes.release  ();

25  }

26  }

27 

28  class  Consumer  public  QThread

29  {

30  public  :

31  void  run();

32  };

33 

34  void  Consumer::run()

35  {

36  for  (  int  =  0  i  <  DataSize;  ++  i)  {

37  usedBytes.acquire  ();

38  fprintf(stderr,  "  %c  "  buffer[i  %  BufferSize]);

39  freeBytes.release  ();

40  }

41  fprintf(stderr,  "  /n  "  );

42  }

43  //  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. 

 

46  int  main(  int  argc,  char  *  argv[])

47  {

48  QCoreApplication  app(argc,  argv); 

49  Producer  producer;

50  Consumer  consumer; 

51  producer.start();

52  consumer.start(); 

53  producer.wait();

54  consumer.wait(); 

55  return  0  ;

56  } 

producerrun函数: 

producer线程执行run函数,如果buffer中已经满了,而没有consumer线程没有读,这样producer就不能再往buffer

中写字符。此时在  freeBytes.acquire  处就阻塞直到  consumer线程读(consume)数据。一旦producer获取到一个字节(资源)

就写如一个随机的字符,并调用  usedBytes.release  从而  consumer线程获取一个资源可以读一个字节的数据了。

consumerrun函数: 

consumer线程执行run函数,如果buffer中没有数据,就是资源=0,则consumer线程在此处阻塞。直到producer线程执行

写操作,写入一个字节,并执行usedBytes.release  从而使得consumer线程的可用资源数=1。则consumer线程从阻塞状态中退出, 

并将  usedBytes  资源数-1,当前资源数=0 

6.QWaitCondition 

·         QWaitCondition  ()

·         virtual  ~QWaitCondition  ()

·         bool  wait  (  QMutex  *  mutex,  unsigned  long  time  =  ULONG_MAX  )

·         void  wakeOne  ()

·         void  wakeAll  () 

Public  function: 

bool  QWaitCondition::wait  (  QMutex  *  mutex,  unsigned  long  time  =  ULONG_MAX  ) 

1)  释放锁定的mutex

2)  在线程对象上等待

mutex必须由调用线程进行初锁定  。注意调用wait的话,会自动调用unlock解锁之前锁住的资源,不然会造成死锁。

线程1等待线程2来改变共享资源,从而达到一定的条件然后发出信号,使得线程1wait中的阻塞状态中被唤醒。

但是线程2想改变资源,却无法办到,因为线程1调用lock之后就在waitblocking,了但是没有及时的unlock,那么这就

构成了死锁的条件。所以说wait函数除了使调用线程切换到内核态之外,还自动unlock(&mutex)

mutex  将被解锁,并且调用线程将会阻塞,直到下列条件之一满足时才醒来: 

o    另一个线程使用wakeOne  ()wakeAll  ()传输信号给它。在这种情况下,这个函数将返回真。 

o    time  毫秒过去了。如果time  ULONG_MAX(默认值),那么这个等待将永远不会超时(这个事件必须被传输)。如果等待的事件超时,这个函数将会返回假互斥量将以同样的锁定状态返回。这个函数提供的是允许从锁定状态到等待状态的原子转换。 

void  QWaitCondition::wakeAll  () 

这将会唤醒所有等待QWaitCondition的线程。这些线程被唤醒的顺序依赖于操组系统的调度策略,并且不能被控制或预知。 

void  QWaitCondition::wakeOne  () 

这将会唤醒所有等待QWaitCondition的线程中的一个线程。这个被唤醒的线程依赖于操组系统的调度策略,并且不能被控制或预知。 

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

QWaitCondition  key_pressed;

 

for  (;;)  {

key_pressed.wait();  //  这是一个QWaitCondition全局变量

//  键被按下,做一些有趣的事 

do_something();

}

或是这样:

forever  {

mutex.  lock  ();

keyPressed.wait(  &  mutex);

do_something();

mutex.unlock  ();

第四个线程回去读键按下并且每当它接收到一个的时候唤醒其它三个线程,就像这样:

QWaitCondition  key_pressed;

 

for  (;;)  {

getchar();

//  key_pressed中导致引起任何一个线程。wait()将会从这个方法中返回并继续执行 

key_pressed.wakeAll();

}

注意这三个线程被唤醒的顺序是未定义的,并且当键被按下时,这些线程中的一个或多个还在do_something(),它们将不会被唤醒(因为它们现在没有等待条件变量)  并且这个任务也就不会针对这次按键执行操作。这种情况是可以避免得,比如,就像下面这样做:

1  QMutex  mymutex;

2  QWaitCondition  key_pressed;

3  int  mycount  =  0  ;

4 

5  //  Worker线程代码 

6  for  (;;)  {

7  key_pressed.wait  ()  ;  //  这是一个QWaitCondition全局变量 

//keyPressed.wait(&mutex)  ; 

8  mymutex.  lock  ();

9  mycount  ++  ;

10  mymutex.unlock  (); 

11  do_something  (); 

12  mymutex.  lock  ();

13  mycount  --  ;

14  mymutex.unlock  ();

15  }

16 

17  //  读取按键线程代码 

18  for  (;;)  {

19  getchar();

20  mymutex.  lock  ();

21  //  睡眠,直到没有忙碌的工作线程才醒来。  count==0说明没有Worker线程在do  something

22  while  count  >  0  {

23  mymutex.unlock  ();

24  sleep(  1  );

25  mymutex.  lock  ();

26  }

27  mymutex.unlock  ();

28  key_pressed.wake  All  ()  ;

29  }

30 

应用条件变量对前面用信号量进行保护的环状缓冲区的例子进行改进:

下面的例子中:

1)生产者首先必须检查缓冲是否已满(numUsedBytes==BufferSize),如果是,线程停下来等待bufferNotFull条件。如果不是,在缓冲中生产数据,增加numUsedBytes,激活条件  bufferNotEmpty

2)使用mutex来保护对numUsedBytes的访问。

另外,QWaitCondition::wait  ()接收一个mutex作为参数,这个mutex应该被调用线程初始化为锁定状态。在线程进入休眠状态之前,mutex会被解锁。而当线程被唤醒时,mutex会再次处于锁定状态。

而且,从锁定状态到等待状态的转换是原子操作,这阻止了竞争条件的产生。当程序开始运行时,只有生产者可以工作。消费者被阻塞等待  bufferNotEmpty条件,一旦生产者在缓冲中放入一个字节,bufferNotEmpty条件被激发,消费者线程于是被唤醒。

1  const  int  DataSize  =  100000  ;

2  const  int  BufferSize  =  8192  ;

3  char  buffer[BufferSize];

4 

5  QWaitCondition  bufferNotEmpty;

6  QWaitCondition  bufferNotFull;

7  QMutex  mutex; 

8  int  numUsedBytes  =  0  ;

9 

10  class  Producer  public  QThread

11  {

12  public  :

13  void  run();

14  };

15 

16  void  Producer::run()

17  {

18  qsrand(QTime(  0  ,  0  ,  0  ).secsTo(QTime::currentTime()));

19 

20  for  (  int  =  0  i  <  DataSize;  ++  i)  {

21  mutex.  lock  (); 

//producer线程首先检查缓冲区是否已满 

22  if  (numUsedBytes  ==  BufferSize)//缓冲区已满,等待consumer来减少numUsedBytes 

//  bufferNotFull.wait(&mutex)先调用  mutex.unlock  ()然后收到信号时调用  mutex.  lock  () 

23  bufferNotFull.wait(  &  mutex);//缓冲区已满等待bufferNotFull的条件变量成立变为有信号 

24  mutex.unlock  ();

25 

26  buffer[i  %  BufferSize]  =  "  ACGT  "  [(  int  )qrand()  %  4  ];

27 

28  mutex.  lock  ();

29  ++  numUsedBytes;  //producer用掉一个Bytes,表示producer写入buffer中的字节数 

30  bufferNotEmpty.wakeAll();

31  mutex.unlock  ();

32  }

33  }

34 

35  class  Consumer  public  QThread

36  {

37  public  :

38  void  run();

39  };

40 

41  void  Consumer::run()

42  {

43  for  (  int  =  0  i  <  DataSize;  ++  i)  {

44  mutex.  lock  ();

45  if  (numUsedBytes  ==  0  )

46  bufferNotEmpty.wait(  &  mutex);

47  mutex.unlock  ();

48 

49  fprintf(stderr,  "  %c  "  buffer[i  %  BufferSize]);

50 

51  mutex.  lock  ();

52  --  numUsedBytes;

53  bufferNotFull.wakeAll();

54  mutex.unlock  ();

55  }

56  fprintf(stderr,  "  /n  "  );

57  }

58 

59  int  main(  int  argc,  char  *  argv[])

60  {

61  QCoreApplication  app(argc,  argv);

62  Producer  producer;

63  Consumer  consumer;

64  producer.start();

65  consumer.start();

66  producer.wait();

67  consumer.wait();

68  return  0  ;

69 

另外一个例子:

1  #include  <qapplication.h> 

2  #include  <qpushbutton.h> 

3 

4  //  全局条件变量 

5  QWaitCondition  mycond;

6 

7  //  Worker类实现 

8  class  Worker  public  QPushButton,  public  QThread

9  {

10  Q_OBJECT

11 

12  public  :

13  Worker(QWidget  *  parent  =  0  const  char  *  name  =  0  )

14  QPushButton(parent,  name)

15  {

16  setText(  "  Start  Working  "  );

17 

18  //  连接从QPushButton继承来的信号和我们的slotClicked()方法 

19  connect(  this  SIGNAL(clicked()),  SLOT(slotClicked()));

20 

21  //  调用从QThread继承来的start()方法……这将立即开始线程的执行 

22  QThread::start();

23  }

24 

25  public  slots:

26  void  slotClicked()

27  {

28  //  唤醒等待这个条件变量的一个线程 

29  mycond.wakeOne  ();

30  }

31 

32  protected  :

33  void  run()

34  {

35  //  这个方法将被新创建的线程调用…… 

36 

37  while  TRUE  )  {

38  //  锁定应用程序互斥锁,并且设置窗口标题来表明我们正在等待开始工作 

39  qApp  ->  lock  ();

40  setCaption(  "  Waiting  "  );

41  qApp  ->  unlock  ();

42 

43  //  等待直到我们被告知可以继续 

44  mycond.wait  ();

45 

46  //  如果我们到了这里,我们已经被另一个线程唤醒……让我们来设置标题来表明我们正在工作 

47  qApp  ->  lock  ();

48  setCaption(  "  Working!  "  );

49  qApp  ->  unlock  ();

50 

51  //  这可能会占用一些时间,几秒、几分钟或者几小时等等,因为这个一个和GUI线程分开的线程,在处理事件时,GUI线程不会停下来…… 

52  do_complicated_thing();

53  }

54  }

55  };

56 

57  //  主线程——所有的GUI事件都由这个线程处理。 

58  int  main(  int  argc,  char  **  argv  )

59  {

60  QApplication  app(  argc,  argv  );

61 

62  //  创建一个worker……当我们这样做的时候,这个worker将在一个线程中运行 

63  Worker  firstworker(  0  "  worker  "  );

64 

65  app.setMainWidget(  &  worker  );

66  worker.show();

67 

68  return  app.exec();

69  }

70 

7.线程安全类 

非线程安全类 

这个类不是线程安全的,因为假如多个线程都试图修改数据成员  n,结果未定义。这是因为c++中的++--操作符不是原子操作。实际上,它们会被扩展为三个机器指令:

1,把变量值装入寄存器

2,增加或减少寄存器中的值

3,把寄存器中的值写回内存 

假如线程AB同时装载变量的旧值,在寄存器中增值,回写。他们写操作重叠了,导致变量值仅增加了一次。很明显,访问应该串行化:A执行123步骤时不应被打断。

class  Counter

{

public  :

Counter()  {n  =  0  ;}

void  increment()  {  ++  n;}

void  decrement()  {  --  n;}

int  value()  const  {  return  n;}

private  :

int  n;

};

线程安全类 

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文档中,术语可重入  线程安全  被用来说明一个函数如何用于多线程程序。假如一个类的任何函数在此类的多个不同的实例上,可以被多个线程同时调用,那么这个类被称为是可重入的。假如不同的线程作用在同一个实例上仍可以正常工作,那么称之为线程安全的。

大多数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,把寄存器中的值写回内存 

假如线程AB同时装载变量的旧值,在寄存器中增值,回写。他们写操作重叠了,导致变量值仅增加了一次。很明显,访问应该串行化: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  也是可重入的,在多个线程中同时使用这些类是可能的。需要注意的是,这些类被设计成在一个单线程中创建与使用,因此,在一个线程中创建一个对象,而在另外的线程中调用它的函数,这样的行为不能保证工作良好。有三种约束需要注意:

1QObject的孩子总是应该在它父亲被创建的那个线程中创建  。这意味着,你绝不应该传递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来串行化访问顺序,就如同其他可重入对象那样。总的来讲,隐含共享真的给隐含掉了,在多线程程序中,你可以把它们看成是一般的,非共享的,可重入的类型,这种做法是安全的。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值