Qt 线程

Qt 线程

概述

参考: Qt线程QThread简析

QThread实例代表一个线程,我们可以重新实现QThread::run(),要新建一个线程,我们应该先继承QThread并重新实现run()函数。

定义一个Thread:

class MyThread : public  QThread
{
    Q_OBJECT
protected:
    void run();
};

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

    qDebug( "finish!");
}

我们可以在另外的函数这样调用:

MyThread thread;
thread.start();
// 如果没有消息循环,必须要加的语句,等待thread结束。
thread.wait();

thread.wait()会导致当前线程卡主,使用 QEventLoop 就可以轻松解决此类问题:

MyThread thread;
thread.start();
QEventLoop eventLoop;
connect(&thread,SIGNAL(finished ()),&eventLoop,SLOT(quit()));
// thread.wait(1);
eventLoop.exec();

如果在主线程创建线程,可以直线使用主线程的消息循环:

QCoreApplication a(argc, argv);

MyThread thread;
thread.start();

return a.exec();

新建线程的方法

参考:Qt新建线程的方法

继承QThread

继承 QThread,这应该是最常用的方法了。我们可以通过重写虚函数 void QThread::run ()实现我们自己想做的操作,实现新建线程的目的。前面已经介绍了 QThread,这里就不重复了。

这种方法,我们每一次要新建一个线程都需要继承QThread,实现一个新的类,有点不太方便。但是相对于QRunnable,这种方法的好处就是我们可以直接调用对象的start()函数启动线程,而Qrunnable必须借助 QThreadPool

继承QRunnable

QRunnable 是所有可执行对象的基类。我们可以继承 QRunnable,并重写虚函数 void QRunnable::run() 。我们可以用QThreadPool 让我们的一个 QRunnable对象在另外的线程中运行,如果 autoDelete() 返回 true(默认),那么 QThreadPool 将会在 run()运行结束后自动删除 Qrunnable 对象。可以调用 void QRunnable::setAutoDelete ( bool autoDelete ) 更改auto-deletion 标记。需要注意的是,必须在调用 QThreadPool::start() 之前设置,在调用 QThreadPool::start() 之后设置的结果无效。

下面是我写的简单的例子:

.h:

class Runnable:public QRunnable
{

    //Q_OBJECT   注意了,QRunnable不是QObject的子类。
    public:
           Runnable();
           ~Runnable();
           void run();
    protected:
    private:
};

.cpp:

// ...
void Runnable::run()
{
   cout<<"Runnable::run()thread :"<"dosomething ...."<

main:

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

       cout<<"mainthread :"<start(&runObj);

       return a.exec();
}

由结果可看出,run()确实是在不同于主线程的另外线程中运行的,而且在运行结束后就调用了析构函数,因为默认是可以自动被销毁的。
我们可以对同一个对象多次调用QThreadPool::start(),如果是可以自动被销毁的,Qrunnable对象会在最后一个线程离开了run函数之后才被销毁的。

Runnable runObj;
QThreadPool::globalInstance()->start(&runObj);
QThreadPool::globalInstance()->start(&runObj);
QThreadPool::globalInstance()->start(&runObj);   

三次调用QThreadPool::globalInstance()->start(&runObj);,但是在三次都执行完之后才运行析构函数。

这种新建线程的方法的最大的缺点就是:不能使用Qt的信号—槽机制,因为Qrunnable不是继承自QObject。所以我们要想知道线程是否运行结束或获取运行结果可能会比较麻烦。还有就是我们不能直接调用run()启动线程,必须借助于QthreadPool。

但是这种方法的好处就是,可以让 QThreadPool 来管理线程,QThreadPool 会自动的清理我们新建的Qrunnable对象。

使用 moveToThread

首先我们必须实现继承QObject的一个类,实现我们想要的功能。

class Worker:public QObject
{
       Q_OBJECT
public:
    Worker();
    ~Worker();
protected slots:
    void fun1();
    void fun2();
};

// ... 构造函数、析构函数实现
void Worker::fun1()
{
    cout<<"Worker::fun1()  thread : "<

接着创建一个对象,并调用:moveToThread ( QThread * targetThread ),让对象在新的线程中运行。

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    cout<<"mainthread :"<//注意记得启动线程

    work.moveToThread(&thread);

    //由于不能直接调用worker的函数,所以一般用信号触发调用
    QTimer::singleShot(0,&work,SLOT(fun1()));  
    QTimer::singleShot(0,&work,SLOT(fun1()));  
    returna.exec();
}

这样就能让fun1()和fun2()都运行在thread线程中了。

需要注意的是:在work 的函数结束运行前,thread不能被析构。Thread的生命期不能小于work。否则的话程序就崩掉了。

像下面的代码肯定是不行的(无消息循环):

void startWork()
{
    QThread thread;
    Worker* work = new Worker;
    thread.start();
    work->moveToThread(&thread);
    QTimer::singleShot(0,work,SLOT(fun1()));
    QTimer::singleShot(0,work,SLOT(fun2()));
}

使用这种方法需要同时管理thread和work,因为都是 new 出来,我们需要负责清理。为了避免这样的麻烦,我想到的方法是,在work类中添加一个QThread成员。

class Worker:public QObject
{
       Q_OBJECT
public:
    Worker();
    ~Worker();

protected slots:
    void fun1();
    void fun2();

private:
    QThread m_thread;
};

Worker::Worker():QObject()
{
   m_thread.start();
   this->moveToThread(&m_thread);
}

这样我们在用的时候只需要 new Work 就行了。

使用QtConcurrent::run

其实前面也有用到QtConcurrent::run启动新线程了。QtConcurrent命名空间提供了很多方法可以实现并发编程,这个以后再深入探讨了,这里只是大概讲一下启动线程。还是用上面的worker代码作为例子:

void Worker::start()
{
    QtConcurrent::run(this,&Worker::fun1);
    QtConcurrent::run(this,&Worker::fun2);
}

QtConcurrent::run是个模板函数,有很多种形式,我们也可以让全局的函数允许在另外的线程中。

void printMes(char*mes)
{
    cout<<"pprintMes(char*mes)  thread : "<cout<int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    cout<<"mainthread :"<char *mes= "hello world";
    QtConcurrent::run(printMes,mes);
    returna.exec();
}

比较

\ QThread QRunnable QtConcurrent1
High level API
Job-oriented
Builtin support for pause/resume/cancel
Can run at a different priority
Can run an event loop

Threads and QObjects

QThread 继承 QObject。它可以发送 started 和 finished 信号,也提供了一些 slot 函数。

QObject 可以用于多线程,可以发送信号调用存在于其他线程的 slot 函数,也可以 postEvent 给其他线程中的对象。之所以可以这样做,是因为每个线程都有自己的事件循环。

在进行下面的讲解之前,应该了解的重要的一点是: QThread 对象所在的线程,和 QThread 创建的线程,也就是 run() 函数执行的线程不是同一个线程。 QThread 对象所在的线程,就是创建对象的线程。 我们通过一个例子说明更能清楚一点:

MyThread::MyThread(QObject *parent /* = NULL */ ):QThread(parent)
{
    qDebug()<< "MyThreadobject currentThreadId :" <<QThread::currentThreadId();
}

void MyThread::run()
{
    qDebug()<< "run() currentThreadId : " <<QThread::currentThreadId();
}

int main( int argc, char *argv[])
{
    QApplication a(argc, argv);

    MyThread thread;
    qDebug()<< "mainThread : " <

输出结果: MyThread所在的线程就是主线程,run()函数是新开的线程。

Per-Thread Event Loop

每个线程都有自己的事件循环。起始的线程用 QCoreApplication::exec () 开启事件循环。其他的线程用 QThread::exec () 开始事件循环。与 QCoreApplication 一样, QThread也提供了 exit (int) 函数 和 quit() 槽函数。

线程里的事件循环,使得可以在线程里使用需要事件循环的非 GUI 类,例如 ( QTimer , QTcpSocket , and QProcess )。也可以把任意的线程的信号连接到特定线程的槽。

QObject 实例存在于创建实例的线程中,发送给实例事件也是有线程的事件循环实现的。可以用 QObject::thread () 获取对象存活于哪个线程。

MyThread::MyThread(QObject*parent /* = NULL */ ):QThread(parent)
{
    m_pTimer = new QTimer( this );
    qDebug()<< "MyThread object current ThreadId :" <<QThread::currentThread();

    QObject obj1;
    obj1.thread();
    qDebug()<< "obj1 live in the thread :" <<obj1.thread();

    connect(m_pTimer, SIGNAL(timeout()), this , SLOT(timeOutSlot()));
    //QThread::start();
}

void MyThread::run()
{
    QObject obj2;
    obj2.thread();
    qDebug()<< "button2 live in the thread :" <<obj2.thread();
    //m_pTimer->start(500);
    qDebug()<< "run() currentThreadId : " <<QThread::currentThread();
    qDebug( "finish!" );
}

这个再一次说明 了,对象所处的线程就是创建它的线程。

注意:对于那些在 QApplication 定义之前创建的对象,QObject::thread () 返回 0 。

使用 QObject::moveToThread () 函数可以改变对象及其子对象的线程关联度,说白了就是把对象从当前的线程移到另外的线程里。但是如果一个对象已经有了 parent ,那是不能 move 了。

调用 delete 删除处于另外一个线程的 QObject 对象是不安全的。除非你能保证对象当前不是在进行事件处理。应该用QObject::deleteLater () 替代,这样会发出一个 DeferredDelete 事件,这个事件会最终会被对象所在线程的事件循环所捕获。

如果没有事件循环,就不会有事件传递给对象。例如,如果你在一个线程中创建了一个QTimer对象,但是不调用exec() , ,那么 QTimer永远不会发出timeout()信号,调用deleteLater() 也不起作用。

void MyThread::run()
{
    m_pTimer = new QTimer();
    m_pTimer->start(500);
    connect(m_pTimer, SIGNAL (timeout()), this , SLOT (timeOutSlot()));

    qDebug()<< "run() currentThreadId : " <<QThread::currentThread();
    this ->exec();
    //qDebug("finish!" );
}

void MyThread::timeOutSlot()
{
    qDebug()<< "timer timeout " ;
    //m_pTimer->stop();
}

这时候是可以调用 timeOutSlot() 的。

void MyThread::run()
{
    m_pTimer = new QTimer();
    m_pTimer->start(500);
    connect(m_pTimer, SIGNAL (timeout()), this , SLOT (timeOutSlot()));
    qDebug()<< "run() currentThreadId : " <<QThread::currentThread();
    //this->exec();
    //qDebug("finish!" );
}

如果注释 this->exec(); , timeOutSlot() 将不会被调用。

还有一点要注意的:QTimer对象也不能在另外的线程 stop。如果把 timeOutSlot 里的 m_pTimer->stop(); 取消注释。会看到一行输出:

QObject::killTimer: timers cannot be stopped from another thread

你可以用QCoreApplication::postEvent () 函数在任意时间给任意线程中的任意对象发送事件,事件自动被创建object的线程的事件循环分发。所有的线程都支持事件过滤器,唯一的限制就是,监视对象必须与被监视对象处于同一个线程。

同样的,QCoreApplication::sendEvent () 只能用来给与调用 QCoreApplication::sendEvent () 函数处于同一个线程的对象发送事件。说白了就是, QCoreApplication::sendEvent () 不能给处于另外线程的对象发送事件。

Accessing QObject Subclasses from Other Threads

QObject 和它所有的子类都不是线程安全的。这包含了整个事件发送系统,需要记住的很重要的一点是:事件循环可能正在给一个对象发送一个事件,同时你可能从别的线程访问该对象。

如果你调用了一个不是处于当前线程 QObject 子类对象的一个函数,而此时对象可能接收一个事件,你必须用一个 mutex 保护对象的内在的数据。否则,可能引起程序崩溃或者未定义的行为。

与其他的对象一样, QThread对象存活于创建对象的线程中,而不是存在于QThread::run () 线程。这点在前面讲到了。在自定义QThread子类中提供slot函数是不安全的,除非你用一个 mutex 保护了成员变量。然而,你可以在实现的 QThread::run () 里发出信号,因为信号发送是线程安全的。

Signals and Slots Across Threads

Qt 支持了几种信号(槽的连接方式):

  1. Auto Connection ( 默认 ) :如果如果信号的发送方与接收方是处于同一个线程,这个连接就是 Direct Connection ,否则就跟 Queued Connection 一样。
  2. Direct Connection :当信号发出之后,槽会立即被调用。槽函数是在信号发送方的线程中运行的,不需要接收方的线程。
  3. Queued Connection:当控制权回到接收方线程时调用槽函数。槽函数是在接收方的线程中运行的。
  4. Blocking Queued Connection :调用方式跟 Queued Connection 一样,区别在于,当前线程会被阻塞直到槽函数返回。
  5. Unique Connection :这种方式跟 Auto Connection 一样,但是只有当不存在一个相同的连接时才会创建一个连接。如果已经存在相同的连接,则不会创建连接, connect() 返回 false 。

可以在 connect()添加参数指定连接类型。需要注意的一点是:如果信号发送方和接收方处于不同的线程,而且接收方线程运行着一个事件循环,此时用 Direct Connection 是不安全,原因跟调用一个对象的函数,而这个对象处于另外的线程一样,那样的调用是不安全。

QObject::connect () 本身是线程安全的。

下面通过结果例子验证一下:

class Receiver: public QObject
{
    Q_OBJECT
    public :
        void sendmes()
        {
            emit emitSignal( "emit message from A To B" );
        }

        Receiver()
        {

        }

        protected  slots :
            void messageSlot(QString mes)
            {
                qDebug()<void emitSignal(QString mes);
};

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

    QApplication a(argc, argv);
    Receiver objA,objB;
    QObject::connect(&objA, SIGNAL (emitSignal(QString)),&objB, SLOT (messageSlot(QString)));
    qDebug()<< "beforeemitsignal " ;
    objA.sendmes();
    qDebug()<< "afteremitsignal " ;
    return a.exec();
}

objA,objB;出于同一个线程,所以connect的连接类型是Direct Connection

由输出我们可以看出执行顺序,

如果我们写了两句连接:

QObject::connect(&objA, SIGNAL (emitSignal(QString)),&objB, SLOT (messageSlot(QString)));
QObject::connect(&objA, SIGNAL (emitSignal(QString)),&objB, SLOT (messageSlot(QString)));

就会相应的有两句消息输出:

如果指定了连接类型 Qt::UniqueConnection ,就会只有一句消息输出了。

QObject::connect(&objA, SIGNAL (emitSignal(QString)),&objB, SLOT (messageSlot(QString)),Qt::UniqueConnection );
QObject::connect(&objA, SIGNAL (emitSignal(QString)),&objB, SLOT (messageSlot(QString)),Qt::UniqueConnection);

int main( int argc, char *argv[])
{
    QApplication a(argc, argv);
    QThread *thread = new QThread;
    thread->start();
    Receiver objA,objB;
    objB.moveToThread(thread);
    QObject::connect(&objA, SIGNAL(emitSignal(QString)), &objB, SLOT(messageSlot(QString)));
    qDebug()<< "beforeemitsignal " ;
    objA.sendmes();
    qDebug()<< "afteremitsignal " ;
    return a.exec();
}

如果我们把 objB放到另外的线程,connect的连接类型应该是Queued Connection 。

int main( int argc, char *argv[])
{
    QApplication a(argc, argv);
    QThread *thread = new QThread;
    thread->start();
    Receiver objA,objB;
    objB.moveToThread(thread);
    QObject::connect(&objA, SIGNAL (emitSignal(QString)),&objB, SLOT (messageSlot(QString)) ,Qt::BlockingQueuedConnection);
    qDebug()<< "beforeemitsignal " ;
    objA.sendmes();
    qDebug()<< "afteremitsignal " ;
    return a.exec();
}

显示的指定连接类型为 Qt::BlockingQueuedConnection,则输出为:

可重入和线程安全

参考:可重入和线程安全简单介绍 - hai200501019的专栏 - 博客频道 - CSDN.NET

可重入和线程安全这两个术语,经常出现在计算机编程中,用于指明类和函数在多线程程序中的使用。

可重入 一个类被称为是可重入的:只要在同一时刻至多只有一个线程访问同一个实例,那么我们说多个线程可以安全地使用各自线程内自己的实例。 一个函数被称为是可重入的:如果每一次函数的调用只访问其独有的数据(译者注:全局变量就不是独有的,而是共享的),那么我们说多个线程可以安全地调用这个函数。 也就是说,类和函数的使用者必须通过一些外部的加锁机制来实现访问对象实例或共享数据的序列化。

若一个函数是可重入的,则该函数:

  1. 不能含有静态(全局)非常量数据。
  2. 不能返回静态(全局)非常量数据的地址。
  3. 只能处理由调用者提供的数据。
  4. 不能依赖于单实例模式资源的锁。
  5. 不能调用(call)不可重入的函数。

线程安全 如果多个线程可以同时使用一个类的对象,那么这个类被称为是线程安全的;如果多个线程可以同时使用一个函数体里的共享数据,那么这个函数被称为线程安全的。
(译者注: 更多可重入(reentrant)和t线程安全(thread-safe)的解释: 对于类,如果它的所有成员函数都可以被不同线程同时调用而不相互影响——即使这些调用是针对同一个类对象,那么该类被定义为线程安全。 对于类,如果其不同实例可以在不同线程中被同时使用而不相互影响,那么该类被定义为可重入。在Qt的定义中,在类这个层次,thread-safe是比reentrant更严格的要求)

线程安全的方法可以被多个线程同时调用,即使这些调用都使用到了共享的数据,因为它对共享数据的使用都是序列化,也就是每个序列操作全部完成之前是不可能被别的线程的使用而中断的。可重入的方法也能被多个线程同时调用,但是必须是每个调用都只使用自己的数据。因此,线程安全的方法必定是可重入的,但是可重入的方法未必是线程安全的。

一个类是可重入的,指的是,只要每个线程用的是不同的类的实例。一个类是线程安全的,指的是它的成员方法可以安全的被多个线程同时调用,即使每个线程使用的是相同的类实例。

C++类一般情况下都是可重入的,因为类的成员方法一般都只能操作成员数据,每个线程都是通过自己的类实例调用成员方法,不可能存在别的线程调用同一个类实例的方法。

以下这个类就不是可重入的,很明显,存在静态数据成员。

class Counter
{

public:
    Counter() { }
    static  void decrement() { --n; }
    static  int value() { return n; }
private:
    static  int n;
};


class Counter
{
public:
    Counter() { }
    void increment() { ++n; }
    void decrement() { --n; }
    int value() { return n; }
private:
    int n;
};

简单的改一下,把static去掉就可以成为一个可重入的类。但是它并不是线程安全的,当存在多个线程试图修改n的时候,结果就有可能是未定义的。因为++ 和—操作并不是原子性的操作,其包括以下步骤:

  1. 把变量加载到一个寄存器。
  2. 寄存器进行自增或自减。
  3. 把寄存器的值重新保存到变量中。

假设存在两个线程A和B同时保存了n的旧的的值到寄存器中,再各自进行自增,随后再写回变量,这样便会出现覆盖,n实际也只增加了1。上面说过,就是线程安全方法对数据的操作必须是一序列的不可中断的,就如上面的这个例子,线程A在执行步骤1,2,3必须是连续操作的,在完成3个步骤之前,线程B不允许操作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    int n;
};

QMutexLocker 对 n 加锁和解锁,保证了对n的操作是序列化的。我们知道const成员方法不能改变实例的任何数据,所以mutex必须的声明为mutable,因为在value() const进行加锁和解锁改变了mutex。

Qt的许多类都是可重入的,但并不是线程安全的,因为保证线程安全要在每个方法的重复的进行加锁和解锁,那是相当麻烦的。

可重入与线程安全两个概念都关系到函数处理资源的方式。可重入概念会影响函数的外部接口,而线程安全只关心函数的实现。

大多数情况下,要将不可重入函数改为可重入的,需要修改函数接口,使得所有的数据都通过函数的调用者提供。

要将非线程安全的函数改为线程安全的,则只需要修改函数的实现部分。一般通过加入同步机制以保护共享的资源,使之不会被几个线程同时访问。

QObject Reentrancy

QObject 是可重入的,它的大多数非 GUI 子类,例如:

  • QTimer
  • QTcpSocket
  • QUdpSocket
  • QProcess

都是可重入的,使得这些类可以同时用于多线程。需要注意的是,这些类设计为在一个单一的线程中创建和使用的,在一个线程创建对象,而从另外一个线程调用对象的函数并不能保证行得通。有三个限制需要注意:

  1. QObject 的子对象必须在创建其 parent 的线程中创建。这意味着,你不能把 QThread 对象作为 parent 传递给在线程中创建的对象,因为QThread 对象本身在另外一个线程中创建。

  2. 事件驱动对象只能用于单线程。尤其是在定时器机制和网络模块。例如,你不能在不是对象所处的线程中 start 一个计时器或者链接一个 socket 。简单的说就是,你不能在线程 A 创建了一个计时器 timer ,然后在线程 B 从启动 timer 。

  3. 你必须保证在线程中创建的对象要在线程销毁前 delete 。这很容易做到,只要是在 run() 函数栈里创建对象就行。

我们可以验证一下:

class MyThread : public QThread
{
Q_OBJECT

public :
    MyThread(QObject *parent = NULL);
    ~MyThread();
public  slots :
    void timeOutSlot();
protected :
    void run();
    QTimer *m_pTimer;
};

MyThread::MyThread(QObject*parent /* = NULL */ ):QThread(parent)
{
    m_pTimer = new QTimer( this );
    qDebug()<< "MyThreadobject currentThreadId :" <this , SLOT (timeOutSlot()));
}

void MyThread::timeOutSlot()
{
    qDebug()<< "timer timeout " ;
}

MyThread::~MyThread()
{

}

void MyThread::run()
{
    m_pTimer->start(500);
    qDebug()<< "run() currentThreadId : " <"finish!" );
}

int main( int argc, char *argv[])
{
    QApplication a(argc, argv);

    MyThread thread;
    qDebug()<< "mainThread : " <return a.exec();
}

Timeout函数并没有被调用,因为 QTimer 在主线程中被创建,而在子线程中启动计时。我们还发现有多了一行输出:

QObject::startTimer: timers cannot be started from another thread

尽管 QObject 是可重入的,但是 GUI 类,特别是 QWidget 和它的子类都是不可重入的。它们只能在主线程中用。就如前面提到的,QCoreApplication::exec () 必须从主线程进行调用。

Web Service

  • QtSoap调用Web Service

Tutorial

  • Qt线程基础

参考

  • QThread 与 QObject的关系
  • 信号槽机制浅析


转载:http://johnnyfee.github.io/2014/05/03/qt-thread/

你可能感兴趣的:(qt)