分类: LINUX
2014-12-17 18:21:48
近来用Qt写了一个多线程的TCP服务器,每个线程为一个客户端服务,为了使客户端之间能够通信,遇到了不少麻烦事,如在运行的时候出现:“Cannot create children for a parent that is in a different thread”,更离奇的是,如果是服务器给客户端发消息后出现上面那个警告后,服务器可以继续往客户端发,但客户端不能往服务器发,成了单向通信了。为了解决上面那个警告,花了我不少时间看相关的文档和书籍,最后知道了一些以前所不知道的知识点。作为过来人,如果有同学遇到跟我一样的问题,我建议你彻底搞懂后再继续调试代码,这样既可以学到更多东西,不至于把时间花在不知头尾的问题上。
可重入一个类被称为是可重入的:只要在同一时刻至多只有一个线程访问同一个实例,那么我们说多个线程可以安全地使用各自线程内自己的实例。 一个函数被称为是可重入的:如果每一次函数的调用只访问其独有的数据(译者注:全局变量就不是独有的,而是共享的),那么我们说多个线程可以安全地调用这个函数。 也就是说,类和函数的使用者必须通过一些外部的加锁机制来实现访问对象实例或共享数据的序列化。
线程安全如果多个线程可以同时使用同一个类的对象,那么这个类被称为是线程安全的;如果多个线程可以同时使用一个函数体里的共享数据,那么这个函数被称为线程安全的。
(译者注: 更多可重入(reentrant)和线程安全(thread-safe)的解释: 对于类,如果它的所有成员函数都可以被不同线程同时调用而不相互影响——即使这些调用是针对同一个类对象,那么该类被定义为线程安全。 对于类,如果其不同实例可以在不同线程中被同时使用而不相互影响,那么该类被定义为可重入。在Qt的定义中,在类这个层次,thread-safe是比reentrant更严格的要求)
我对多线程编程中的QObject对象创建以及connect执行机制是:
1. 一个对象的线程就是创建该对象时的线程,而不论该对象的定义是保存在那个线程中;
2. QObject的connect函数有几种连接方式,
a) DirectConnection,信号发送后槽函数立即执行,由sender的所在线程执行;
b) QueuedConnection,信号发送后返回,相关槽函数由receiver所在的线程在返回到事件循环后执行;
c) 默认使用的是Qt::AutoConnection,当sender和receiver在同一个线程内时,采用DirectConnection的方式,当sender和receiver在不同的线程时,采用QueuedConnection的方式。
为了更清楚的理解这些问题,在此特编了个小例子说明一下。首先定义一个从QObject继承的类SomeObject,包含一个信号someSignal和一个成员函数callEmitSignal,此函数用于发送前面的someSignal信号。定义如下:
class SomeObject : public QObject
{
Q_OBJECT
public:
SomeObject(QObject* parent=0) : QObject(parent) {}
void callEmitSignal() // 用于发送信号的函数
{
emit someSignal();
}
signals:
void someSignal();
};
然后再定义一个从QThread继承的线程类SubThread,它包含一个SomeObject的对象指针obj,另外有一个slot函数someSolt,定义如下:
class SubThread : public QThread
{
Q_OBJECT
public:
SubThread(QObject* parent=0) : QThread(parent){}
virtual ~SubThread()
{
if (obj!=NULL) delete obj;
}
public slots:
// slot function connected to obj's someSignal
void someSlot();
public:
SomeObject * obj;
};
// slot function connected to obj's someSignal
void SubThread::someSlot()
{
QString msg;
msg.append(this->metaObject()->className());
msg.append("::obj's thread is ");
if (obj->thread() == qApp->thread())
{
msg.append("MAIN thread;");
}
else if (obj->thread() == this)
{
msg.append("SUB thread;");
}
else
{
msg.append("OTHER thread;");
}
msg.append(" someSlot executed in ");
if (QThread::currentThread() == qApp->thread())
{
msg.append("MAIN thread;");
}
else if (QThread::currentThread() == this)
{
msg.append("SUB thread;");
}
else
{
msg.append("OTHER thread;");
}
qDebug() << msg;
quit();
}
这里someSlot函数主要输出了obj所在的线程和slot函数执行线程。
接着从SubThread又继承了3个线程类,分别是SubThread1, SubThread2, SubThread3.分别实现线程的run函数。定义如下:
// define sub thread class 1
class SubThread1 : public SubThread
{
Q_OBJECT
public:
SubThread1(QObject* parent=0);
// reimplement run
void run();
};
class SubThread2 : public SubThread
{
Q_OBJECT
public:
SubThread2(QObject* parent=0);
// reimplement run
void run();
};
class SubThread3 : public SubThread
{
Q_OBJECT
public:
SubThread3(QObject* parent=0);
// reimplement run
void run();
};
在主程序中分别创建3个不同的线程并运行,查看运行结果。
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
SubThread1* t1 = new SubThread1(&a); //由主线程创建
t1->start();
SubThread2* t2 = new SubThread2(&a); //由主线程创建
t2->start();
SubThread3* t3 = new SubThread3(&a); //由主线程创建
t3->start();
return a.exec();
}
下面我们来分析不同写法的程序,其obj对象所在的线程空间和someSlot函数执行的线程空间分别是怎样的。
首先看SubThread1的实现:
////////////////////////////////////////////////////////
// class SubThread1
////////////////////////////////////////////////////////
SubThread1::SubThread1(QObject* parent)
: SubThread(parent)
{
obj = new SomeObject();//由主线程创建
connect(obj, SIGNAL(someSignal()), this, SLOT(someSlot()));
}
// reimplement run
void SubThread1::run()
{
obj->callEmitSignal();
exec();
}
可以看到,obj是在构造函数中被创建的,那么创建obj对象的线程也就是创建SubThread1的线程,一般是主线程,而不是SubThread1所代表的线程。同时由于obj和this(即t1)都位于主线程,所以someSlot函数也是由主线程来执行的。
而在线程SubThread2中,我们把obj对象的创建放到子线程的run函数中,那么obj对象的线程就应该SubThread2代表的线程,即t2,就不再是主线程了。
////////////////////////////////////////////////////////
// class SubThread2
////////////////////////////////////////////////////////
SubThread2::SubThread2(QObject* parent)
: SubThread(parent)
{
obj=0;
}
// reimplement run
void SubThread2::run()
{
obj = new SomeObject(); //由当前子线程创建
connect(obj, SIGNAL(someSignal()), this, SLOT(someSlot()));
obj->callEmitSignal();
exec();
}
同时,在connect函数中由于obj和this(这里是t2)不是在同一个线程中,因此会采用QueuedConnection的方式,其slot函数由this对象所在的线程即主线程来执行。这里有一个特别容易误解的地方,就是这个slot函数虽然是子线程SubThread2的一个成员函数,connect操作也是在子线程内完成的,但是该函数的执行却不在子线程内,而是在主线程内。
那么如果想让相应的slot函数在子线程内执行,该如何做呢?在子线程的run函数中创建obj对象的同时,在执行connect时指定连接方式为DirectConnection,这样就可以使slot函数在子线程中运行,因为DirectConnection的方式始终由sender对象的线程执行。如
////////////////////////////////////////////////////////
// class SubThread3
////////////////////////////////////////////////////////
SubThread3::SubThread3(QObject* parent)
: SubThread(parent)
{
obj=0;
}
// reimplement run
void SubThread3::run()
{
obj = new SomeObject();
connect(obj, SIGNAL(someSignal()), this, SLOT(someSlot()),
Qt::DirectConnection);
obj->callEmitSignal();
exec();
}
最后,该程序的运行结果应该是:
SubThread1::obj's thread is MAIN thread; someSlot executed in MAIN thread;"
"SubThread2::obj's thread is SUB thread; someSlot executed in MAIN thread;"
"SubThread3::obj's thread is SUB thread; someSlot executed in SUB thread;"
线程的事件循环
我们可能理所当然地认为在Qt的应用程序中只有一个事件循环,但事实并不是这样:QThread对象在它们所代表的线程中开启了新的事件循环。因此,我们说main 事件循环是由调用main()的线程通过QCoreApplication::exec() 创建的。 它也被称做是GUI线程,因为它是界面相关操作唯一允许的进程。一个QThread的局部事件循环可以通过调用QThread::exec() 来开启(它包含在run()方法的内部)
class Thread : public {
protected:
void run() {
/* ... initialize ... */
exec();
}
};
正如我们之前所提到的,自从Qt 4.4 的QThread::run() 方法不再是一个纯虚函数,它调用了QThread::exec()。就像QCoreApplication,QThread 也有QThread::quit() 和QThread::exit()来停止事件循环。
一个线程的事件循环为驻足在该线程中的所有QObjects派发了所有事件,其中包括在这个线程中创建的所有对象,或是移植到这个线程中的对象。我们说一个QObject的线程依附性(thread affinity)是指某一个线程,该对象驻足在该线程内。我们在任何时间都可以通过调用QObject::thread()来查询线程依附性,它适用于在QThread对象构造函数中构建的对象。
class MyThread : public
{
public:
MyThread()
{
otherObj = new QObject;
}
private:
obj;
*otherObj;
QScopedPointer<> yetAnotherObj;
};
如上述代码,我们在创建了MyThread 对象后,obj, otherObj, yetAnotherObj 的线程依附性是怎么样的?要回答这个问题,我们必须要看一下创建他们的线程:是这个运行MyThread 构造函数的线程创建了他们。因此,这三个对象并没有驻足在MyThread 线程,而是驻足在创建MyThread 实例的线程中。
要注意的是在QCoreApplication 对象之前创建的QObjects没有依附于某一个线程。因此,没有人会为它们做事件派发处理。(换句话说,QCoreApplication构建了代表主线程的QThread 对象)
我们可以使用线程安全的QCoreApplication::postEvent()方法来为某个对象分发事件。它将把事件加入到对象所驻足的线程事件队列中。因此,除非事件对象依附的线程有一个正在运行的事件循环,否则事件不会被派发。
在论坛、博客里面找到的解决方案是在线程的构造函数里加一个moveToThread(this)方法。
class Thread : public {
Q_OBJECT
public:
Thread() {
moveToThread(this); // 错误????有改变QThread对象的线程依附性嘛?
// this是指针,指向当前线程
}
/* ... */
};
这样做确实可以工作(因为现在线程对象的依附性已经发生了改变),但这是一个非常不好的设计。这里的错误在于我们正在误解线程对象的目的(QThread子类):QThread对象们不是线程;他们是围绕在新产生的线程周围用于控制管理新线程的对象,因此,它们应该用在另一个线程(往往在它们所驻足的那一个线程)
一个比较好而且能够得到相同结果的做法是将“工作”部分从“控制”部分剥离出来,也就是说,写一个QObject子类并使用QObject::moveToThread()方法来改变它的线程依附性:
class Worker : public
{
Q_OBJECT
public slots:
void doWork() {
/* ... */
}
};
/* ... */
thread;
Worker worker;
connect(obj, SIGNAL(workReady()), &worker, SLOT(doWork()));
worker.moveToThread(&thread);
thread.start();
这两个函数的区别就在于postEvent能跨线程“投递”,因为sendEvent是直接将事件送至目标Widget来处理,并返回处理结果,而postEvent是将事件插入处理队列之后返回,并不等待至事件获得实际处理。至此,我还有一个问题没有搞清楚,我把创建的QTcpSocket移入子线程中,但主线程向子线程投递事件,然后用此socket发送消息,采用postEvent时,socket发送消息函数在子线程中运行,但会出现错误。但采用sendEvent时,socket发送消息函数在主线程运行,却没有出现错误。
参考:
Qt多线程编程中的对象线程与函数执行线程
http://blog.csdn.net/lutx/article/details/7353957
线程、事件与QObject
首先准备个ICO图标。例如:A.ico,网上有很多图标文件。
用记事本新建个txt
里面就写一行:
IDI_ICON1 ICON DISCARDABLE "A.ico"
保存,修改后缀为.rc,例如: myapp.rc
把它和图标A.ico一起复制到你的QT工程项目的目录。
打开你的QT工程文件.pro(例如 "myapp.pro" ),
在里面最后新添一行
RC_FILE = myapp.rc
保存,重新编译你的工程。
如果想换图标,就重换一个图标,重命名为A.ico替换原来的,重新编译就可以了。