Qt跨线程发送信号与元数据
Qt的signals/slots是可以用在线程间的。由于事件循环(event loop)是在主线程完成的,所以在非主线程发送一个信号时,对应的槽函数将会由主线程执行。
熟悉多线程的读者应该都感受到这里会有一个微妙的问题。如果signals/slots的函数参数是一个自己定义的类型。比如自己定义了一个Student类,信号函数为sendStudent(const Student &stu);对应的槽函数为:getStudent(const Student &stu);如果在非主线程使用emit发射信号的时候Student参数是一个临时变量的话(即可能马上被析构掉),那么主线程在执行这个槽函数的时候这个临时变量可能被析构了。这就相当于使用了野指针。
Qt的作者肯定也想到了这一点。
在connect函数中,我们一般都只使用4个参数。实际上它是有5个参数的,只是使用了默认参数而已。第5个参数是一个枚举类型Qt::ConnectionType,有下面5种:
Qt::AutoConnection: 如果发射信号的线程和执行槽函数的线程是在同一个线程,此时等同于Qt::DirectConnection。如果不在同一个线程,就等同于Qt::QueuedConnection。这个是connect函数的默认参数Qt::DirectConnection: 发射信号和执行槽是由同一个线程完成。此时槽函数马上被执行。执行完毕后才执行”emit 信号”后面的代码。即”emit 信号”是阻塞的
Qt::QueuedConnection: 发射信号的线程和执行槽函数的线程不是在同一个线程。此时发射信号的线程不阻塞,马上返回。当执行槽函数的线程被CPU调度时,就会执行槽函数
Qt::BlockingQueuedConnection: 和Qt::QueuedConnection基本一样,只是发射信号的线程会被阻塞,知道槽函数被执行完毕。所以如果设置了这个connect属性,那么就要确保发射信号线程不是执行槽函数的线程。否则将发生死锁
Qt::UniqueConnection: 唯一关联。即同一个信号与同一个槽只能调用connect一次。不能多次调用。注意,此时一个信号还是可以关联多个槽的上面枚举中,有说到是否在同一个线程。其判断也简单,执行槽函数的线程是执行事件循环的线程,即执行QCoreApplication a(argc, argv); a.exec()函数的线程。一般都是主线程执行事件循环。发射线程也就是调用emit的线程。
从上面的关联类型可以看到,Qt的作者是有考虑到发射信号的线程不是执行槽函数的线程。那么Qt是怎么解决刚才那个微妙的问题的呢?答案是元数据。在调用connect的前面调用qRegisterMetaType<Student>("Student");把这个Student类型注册成元数据。这样就能避免那个问题了。
由于我没有阅读Qt的源代码,不知道Qt内部具体是怎么实现的。我把Qt的实现当作一个黑盒子,通过一个文档和测试进行一些猜想。下面就是我的猜想,不一定正确。
假如这个问题给我们自己来处理,想到的方法是:在内部复制一个Student类。这样无论发射线程的临时Student是否被析构都无所谓了。
通过阅读Qt助手的qRegisterMetaType词条,可以看到,把一个类型注册成元数据是有条件的。就是这个类型要提供public属性的默认构造函数、复制构造函数、析构函数。这应该是为了复制一个Student吧。哈哈。下面通过一个例子验证之。
头文件:
01.
#ifndef TK_HPP
02.
#define TK_HPP
03.
04.
#include<QString>
05.
#include<<a href=
"http://www.it165.net/pro/pkqt/"
target=
"_blank"
class
=
"keylink"
>QT</a>hread>
06.
#include<QDebug>
07.
08.
class
Student
09.
{
10.
public
:
11.
Student(){}
12.
Student(
const
QString &name,
const
QString &id)
13.
:m_name(name), m_id(id)
14.
{}
15.
16.
Student(
const
Student& stu)
17.
{
18.
//故意这样赋值。就是让Qt不能正确复制构造。哈哈!!!
19.
m_name =
"xxxx"
;
20.
}
21.
22.
QString name()
const
{
return
m_name; }
23.
void
name(
const
QString& name) { m_name = name; }
24.
25.
private
:
26.
QString m_name;
27.
QString m_id;
28.
};
29.
30.
31.
class
Test :
public
QObject
32.
{
33.
Q_OBJECT
34.
public
:
35.
Test()
36.
{
37.
qRegisterMetaType<Student>(
"Student"
);
38.
39.
connect(
this
, SIGNAL(sendStu(Student)),
40.
this
, SLOT(getStu(Student)));
//, Qt::QueuedConnection );
41.
}
42.
43.
private
slots:
44.
45.
void
getStu(
const
Student &stu)
46.
{
47.
qDebug()<<<a href=
"http://www.it165.net/pro/pkqt/"
target=
"_blank"
class
=
"keylink"
>QT</a>hread::currentThreadId()<<
" "
<<stu.name();
48.
}
49.
50.
public
:
51.
void
printStu(
const
Student& stu)
52.
{
53.
emit sendStu(stu);
54.
}
55.
56.
private
:
57.
signals:
58.
void
sendStu(
const
Student& stu);
59.
};
60.
61.
class
MyThread :
public
QThread
62.
{
63.
Q_OBJECT
64.
65.
public
:
66.
MyThread(Test * test) : m_test(test)
67.
{}
68.
69.
protected
:
70.
void
run()
71.
{
72.
qDebug()<<
"non main thread "
<<QThread::currentThreadId()<<
'\n'
;
73.
Student stu(
"aaa"
,
"213"
);
74.
75.
//发射信号
76.
m_test->printStu(stu);
77.
78.
stu.name(
"bbb"
);
79.
80.
qDebug()<<
"I have reset student name\n"
;
81.
}
82.
83.
private
:
84.
Test *m_test;
85.
};
86.
87.
#endif
// TK_HPP
源文件:01.
#include <QCoreApplication>
02.
#include
"tk.hpp"
03.
04.
05.
int
main(
int
argc,
char
*argv[])
06.
{
07.
QCoreApplication a(argc, argv);
08.
Test test;
09.
10.
MyThread mythread(&test);
11.
mythread.start();
12.
13.
return
a.exec();
14.
}
执行输出为:
可以看到,Qt果然是复制错误了。
如果删除Student中的复制构造函数,那么输出就会通ky"http://www.it165.net/qq/" target="_blank" class="keylink">qqjujwvcD4KICAgICAgICA8aW1nIHNyYz0="http://www.it165.net/uploadfile/files/2014/0919/20140919193200334.jpg" alt="">
可以看到,Qt内部已经复制了一份Student,所以即使次线程修改了Student的name,也不影响。
如果在Test的构造函数中,删除qRegisterMetaType<Student>("Student")。那么在运行时候就会出现:
估计Qt在emit的实现里面会判断发出信号的线程是否为主线程。如果不是的话,那么就检测signals/slots的参数是否为一个元数据。如果不是的话那么就拒绝发送信号。非元数据是不安全的。
对于C/C++的基本类型和Qt定义的类,都不用我们手动将之注册为元数据了。Qt已经帮我们干了这些事情
转载于:https://blog.51cto.com/11496263/1881797