线程的相关概念非常庞杂和抽象,好多地方搞不懂,但是还是一字不落的写下来,以便今后常常查看,总会搞清楚的。

1. 使用QThread启动线程

1.1 相关概念

  • Qt中的QThread类提供了与平台无关的线程。
  • 一个QThread代表了一个在应用程序中可以独立控制的线程,它与进程中的其他线程分享数据,但是是独立执行的。
  • 相对于一般的程序都从**main()**函数开始执行,QThread run() 函数开始执行。
  • 默认的,run() 通过调用 exec() 来开启时间循环,并在线程内运行一个Qt事件循环。
  • 要创建一个线程,需要子类化QThread,并且重新实现run() 函数,例如下面的代码:
class MyThread:public QThread
{
protected:
	void run();
};
void MyThread::run()
{
	QTcpSocket socket;
	...
	socket.connectToHost(hostName, portNumber);
	exec();
}
  • 这样会在一个线程中创建一个 QTcpSocket ,然后执行这个线程的事件循环。
  • 可以在外部创建该线程的实例,然后调用 start() 函数来开始执行该线程,start() 默认调用 run() 函数。当 run() 函数返回后,线程便执行结束,就像应用程序离开 main() 函数一样。QThread会在开始、结束和终止时发射 started()、 finished() 和 terminated() 等信号。也可以使用 isFinished() 和 isRunning() 来查询线程的状态。可以使用 wait() 来阻塞,知道线程结束执行。
  • 每个线程都会从操作系统获得自己的堆栈,操作系统会决定堆栈的默认大小,也可以使用setStackSize() 来设置一个自定义堆栈大小。
  • 每一个线程可以有自己的事件循环,可以通过调用exec()函数来启动事件循环。可以通过调用exit() 或 **quit()**来停止事件循环。
  • 线程中拥有一个事件循环,是它可以关联其他线程中的信号到本线程的槽上,这使用了队列关联机制,就是在使用 connect() 函数进行信号和槽的关联时,将Qt::ConnectionType类型的参数指定为Qt::QueuedConnection。
  • 拥有事件循环还可以使该线程能够使用需要事件循环的类,比如 Qtimer 和 QTcpSocket 类等。注意,在线程中使无法使用任何界面部件类的。
  • 在极端情况下,可能想要强制终止一个正在执行的线程,这是可以使用 terminate() 函数。但是,线程是否会被立即终止,依赖于操作系统的调度策略。可以在调用完 terminate() 后调用 QThread::wait() 来同步终止。使用 terminate() 函数时,线程可能在任何时刻被终止而无法进行一些清理工作。因此该函数是危险的,一般不建议使用。
  • 静态函数 currentThreadId() 和 currentThread() 可以返回当前执行的线程的标识符,前者返回一个线程的系统特定的ID;后者返回一个 QThread 指针。QThread 也提供了多个平台无关的睡眠函数,其中,sleep() 精度为秒,msleep() 精度为毫秒,usleep() 精度为微妙。

1.2 在图形界面程序中启动线程的例程代码

在这里插入图片描述

  • mythread.h
#ifndef MYTHREAD_H
#define MYTHREAD_H
#include <QThread>

class MyThread : public QThread
{
    Q_OBJECT
public:
    explicit MyThread(QObject * parent = 0);
    void stop();
protected:
    void run();
private:
    //volatile关键字可以使它在任何时候都保持最新的值,从而可以避免在多个线程中访问它时出错。
    volatile bool stopped;
};

#endif // MYTHREAD_H
  • mythread.cpp
#include "mythread.h"
#include <QDebug>

MyThread::MyThread(QObject * parent):QThread(parent)
{
    stopped = false;
}

void MyThread::run()
{
    qreal i = 0;
    while (!stopped)
    {
        qDebug()<<QString("in MyThread: %1").arg(i);
        msleep(1000);
        i++;
    }
    stopped = false;
}

void MyThread::stop()
{
    stopped = true;
}
  • dialog.h
#ifndef DIALOG_H
#define DIALOG_H

#include <QDialog>
#include "mythread.h"

namespace Ui {
class Dialog;
}

class Dialog : public QDialog
{
    Q_OBJECT

public:
    explicit Dialog(QWidget *parent = 0);
    ~Dialog();

private slots:
    void on_pushButtonStart_clicked();
    void on_pushButtonStop_clicked();

private:
    Ui::Dialog *ui;

    MyThread thread;
};

#endif // DIALOG_H

  • dialog.cpp
#include "dialog.h"
#include "ui_dialog.h"

Dialog::Dialog(QWidget *parent) :
    QDialog(parent),
    ui(new Ui::Dialog)
{
    ui->setupUi(this);
}

Dialog::~Dialog()
{
    delete ui;
}

void Dialog::on_pushButtonStart_clicked()
{
    thread.start();
    ui->pushButtonStart->setEnabled(false);
    ui->pushButtonStop->setEnabled(true);
}

void Dialog::on_pushButtonStop_clicked()
{
    if(thread.isRunning())
    {
        thread.stop();
        ui->pushButtonStart->setEnabled(true);
        ui->pushButtonStop->setEnabled(false);
    }
}

2. 同步线程

2.1 相关概念

  • Qt 中的 QMutex、QReadWriteLock、QSemaphore 和 QWaitCondition 类提供了同步线程的方法。虽然使用现成的思想是多个线程可以尽可能地并发执行,但是总有一些时刻,一些线程必须停止来等待其他线程。例如,如果两个线程尝试同时访问相同的全局变量,则结果通常是不确定的。
  • QMutex 提供了一个互斥锁(mutex),在任何时间至多有一个 线程可以获得 mutex 。如果一个线程尝试获得 mutex ,而此时 mutex 已经被锁住了,则这个线程将会睡眠,知道现在获得 mutex 的线程对 mutex 进行解锁为止。互斥锁经常用于对共享数据(例如,可以同时被多个线程访问的数据)的访问进行保护。
  • QReadWriteLock 即读-写锁,与 mutex 很相似,只不过它将对共享数据的访问区分为“读”访问和“写”访问,允许多个线程同时对数据进行“读”访问。在可能的情况下使用 QReadWriteLock 代替 QMutex ,可以提高多线程的并发度。
  • QSemaphore 即信号量,是 QMutex 的一般化,它用来保护一定数量的相同的资源,而互斥锁 mutex 只能保护一个资源。
  • QWaitCondition 即条件变量,允许一个线程在一些条件满足时唤醒其他的线程。一个或者多个线程可以被阻塞来等待一个QWaitCondition ,从而设置一个用于 wakeOne() 或者 wakeAll() 的条件。使用 wakeOne() 可以唤醒一个随机选取的等待的线程,而使用 wakeAll() 可以唤醒所有正在等待的线程。

2.2 使用信号量来解决生产者-消费者问题

这个例子演示了怎样使用 QSemaphore 信号量来保护对生产者线程和消费者线程共享的环形缓冲区的访问。生产者向缓冲区写入数据,直到它到达缓冲区的终点,这是它会从起点重新开始,覆盖已经存在的数据。消费者线程读取它产生的数据,并将其输出。
在这里插入图片描述

  • main.cpp
#include <QtCore>
#include <stdio.h>
#include <stdlib.h>
#include <QDebug>

const int DataSize = 10;    //生产者将要产生数据的数量
const int BufferSize = 5;   //环形缓冲区的大小
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];
        qDebug()<<QString("producer: %1").arg(buffer[i%BufferSize]);
        usedBytes.release();
    }
}

//消费者
class Consumer:public QThread
{
public:
    void run();
};

void Consumer::run()
{
    for(int i =0;i<DataSize;++i)
    {
        usedBytes.acquire();
        qDebug()<<QString("consumer: 1%").arg(buffer[i%BufferSize]);
        freeBytes.release();
    }
}

//主函数
int main(int argc,char * argv[])
{
    QCoreApplication app(argc,argv);
    Producer producer;
    Consumer consumer;
    producer.start();
    consumer.start();
    producer.wait();
    consumer.wait();


    return app.exec();
}

3. 可重入与线程安全

  • 一个线程安全的函数可以同时被多个线程调用,即便是它们使用了共享数据,因为该共享数据的所有实例都被序列化了。
  • 一个可重入的函数也可以同时被多个线程调用,但是只能是在每个调用使用自己的数据的情况下。
  • 因此,一个线程安全的函数总是可重入的,但是一个可重入的函数不总是线程安全的。推而广之,如果每个线程使用一个类的不同的实例,则该类的成员函数可以被多个线程安全地调用,那么这个类被称为可重入的;如果计时所有的线程使用一个类的相同的实例,该类的成员函数也可以被多个线程安全地调用,那么这个类被称为线程安全的。
  • 注意:只用在文档中标记为**线程安全(thread-safe)**的Qt类,才可以用于多线程。所以如果一个了没有标记为线程安全或者可重入,则该类的一个特定的实例不应该被多个线程访问。
  • C++类一般是可重入的,因为他们只访问自己的数据成员。任何线程都可以调用可重入类实例的成员函数,只要没有其他线程在同一时间调用该类的相同实例的成员函数即可。

3.1可重入

class Counter
{
public:
	Counter(){ n = 0;}
	void increment(){ ++n;}
	void decrement(){ --n; }
	int value() const{ return n; }
private:
	int n;
}
  • 上面这个类是可重入的,但并不是线程安全的,因为如果多个线程尝试修改数据成员n,结果便是不可预测的。这是因为**++和–操作并不总原子**的(原子操作就是一个操作不会被其他线程中断)。事实上,它们会被分为3个机器指令:第一条,向寄存器中加载变量的值;第二条,递增或者递减寄存器的值;第三条,将寄存器的值存储到内存中。如果线程A和线程B同时加载了变量的旧值,然后递增它们的寄存器并存储回去,则它们相互覆盖,结果变量只递增了一次。

3.2 线程安全

  • 对于线程A和线程B来说,访问应该按照顺序进行:在线程B执行相同的操作前,线程A必须执行完3条机器指令而不能中断,反之亦然。一个简单的方法来时类成为线程安全的,就是使用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:
	int n;
	mutable QMutex mutex;
}
  • 这里的QMutexLocker 类在其构造函数中自动锁住mutex,然后再析构函数进行调用时对齐进行解锁,在函数结束时会调用析构函数。锁住mutex确保了不同线程的访问可以序列化进行。

  • 数据成员mutex使用了mutable限定符,是因为需要在value()函数中对mutex进行加锁和解锁,而它是一个const函数。

4. 线程和QObjects

4.1 QObjects的可重入性

  • QObject是可重入的。它的大多数非GUI子类,例如QTimer、QTcpSocket、QUdpSocket 和 QProcess都是可重入的,可以在多个线程中同时使用这些类。
  • 注意,这些类是被设计在单一的线程中进行创建和使用的,在一个线程中创建一个对象,然后再另外一个线程中调用这个对象的一个函数是无法保证一定可以工作的。需要注意3个约束条件:
    • QObject的子对象必须在创建它的父对象的线程中创建。这意味着,永远不要将**QThread对象(this)**作为在该线程中创建的对象的父对象(因为QThread对象本身是在其他线程中创建的)。
    • 事件驱动对象只能在单一的线程中使用。具体来说,这条规则应用在定时机制和网络模块中。例如,不可以在对象的线程QObject::Thread以外的其他线程中启动一个定时器或者连接一个套接字。
    • 必须确保删除QThread对象以前删除在该线程中创建的所有对象,可以通过在**run()**函数中的栈上创建对象来保证这一点。
  • 虽然QObject是可重入的,但是对于GUI类,尤其是QWidget及其所有子类,是不可重入的,它们只能在主线程中使用。**QCoreApplication::exec()**也必须在主线程中调用。
  • 在实际应用中,无法在主线程以外的线程中使用GUI类的问题,可以简单地通过这样的方式来解决:将一些给常耗时的操作放在一个单独的工作线程中来运行,等该工作线程完成后将结果返回给主线程,最后由主线程将结果显示到屏幕上。Qt中的 Mandelbrot Example 和 Blocking Fortune Client Example 示例程序都使用了这种方法。

4.2 每个线程的事件循环

  • 每一个线程都可以有它自己的事件循环。初始化线程使用QCoreApplication::exec()来开启它的时间循环(对于独立的对话框界面程序,也可以使用 QDialog::exec());其他的线程可以使用 QThread::exec() 来开启一个事件循环。与QCoreApplication相似,QThread提供了一个**exit()函数和一个quit()**槽。在一个线程中使用事件循环,使得该线程可以使用那些需要事件循环的非GUI类(例如,QTimer、QTcpSocket 和 QProcess)。也使得该线程可以关联任意一个线程的信号到一个指定的槽。

4.3 从其他线程中访问QObject子类

  • QObject和它所有的四类都不是线程安全的,包括了这个事件传递系统。需要时刻记着,正在从其他线程中访问一个对象时,事件循环可能正在向这个对象传递事件。
  • 如果调用一个没有在当前线程中的QObject子类的函数,而这个对象有可能会获取事件,那么就必须使用mutex来保护对这个QObject子类的内部数据的所有访问;否则可能出现崩溃或者其他意外行为。
  • 与其他对象相似,QThread对象居住于(live in)创建该对象的线程。在QThread子类中提供槽一般是不安全的,除非使用mutex来保护成员变量。但是,可以安全地在**QThread::run()**函数中发射信号,因为信号发射是线程安全的。在这里插入图片描述
  • 如果在一个线程中创建了一个QObject对象,那么这个对象被称为居住在该线程(live in the thread)。发往这个对象的事件由该线程的事件循环进行分派。可以使用**QObject::thread()获得该对象所在的线程。可以使用QObject::moveToThread()**来改变对象及其孩子所在的线程(如果该对象有父对象,那么它无法被移动)。
  • 在其他线程(不是拥有该QObject对象的线程)中调用delete来删除(或通过其他方式访问)该QObject对象时不安全的,除非可以确保该对象当前没有在处理事件。可以使用QObject::deleteLater()来替代,这样会发送一个DeferredDelete事件,最终该对象所在的事件循环将会获取该事件。默认的,拥有该QObject对象的线程就是创建QObject对象的线程,只要没有调用过**QObject::moveToThread()**函数。
  • 如果没有运行事件循环,则事件将不会传送到对象。例如,如果在一个线程中创建了一个QTimer对象,但是从来没有调用**exec()函数,那么QTimer 将永远不会发射它的 timeoout() 信号。调用deleteLater()**也不会工作。
  • 可以手动使用线程安全函数**QCoreApplication::postEvent()**在任何时间向任何人和线程的任何对象发送事件。该事件将会被创建该对象的线程的事件循环进行分派。
  • 所有的线程都支持事件过滤器,但是被监视的对象必须与监控对象在同一个线程中。相似的,**QCoreApplication::sendEvent(与postEvent()不同)**只能向调用该函数的线程中的对象分派事件。

4.4 跨线程的信号和槽

  • Qt支持集中信号和槽关联类型
    • Auto Connection(默认)。如果信号发射和信号接收的对象在同一个线程,那么执行方式与Direct Connection相同。否则,执行与Queued Connection相同。
    • Direct Connection。信号发射后,槽立即被调用。槽在信号发送者的线程中执行,而接受者并非必须在同一个线程。
    • Queued Connection。控制权返回到接受者线程的事件循环后才调用槽。槽在接受者的线程中被执行。
    • Blocking Connection。槽的调用与Queued Connection,不同的是,当前线程会阻塞直到槽返回。注意:使用这种方式关联在相同线程中的对象时,会引起死锁。
    • Unique Connection。执行方式与Auto Connection相同。只不过关联了相同的槽上,那么这个关联就不是唯一的,这是connect返回false。
  • 可以通过向**connect()**函数传递附加的参数来指定关联类型。
  • 注意,如果在接受者线程中有一个事件循环,那么当发送者与接收者在不同的线程中时,使用Direct Connection是不安全的;类似的调用其他线程中的对象的任何函数也是不安全的。不过,**QObject::connect()**函数本身是线程安全的。


转载自:https://liuhui.blog.csdn.net/article/details/124300346