事件循环与线程

Qt 线程类

Qt对线程的支持已经有很多年了(发布于2000年九月22日的Qt2.2引入了QThread类),Qt 4.0版本的release则对其所有所支持平台默认地是对多线程支持的。(当然你也可以关掉对线程的支持,参见这里)。现在Qt提供了不少类用于处理线程,让你我们首先预览一下:

QThread

QThread 是Qt中一个对线程支持的核心的底层类。
每个线程对象代表了一个运行的线程。由于Qt的跨平台特性,QThread成功隐藏了所有在不同操作系统里使用线程的平台相关性代码。

为了运用QThread从而让代码在一个线程里运行,我们可以创建一个QThread的子类,并重载QThread::run() 方法:

  1. class Thread : public QThread {
  2. protected:
  3. void run() {
  4. /* your thread implementation goes here */
  5. }
  6. };

接着,我们可以使用:

  1. class Thread : public QThread {
  2. protected:
  3. void run() {
  4. /* your thread implementation goes here */
  5. }
  6. };

来真正的启动一个新的线程。 请注意,Qt 4.4版本之后,QThread不再支持抽象类;现在虚函数QThread::run()实际上是简单调用了QThread::exec(),而它启动了线程的事件循环。(更多信息见后文)

QRunnable 和 QThreadPool

QRunnable [doc.qt.nokia.com] 是一种轻量级的、以“run
and forget”方式来在另一个线程开启任务的抽象类,为了实现这一功能,我们所需要做的全部事情是派生QRunnable 类,并实现纯虚函数方法run()

  1. class Task : public QRunnable {
  2. public:
  3. void run() {
  4. /* your runnable implementation goes here */
  5. }
  6. };

事实上,我们是使用QThreadPool 类来运行一个QRunnable
对象,它维护了一个线程池。通过调用QThreadPool::start(runnable) ,我们把一个QRunnable 放入了QThreadPool的运行队列中;只要线程是可见得,QRunnable 将会被拾起并且在那个线程里运行。尽管所有的Qt应用程序都有一个全局的线程池,且它是通过调用QThreadPool::globalInstance()可见得,但我们总是显式地创建并管理一个私有的QThreadPool 实例。

请注意,QRunnable 并不是一个QObject类,它并没有一个内置的与其他组件显式通讯的方法。你必须使用底层的线程原语(比如收集结构的枷锁保护队列等)来亲自编写代码。

QtConcurrent

QtConcurrent 是一个构建在QThreadPool之上的上层API,它用于处理最普通的并行计算模式:map[en.wikipedia.org], reduce [en.wikipedia.org],
and filter [en.wikipedia.org] 。同时,QtConcurrent::run()方法提供了一种便于在另一个线程运行一个函数的方法。

不像QThread 以及QRunnable,QtConcurrent 没有要求我们使用底层的同步原语,QtConcurrent 所有的方法会返回一个QFuture 对象,它包含了结果而且可以用来查询线程计算的状态(它的进度),从而暂停、继续、取消计算。QFutureWatcher 可以用来监听一个QFuture
进度,并且通过信号和槽与之交互(注意QFuture是一个基于数值的类,它并没有继承自QObject).

功能比较

/ 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


线程与QObjects

线程的事件循环

我们在上文中已经讨论了事件循环,我们可能理所当然地认为在Qt的应用程序中只有一个事件循环,但事实并不是这样:QThread对象在它们所代表的线程中开启了新的事件循环。因此,我们说main
事件循环是由调用main()的线程通过QCoreApplication::exec() 创建的。 它也被称做是GUI线程,因为它是界面相关操作唯一允许的进程。一个QThread的局部事件循环可以通过调用QThread::exec() 来开启(它包含在run()方法的内部)

  1. class Thread : public QThread {
  2. protected:
  3. void run() {
  4. /* ... initialize ... */
  5. exec();
  6. }
  7. };

正如我们之前所提到的,自从Qt 4.4 的QThread::run() 方法不再是一个纯虚函数,它调用了QThread::exec()。就像QCoreApplication,QThread 也有QThread::quit() 和QThread::exit()来停止事件循环。

一个线程的事件循环为驻足在该线程中的所有QObjects派发了所有事件,其中包括在这个线程中创建的所有对象,或是移植到这个线程中的对象。我们说一个QObject的线程依附性(thread
affinity)是指某一个线程,该对象驻足在该线程内。我们在任何时间都可以通过调用QObject::thread()来查询线程依附性,它适用于在QThread对象构造函数中构建的对象。

  1. class MyThread : public QThread
  2. {
  3. public:
  4. MyThread()
  5. {
  6. otherObj = new QObject;
  7. }
  8. private:
  9. QObject obj;
  10. QObject *otherObj;
  11. QScopedPointer<QObject> yetAnotherObj;
  12. };

如上述代码,我们在创建了MyThread 对象后,obj, otherObj, yetAnotherObj 的线程依附性是怎么样的?要回答这个问题,我们必须要看一下创建他们的线程:是这个运行MyThread 构造函数的线程创建了他们。因此,这三个对象并没有驻足在MyThread
线程,而是驻足在创建MyThread 实例的线程中。

要注意的是在QCoreApplication 对象之前创建的QObjects没有依附于某一个线程。因此,没有人会为它们做事件派发处理。(换句话说,QCoreApplication
构建了代表主线程的QThread 对象)

我们可以使用线程安全的QCoreApplication::postEvent() 方法来为某个对象分发事件。它将把事件加入到对象所驻足的线程事件队列中。因此,除非事件对象依附的线程有一个正在运行的事件循环,否则事件不会被派发。

理解QObject和它所有的子类不是线程安全的(尽管是可重入的)非常重要;因此,除非你序列化对象内部数据所有可访问的接口、数据,否则你不能让多个线程同一时刻访问相同的QObject(比如,用一个锁来保护)。请注意,尽管你可以从另一个线程访问对象,但是该对象此时可能正在处理它所驻足的线程事件循环派发给它的事件!
基于这种原因,你不能从另一个线程去删除一个QObject,一定要使用QObject::deleteLater(),它会Post一个事件,目标删除对象最终会在它所生存的线程中被删除。(译者注:QObject::deleteLater作用是,当控制流回到该对象所依附的线程事件循环时,该对象才会被“本”线程中删除)。

此外,QWidget 和它所有的子类,以及所有与GUI相关的类(即便不是基于QObject的,像QPixmap)并不是可重入的。它们必须专属于GUI线程。

我们可以通过调用QObject::moveToThread()来改变一个QObject的依附性;它将改变这个对象以及它的孩子们的依附性。因为QObject不是线程安全的,我们必须在对象所驻足的线程中使用此函数;也就是说,你只能将对象从它所驻足的线程中推送到其他线程中,而不能从其他线程中拉回来。此外,Qt要求一个QObject的孩子必须与它们的双亲驻足在同一个线程中。这意味着:

  • 你不能使用QObject::moveToThread()作用于有双亲的对象;
  • 你千万不要在一个线程中创建对象的同时把QThread对象自己作为它的双亲。 (译者注:两者不在同一个线程中):
  1. class Thread : public QThread {
  2. void run() {
  3. QObject obj = new QObject(this); // WRONG!!!
  4. }
  5. };

这是因为,QThread 对象驻足在另一个线程中,即QThread 对象它自己被创建的那个线程中。

Qt同样要求所有的对象应该在代表该线程的QThread对象销毁之前得以删除;实现这一点并不难:只要我们所有的对象是在QThread::run() 方法中创建即可。(译者注:run函数的局部变量,函数返回时得以销毁)。

跨线程的信号与槽

接着上面讨论的,我们如何应用驻足在其他线程里的QObject方法呢?Qt提供了一种非常友好而且干净的解决方案:向事件队列post一个事件,事件的处理将以调用我们所感兴趣的方法为主(当然这需要线程有一个正在运行的事件循环)。而触发机制的实现是由moc提供的内省方法实现的(译者注:有关内省的讨论请参见我的另一篇文章Qt的内省机制剖析):因此,只有信号、槽以及被标记成Q_INVOKABLE的方法才能够被其它线程所触发调用。

静态方法QMetaObject::invokeMethod() 为我们做了如下工作:

  1. QMetaObject::invokeMethod(object, "methodName",
  2. Qt::QueuedConnection,
  3. Q_ARG(type1, arg1),
  4. Q_ARG(type2, arg2));

请注意,因为上面所示的参数需要被在构建事件时进行硬拷贝,参数的自定义型别所对应的类需要提供一个共有的构造函数、析构函数以及拷贝构造函数。而且必须使用注册Qt型别系统所提供的qRegisterMetaType() 方法来注册这一自定义型别。

跨线程的信号槽的工作方式相类似。当我们把信号连接到一个槽的时候,QObject::connect的第五个可选输入参数用来特化这一连接类型:

  • direct connection 是指:发起信号的线程会直接触发其所连接的槽;
  • queued connection 是指:一个事件被派发到接收者所在的线程中,在这里,事件循环会之后的某一时间将该事件拾起并引起槽的调用;
  • blocking queued connection 与queued connection的区别在于,发送者的线程会被阻塞,直至接收者所在线程的事件循环处理发送者发送(入栈)的事件,当连接信号的槽被触发后,阻塞被解除;
  • automatic connection (缺省默认参数) 是指: 如果接收者所依附的线程和当前线程是同一个线程,direct
    connection会被使用。否则使用queued connection。

请注意,在上述四种连接方式当中,发送对象驻足于哪一个线程并不重要!对于automatic connection,Qt会检查触发信号的线程,并且与接收者所驻足的线程相比较从而决定到底使用哪一种连接类型。特别要指出的是:当前的Qt文档的声明(4.7.1)
是错误的:

如果发射者和接受者在同一线程,其行为与Direct Connection相同;,如果发射者和接受者不在同一线程,其行为Queued Connection相同

因为,发送者对象的线程依附性在这里无关紧要。举例子说明

  1. class Thread : public QThread
  2. {
  3. Q_OBJECT
  4. signals:
  5. void aSignal();
  6. protected:
  7. void run() {
  8. emit aSignal();
  9. }
  10. };
  11. /* ... */
  12. Thread thread;
  13. Object obj;
  14. QObject::connect(&thread, SIGNAL(aSignal()), &obj, SLOT(aSlot()));
  15. thread.start();

如上述代码,信号aSignal() 将在一个新的线程里被发射(由线程对象所代表);因为它并不是Object 对象驻足的线程,所以尽管Thread对象thread与Object对象obj在同一个线程,但仍然是queued
connection被使用。

(译者注:这里作者分析的很透彻,希望读者仔细揣摩Qt文档的这个错误。 也就是说 发送者对象本身在哪一个线程对与信号槽连接类型不起任何作用,起到决定作用的是接收者对象所驻足的线程以及发射信号(该信号与接受者连接)的线程是不是在同一个线程,本例中aSignal()在新的线程中被发射,所以采用queued
connection)。

另外一个常见的错误如下:

  1. class Thread : public QThread
  2. {
  3. Q_OBJECT
  4. slots:
  5. void aSlot() {
  6. /* ... */
  7. }
  8. protected:
  9. void run() {
  10. /* ... */
  11. }
  12. };
  13. /* ... */
  14. Thread thread;
  15. Object obj;
  16. QObject::connect(&obj, SIGNAL(aSignal()), &thread, SLOT(aSlot()));
  17. thread.start();
  18. obj.emitSignal();

当“obj”发射了一个aSignal()信号是,哪种连接将被使用呢?你也许已经猜到了:direct connection。这是因为Thread对象实在发射该信号的线程中生存。在aSlot()槽里,我们可能接着去访问线程里的一些成员变量,然而这些成员变量可能同时正在被run()方法访问:这可是导致完美灾难的秘诀。可能你经常在论坛、博客里面找到的解决方案是在线程的构造函数里加一个moveToThread(this)方法。

class Thread : public QThread {

Q_OBJECT

public:

Thread() {

moveToThread(this); //
错误

}

/* ... */

};

(译注:moveToThread(this)

这样做确实可以工作(因为现在线程对象的依附性已经发生了改变),但这是一个非常不好的设计。这里的错误在于我们正在误解线程对象的目的(QThread子类):QThread对象们不是线程;他们是围绕在新产生的线程周围用于控制管理新线程的对象,因此,它们应该用在另一个线程(往往在它们所驻足的那一个线程)

一个比较好而且能够得到相同结果的做法是将“工作”部分从“控制”部分剥离出来,也就是说,写一个QObject子类并使用QObject::moveToThread()方法来改变它的线程依附性:

  1. class Worker : public QObject
  2. {
  3. Q_OBJECT
  4. public slots:
  5. void doWork() {
  6. /* ... */
  7. }
  8. };
  9. /* ... */
  10. QThread thread;
  11. Worker worker;
  12. connect(obj, SIGNAL(workReady()), &worker, SLOT(doWork()));
  13. worker.moveToThread(&thread);
  14. thread.start();

我应该什么时候使用线程

当你不得不使用一个阻塞式API时

当你需要(通过信号和槽,或者是事件、回调函数)使用一个没有提供非阻塞式API的库或者代码时,为了阻止冻结事件循环的唯一可行的解决方案是开启一个进程或者线程。由于创建一个新的进程的开销显然要比开启一个线程的开销大,后者往往是最常见的一种选择。

这种API的一个很好的例子是地址解析 方法(只是想说我们并不准备谈论蹩脚的第三方API, 地址解析方法它是每个C库都要包含的),它负责将主机名转化为地址。这个过程涉及到启动一个查询(通常是远程的)系统:域名系统或者叫DNS。尽管通常情况下响应会在瞬间发生,但远程服务器可能会失败:一些数据包可能会丢失,网络连接可能断开等等。简而言之,我们也许要等待几十秒才能得到查询的响应。

UNIX系统可见的标准API只有阻塞式的(不仅过时的gethostbyname(3)是阻塞式的,而且更新的getservbyname(3) 以及getaddrinfo(3)也是阻塞式的)。QHostInfo [doc.qt.nokia.com], 
它是一个负责处理域名查找的Qt类,该类使用了QThreadPool 从而使得查询可以在后台进行)(参见here [qt.gitorious.com]);如果屏蔽了多线程支持,它将切换回到阻塞式API).

另一个简单的例子是图像装载和放大。QImageReader [doc.qt.nokia.com] 和QImage [doc.qt.nokia.com]仅仅提供了阻塞式方法来从一个设备读取图像,或者放大图像到一个不同的分辨率。如果你正在处理一个非常大的图像,这些处理会持续数(十)秒。

当你想扩展至多核

多线程允许你的程序利用多核系统的优势。因为每个线程都是被操作系统独立调度的,因此如果你的应用运行在这样多核机器上,调度器很可能同时在不同的处理器上运行每个线程。

例如,考虑到一个通过图像集生成缩略图的应用。一个_n_ threads的线程农场(也就是说,一个有着固定数量线程的线程池),在系统中可见的CPU运行一个线程(可参见QThread::idealThreadCount()),可以将缩小图像至缩略图的工作交付给所有的进程,从而有效地提高了并行加速比,它与处理器的数量成线性关系。(简单的讲,我们认为CPU正成为一个瓶颈)。

什么时候你可能不想别人阻塞

这是一个很高级的话题,你可以忽略该小节。一个比较好的例子来自于Webkit里使用的QNetworkAccessManager 。Webkit是一个时髦的浏览器引擎,也就是说,它是一组用于处理网页的布局和显示的类集合。使用Webkit的Qt widget是QWebView。

QNetworkAccessManager 是一个用于处理HTTP任何请求和响应的Qt类,我们可以把它当作一个web浏览器的网络引擎;所有的网络访问被同一个QNetworkAccessManager 以及它的QNetworkReplys 驻足的线程所处理。

尽管在网络处理时不使用线程是一个很好的主意,它也有一个很大的缺点:如果你没有从socket中尽快地读取数据,内核的缓存将会被填满,数据包可能开始丢失而且传输速率也将迅速下降。

Sokcet活动(即,从一个socket读取一些数据的可见性)由Qt的事件循环管理。阻塞事件循环因此会导致传输性能的损失,因为没有人会被通知将有数据可以读取(从而没人会去读数据)。

但究竟什么会阻塞事件循环呢?令人沮丧地回答: WebKit它自己!只要有数据被接收到,WebKit便用其来布局网页。不幸地是,布局处理过程相当复杂,而且开销巨大。因此,它阻塞事件循环的一小段时间足以影响到正在进行地传输(宽带连接这里起到了作用,在短短几秒内就可填满内核缓存)。

总结一下上述所发生的事情:

  • WebKit提出了一个请求;
  • 一些响应数据开始到达;
  • WebKit开始使用接收到的数据布局网页,从而阻塞了事件循环;
  • 数据被OS接受,但没有一个正在运行的事件循环为之派发,所以并没有被QNetworkAccessManager sockets所读取;
  • 内核缓存将被填满,传输将变慢。

网页的总体装载时间因其自发引起的传输速率降低而变得越来越坏。

诺基亚的工程师正在试验一个支持多线程的QNetworkAccessManager来解决这个问题。请注意因为QNetworkAccessManagers 和QNetworkReplys 是QObjects,他们不是线程安全的,因此你不能简单地将他们移到另一个线程中并且继续在你的线程中使用他们,原因在于,由于事件将被随后线程的事件循环所派发,他们可能同时被两个线程访问:你自己的线程以及已经它们驻足的线程。

是么时候不需要使用线程

If you think you need threads then your processes are too fat.—Rob Pike

计时器

这也许是线程滥用最坏的一种形式。如果我们不得不重复调用一个方法(比如每秒),许多人会这样做:

  1. // 非常之错误
  2. while (condition) {
  3. doWork();
  4. sleep(1); // this is sleep(3) from the C library
  5. }

然后他们发现这会阻塞事件循环,因此决定引入线程:

  1. // 错误
  2. class Thread : public QThread {
  3. protected:
  4. void run() {
  5. while (condition) {
  6. // notice that "condition" may also need volatiness and mutex protection
  7. // if we modify it from other threads (!)
  8. doWork();
  9. sleep(1); // this is QThread::sleep()
  10. }
  11. }
  12. };

一个更好也更简单的获得相同效果的方法是使用timers,即一个QTimer[doc.qt.nokia.com]对象,并设置一秒的超时时间,并让doWork方法成为它的槽:

  1. class Worker : public QObject
  2. {
  3. Q_OBJECT
  4. public:
  5. Worker() {
  6. connect(&timer, SIGNAL(timeout()), this, SLOT(doWork()));
  7. timer.start(1000);
  8. }
  9. private slots:
  10. void doWork() {
  11. /* ... */
  12. }
  13. private:
  14. QTimer timer;
  15. };

所有我们需要做的就是运行一个事件循环,然后doWork()方法将会被每隔秒钟调用一次。

网络/状态机

一个处理网络操作非常之常见的设计模式如下:

  1. socket->connect(host);
  2. socket->waitForConnected();
  3. data = getData();
  4. socket->write(data);
  5. socket->waitForBytesWritten();
  6. socket->waitForReadyRead();
  7. socket->read(response);
  8. reply = process(response);
  9. socket->write(reply);
  10. socket->waitForBytesWritten();
  11. /* ... and so on ... */

不用多说,各种各样的waitFor*()函数阻塞了调用者使其无法返回到事件循环,UI被冻结等等。请注意上面的这段代码并没有考虑到错误处理,否则它会更加地笨重。这个设计中非常错误的地方是我们正在忘却网络编程是异步的设计,如果我们构建一个同步的处理方法,则是自己给自己找麻烦。为了解决这个问题,许多人简单得将这些代码移到另一个线程中。

另一个更加抽象的例子:

  1. result = process_one_thing();
  2. if (result->something())
  3. process_this();
  4. else
  5. process_that();
  6. wait_for_user_input();
  7. input = read_user_input();
  8. process_user_input(input);
  9. /* ... */

它多少反映了网络编程相同的陷阱。

让我们回过头来从更高的角度来想一下我们这里正在构建的代码:我们想创造一个状态机,用以反映某类的输入并相对应的作某些动作。比如,上面的这段网络代码,我们可能想做如下这些事情:

  • 空闲→ 正在连接 (当调用connectToHost());
  • 正在连接→ 已经连接(当connected() 信号被发射);
  • 已经连接→ 发送录入数据 (当我们发送录入的数据给服务器);
  • 发送录入数据 → 录入 (服务器响应一个ACK)
  • 发送录入数据→ 录入错误(服务器响应一个NACK)

以此类推。

现在,有很多种方式来构建状态机(Qt甚至提供了QStateMachine[doc.qt.nokia.com]类),最简单的方式是用一个枚举值(及,一个整数)来记忆当前的状态。我们可以这样重写以下上面的代码:

  1. class Object : public QObject
  2. {
  3. Q_OBJECT
  4. enum State {
  5. State1, State2, State3 /* and so on */
  6. };
  7. State state;
  8. public:
  9. Object() : state(State1)
  10. {
  11. connect(source, SIGNAL(ready()), this, SLOT(doWork()));
  12. }
  13. private slots:
  14. void doWork() {
  15. switch (state) {
  16. case State1:
  17. /* ... */
  18. state = State2;
  19. break;
  20. case State2:
  21. /* ... */
  22. state = State3;
  23. break;
  24. /* etc. */
  25. }
  26. }
  27. };

那么“souce”对象和它的信号“ready()” 究竟是什么? 我们想让它们是什么就是什么:比如说,在这个例子中,我们可能想把我们的槽连接到socket的QAbstractSocket::connected() 以及QIODevice::readyRead()
信号中,当然,我们也可以简单地在我们的用例中加更多的槽(比如一个槽用于处理错误情况,它将会被QAbstractSocket::error() 信号所通知)。这是一个真正的异步的,信号驱动的设计!

分解任务拆成不同的块

假如我们有一个开销很大的计算,它不能够轻易的移到另一个线程中(或者说它根本不能被移动,举个例子,它必须运行在GUI线程中)。如果我们能将计算拆分成小的块,我们就能返回到事件循环,让它来派发事件,并让它激活处理下一个块相应的函数。如果我们还记得queued
connections是怎么实现的,那么会觉得这是很容易能够做到的:一个事件派发到接收者所驻足的线程的事件循环;当事件被传递,相应的槽随之被激活。

我们可以使用特化QMetaObject::invokeMethod() 的激活类型为Qt::QueuedConnection 来得到相同的结果;这需要函数是可激活的。因此它需要一个槽或者用Q_INVOKABLE宏来标识。如果我们同时想给函数中传入参数,他们需要使用Qt元对象类型系统里的qRegisterMetaType()进行注册。请看下面这段代码:

  1. class Worker : public QObject
  2. {
  3. Q_OBJECT
  4. public slots:
  5. void startProcessing()
  6. {
  7. processItem(0);
  8. }
  9. void processItem(int index)
  10. {
  11. /* process items[index] ... */
  12. if (index < numberOfItems)
  13. QMetaObject::invokeMethod(this,
  14. "processItem",
  15. Qt::QueuedConnection,
  16. Q_ARG(int, index + 1));
  17. }
  18. };

因为并没有引入多线程,所以暂停/进行/取消这样的计算并收集回结果变得简单。(结束)

原文出处:

http://developer.qt.nokia.com/wiki/ThreadsEventsQObjects

请尊重原创作品和译文。转载请保持文章完整性,并以超链接形式注明原始作者主站点地址,方便其他朋友提问和指正。

Qt 线程类

Qt对线程的支持已经有很多年了(发布于2000年九月22日的Qt2.2引入了QThread类),Qt 4.0版本的release则对其所有所支持平台默认地是对多线程支持的。(当然你也可以关掉对线程的支持,参见这里)。现在Qt提供了不少类用于处理线程,让你我们首先预览一下:

QThread

QThread 是Qt中一个对线程支持的核心的底层类。
每个线程对象代表了一个运行的线程。由于Qt的跨平台特性,QThread成功隐藏了所有在不同操作系统里使用线程的平台相关性代码。

为了运用QThread从而让代码在一个线程里运行,我们可以创建一个QThread的子类,并重载QThread::run() 方法:

  1. class Thread : public QThread {
  2. protected:
  3. void run() {
  4. /* your thread implementation goes here */
  5. }
  6. };

接着,我们可以使用:

  1. class Thread : public QThread {
  2. protected:
  3. void run() {
  4. /* your thread implementation goes here */
  5. }
  6. };

来真正的启动一个新的线程。 请注意,Qt 4.4版本之后,QThread不再支持抽象类;现在虚函数QThread::run()实际上是简单调用了QThread::exec(),而它启动了线程的事件循环。(更多信息见后文)

QRunnable 和 QThreadPool

QRunnable [doc.qt.nokia.com] 是一种轻量级的、以“run
and forget”方式来在另一个线程开启任务的抽象类,为了实现这一功能,我们所需要做的全部事情是派生QRunnable 类,并实现纯虚函数方法run()

  1. class Task : public QRunnable {
  2. public:
  3. void run() {
  4. /* your runnable implementation goes here */
  5. }
  6. };

事实上,我们是使用QThreadPool 类来运行一个QRunnable
对象,它维护了一个线程池。通过调用QThreadPool::start(runnable) ,我们把一个QRunnable 放入了QThreadPool的运行队列中;只要线程是可见得,QRunnable 将会被拾起并且在那个线程里运行。尽管所有的Qt应用程序都有一个全局的线程池,且它是通过调用QThreadPool::globalInstance()可见得,但我们总是显式地创建并管理一个私有的QThreadPool 实例。

请注意,QRunnable 并不是一个QObject类,它并没有一个内置的与其他组件显式通讯的方法。你必须使用底层的线程原语(比如收集结构的枷锁保护队列等)来亲自编写代码。

QtConcurrent

QtConcurrent 是一个构建在QThreadPool之上的上层API,它用于处理最普通的并行计算模式:map[en.wikipedia.org], reduce [en.wikipedia.org],
and filter [en.wikipedia.org] 。同时,QtConcurrent::run()方法提供了一种便于在另一个线程运行一个函数的方法。

不像QThread 以及QRunnable,QtConcurrent 没有要求我们使用底层的同步原语,QtConcurrent 所有的方法会返回一个QFuture 对象,它包含了结果而且可以用来查询线程计算的状态(它的进度),从而暂停、继续、取消计算。QFutureWatcher 可以用来监听一个QFuture
进度,并且通过信号和槽与之交互(注意QFuture是一个基于数值的类,它并没有继承自QObject).

功能比较

/ 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


线程与QObjects

线程的事件循环

我们在上文中已经讨论了事件循环,我们可能理所当然地认为在Qt的应用程序中只有一个事件循环,但事实并不是这样:QThread对象在它们所代表的线程中开启了新的事件循环。因此,我们说main
事件循环是由调用main()的线程通过QCoreApplication::exec() 创建的。 它也被称做是GUI线程,因为它是界面相关操作唯一允许的进程。一个QThread的局部事件循环可以通过调用QThread::exec() 来开启(它包含在run()方法的内部)

  1. class Thread : public QThread {
  2. protected:
  3. void run() {
  4. /* ... initialize ... */
  5. exec();
  6. }
  7. };

正如我们之前所提到的,自从Qt 4.4 的QThread::run() 方法不再是一个纯虚函数,它调用了QThread::exec()。就像QCoreApplication,QThread 也有QThread::quit() 和QThread::exit()来停止事件循环。

一个线程的事件循环为驻足在该线程中的所有QObjects派发了所有事件,其中包括在这个线程中创建的所有对象,或是移植到这个线程中的对象。我们说一个QObject的线程依附性(thread
affinity)是指某一个线程,该对象驻足在该线程内。我们在任何时间都可以通过调用QObject::thread()来查询线程依附性,它适用于在QThread对象构造函数中构建的对象。

  1. class MyThread : public QThread
  2. {
  3. public:
  4. MyThread()
  5. {
  6. otherObj = new QObject;
  7. }
  8. private:
  9. QObject obj;
  10. QObject *otherObj;
  11. QScopedPointer<QObject> yetAnotherObj;
  12. };

如上述代码,我们在创建了MyThread 对象后,obj, otherObj, yetAnotherObj 的线程依附性是怎么样的?要回答这个问题,我们必须要看一下创建他们的线程:是这个运行MyThread 构造函数的线程创建了他们。因此,这三个对象并没有驻足在MyThread
线程,而是驻足在创建MyThread 实例的线程中。

要注意的是在QCoreApplication 对象之前创建的QObjects没有依附于某一个线程。因此,没有人会为它们做事件派发处理。(换句话说,QCoreApplication
构建了代表主线程的QThread 对象)

我们可以使用线程安全的QCoreApplication::postEvent() 方法来为某个对象分发事件。它将把事件加入到对象所驻足的线程事件队列中。因此,除非事件对象依附的线程有一个正在运行的事件循环,否则事件不会被派发。

理解QObject和它所有的子类不是线程安全的(尽管是可重入的)非常重要;因此,除非你序列化对象内部数据所有可访问的接口、数据,否则你不能让多个线程同一时刻访问相同的QObject(比如,用一个锁来保护)。请注意,尽管你可以从另一个线程访问对象,但是该对象此时可能正在处理它所驻足的线程事件循环派发给它的事件!
基于这种原因,你不能从另一个线程去删除一个QObject,一定要使用QObject::deleteLater(),它会Post一个事件,目标删除对象最终会在它所生存的线程中被删除。(译者注:QObject::deleteLater作用是,当控制流回到该对象所依附的线程事件循环时,该对象才会被“本”线程中删除)。

此外,QWidget 和它所有的子类,以及所有与GUI相关的类(即便不是基于QObject的,像QPixmap)并不是可重入的。它们必须专属于GUI线程。

我们可以通过调用QObject::moveToThread()来改变一个QObject的依附性;它将改变这个对象以及它的孩子们的依附性。因为QObject不是线程安全的,我们必须在对象所驻足的线程中使用此函数;也就是说,你只能将对象从它所驻足的线程中推送到其他线程中,而不能从其他线程中拉回来。此外,Qt要求一个QObject的孩子必须与它们的双亲驻足在同一个线程中。这意味着:

  • 你不能使用QObject::moveToThread()作用于有双亲的对象;
  • 你千万不要在一个线程中创建对象的同时把QThread对象自己作为它的双亲。 (译者注:两者不在同一个线程中):
  1. class Thread : public QThread {
  2. void run() {
  3. QObject obj = new QObject(this); // WRONG!!!
  4. }
  5. };

这是因为,QThread 对象驻足在另一个线程中,即QThread 对象它自己被创建的那个线程中。

Qt同样要求所有的对象应该在代表该线程的QThread对象销毁之前得以删除;实现这一点并不难:只要我们所有的对象是在QThread::run() 方法中创建即可。(译者注:run函数的局部变量,函数返回时得以销毁)。

跨线程的信号与槽

接着上面讨论的,我们如何应用驻足在其他线程里的QObject方法呢?Qt提供了一种非常友好而且干净的解决方案:向事件队列post一个事件,事件的处理将以调用我们所感兴趣的方法为主(当然这需要线程有一个正在运行的事件循环)。而触发机制的实现是由moc提供的内省方法实现的(译者注:有关内省的讨论请参见我的另一篇文章Qt的内省机制剖析):因此,只有信号、槽以及被标记成Q_INVOKABLE的方法才能够被其它线程所触发调用。

静态方法QMetaObject::invokeMethod() 为我们做了如下工作:

  1. QMetaObject::invokeMethod(object, "methodName",
  2. Qt::QueuedConnection,
  3. Q_ARG(type1, arg1),
  4. Q_ARG(type2, arg2));

请注意,因为上面所示的参数需要被在构建事件时进行硬拷贝,参数的自定义型别所对应的类需要提供一个共有的构造函数、析构函数以及拷贝构造函数。而且必须使用注册Qt型别系统所提供的qRegisterMetaType() 方法来注册这一自定义型别。

跨线程的信号槽的工作方式相类似。当我们把信号连接到一个槽的时候,QObject::connect的第五个可选输入参数用来特化这一连接类型:

  • direct connection 是指:发起信号的线程会直接触发其所连接的槽;
  • queued connection 是指:一个事件被派发到接收者所在的线程中,在这里,事件循环会之后的某一时间将该事件拾起并引起槽的调用;
  • blocking queued connection 与queued connection的区别在于,发送者的线程会被阻塞,直至接收者所在线程的事件循环处理发送者发送(入栈)的事件,当连接信号的槽被触发后,阻塞被解除;
  • automatic connection (缺省默认参数) 是指: 如果接收者所依附的线程和当前线程是同一个线程,direct
    connection会被使用。否则使用queued connection。

请注意,在上述四种连接方式当中,发送对象驻足于哪一个线程并不重要!对于automatic connection,Qt会检查触发信号的线程,并且与接收者所驻足的线程相比较从而决定到底使用哪一种连接类型。特别要指出的是:当前的Qt文档的声明(4.7.1)
是错误的:

如果发射者和接受者在同一线程,其行为与Direct Connection相同;,如果发射者和接受者不在同一线程,其行为Queued Connection相同

因为,发送者对象的线程依附性在这里无关紧要。举例子说明

  1. class Thread : public QThread
  2. {
  3. Q_OBJECT
  4. signals:
  5. void aSignal();
  6. protected:
  7. void run() {
  8. emit aSignal();
  9. }
  10. };
  11. /* ... */
  12. Thread thread;
  13. Object obj;
  14. QObject::connect(&thread, SIGNAL(aSignal()), &obj, SLOT(aSlot()));
  15. thread.start();

如上述代码,信号aSignal() 将在一个新的线程里被发射(由线程对象所代表);因为它并不是Object 对象驻足的线程,所以尽管Thread对象thread与Object对象obj在同一个线程,但仍然是queued
connection被使用。

(译者注:这里作者分析的很透彻,希望读者仔细揣摩Qt文档的这个错误。 也就是说 发送者对象本身在哪一个线程对与信号槽连接类型不起任何作用,起到决定作用的是接收者对象所驻足的线程以及发射信号(该信号与接受者连接)的线程是不是在同一个线程,本例中aSignal()在新的线程中被发射,所以采用queued
connection)。

另外一个常见的错误如下:

  1. class Thread : public QThread
  2. {
  3. Q_OBJECT
  4. slots:
  5. void aSlot() {
  6. /* ... */
  7. }
  8. protected:
  9. void run() {
  10. /* ... */
  11. }
  12. };
  13. /* ... */
  14. Thread thread;
  15. Object obj;
  16. QObject::connect(&obj, SIGNAL(aSignal()), &thread, SLOT(aSlot()));
  17. thread.start();
  18. obj.emitSignal();

当“obj”发射了一个aSignal()信号是,哪种连接将被使用呢?你也许已经猜到了:direct connection。这是因为Thread对象实在发射该信号的线程中生存。在aSlot()槽里,我们可能接着去访问线程里的一些成员变量,然而这些成员变量可能同时正在被run()方法访问:这可是导致完美灾难的秘诀。可能你经常在论坛、博客里面找到的解决方案是在线程的构造函数里加一个moveToThread(this)方法。

class Thread : public QThread {

Q_OBJECT

public:

Thread() {

moveToThread(this); //
错误

}

/* ... */

};

(译注:moveToThread(this)

这样做确实可以工作(因为现在线程对象的依附性已经发生了改变),但这是一个非常不好的设计。这里的错误在于我们正在误解线程对象的目的(QThread子类):QThread对象们不是线程;他们是围绕在新产生的线程周围用于控制管理新线程的对象,因此,它们应该用在另一个线程(往往在它们所驻足的那一个线程)

一个比较好而且能够得到相同结果的做法是将“工作”部分从“控制”部分剥离出来,也就是说,写一个QObject子类并使用QObject::moveToThread()方法来改变它的线程依附性:

  1. class Worker : public QObject
  2. {
  3. Q_OBJECT
  4. public slots:
  5. void doWork() {
  6. /* ... */
  7. }
  8. };
  9. /* ... */
  10. QThread thread;
  11. Worker worker;
  12. connect(obj, SIGNAL(workReady()), &worker, SLOT(doWork()));
  13. worker.moveToThread(&thread);
  14. thread.start();

我应该什么时候使用线程

当你不得不使用一个阻塞式API时

当你需要(通过信号和槽,或者是事件、回调函数)使用一个没有提供非阻塞式API的库或者代码时,为了阻止冻结事件循环的唯一可行的解决方案是开启一个进程或者线程。由于创建一个新的进程的开销显然要比开启一个线程的开销大,后者往往是最常见的一种选择。

这种API的一个很好的例子是地址解析 方法(只是想说我们并不准备谈论蹩脚的第三方API, 地址解析方法它是每个C库都要包含的),它负责将主机名转化为地址。这个过程涉及到启动一个查询(通常是远程的)系统:域名系统或者叫DNS。尽管通常情况下响应会在瞬间发生,但远程服务器可能会失败:一些数据包可能会丢失,网络连接可能断开等等。简而言之,我们也许要等待几十秒才能得到查询的响应。

UNIX系统可见的标准API只有阻塞式的(不仅过时的gethostbyname(3)是阻塞式的,而且更新的getservbyname(3) 以及getaddrinfo(3)也是阻塞式的)。QHostInfo [doc.qt.nokia.com], 
它是一个负责处理域名查找的Qt类,该类使用了QThreadPool 从而使得查询可以在后台进行)(参见here [qt.gitorious.com]);如果屏蔽了多线程支持,它将切换回到阻塞式API).

另一个简单的例子是图像装载和放大。QImageReader [doc.qt.nokia.com] 和QImage [doc.qt.nokia.com]仅仅提供了阻塞式方法来从一个设备读取图像,或者放大图像到一个不同的分辨率。如果你正在处理一个非常大的图像,这些处理会持续数(十)秒。

当你想扩展至多核

多线程允许你的程序利用多核系统的优势。因为每个线程都是被操作系统独立调度的,因此如果你的应用运行在这样多核机器上,调度器很可能同时在不同的处理器上运行每个线程。

例如,考虑到一个通过图像集生成缩略图的应用。一个_n_ threads的线程农场(也就是说,一个有着固定数量线程的线程池),在系统中可见的CPU运行一个线程(可参见QThread::idealThreadCount()),可以将缩小图像至缩略图的工作交付给所有的进程,从而有效地提高了并行加速比,它与处理器的数量成线性关系。(简单的讲,我们认为CPU正成为一个瓶颈)。

什么时候你可能不想别人阻塞

这是一个很高级的话题,你可以忽略该小节。一个比较好的例子来自于Webkit里使用的QNetworkAccessManager 。Webkit是一个时髦的浏览器引擎,也就是说,它是一组用于处理网页的布局和显示的类集合。使用Webkit的Qt widget是QWebView。

QNetworkAccessManager 是一个用于处理HTTP任何请求和响应的Qt类,我们可以把它当作一个web浏览器的网络引擎;所有的网络访问被同一个QNetworkAccessManager 以及它的QNetworkReplys 驻足的线程所处理。

尽管在网络处理时不使用线程是一个很好的主意,它也有一个很大的缺点:如果你没有从socket中尽快地读取数据,内核的缓存将会被填满,数据包可能开始丢失而且传输速率也将迅速下降。

Sokcet活动(即,从一个socket读取一些数据的可见性)由Qt的事件循环管理。阻塞事件循环因此会导致传输性能的损失,因为没有人会被通知将有数据可以读取(从而没人会去读数据)。

但究竟什么会阻塞事件循环呢?令人沮丧地回答: WebKit它自己!只要有数据被接收到,WebKit便用其来布局网页。不幸地是,布局处理过程相当复杂,而且开销巨大。因此,它阻塞事件循环的一小段时间足以影响到正在进行地传输(宽带连接这里起到了作用,在短短几秒内就可填满内核缓存)。

总结一下上述所发生的事情:

  • WebKit提出了一个请求;
  • 一些响应数据开始到达;
  • WebKit开始使用接收到的数据布局网页,从而阻塞了事件循环;
  • 数据被OS接受,但没有一个正在运行的事件循环为之派发,所以并没有被QNetworkAccessManager sockets所读取;
  • 内核缓存将被填满,传输将变慢。

网页的总体装载时间因其自发引起的传输速率降低而变得越来越坏。

诺基亚的工程师正在试验一个支持多线程的QNetworkAccessManager来解决这个问题。请注意因为QNetworkAccessManagers 和QNetworkReplys 是QObjects,他们不是线程安全的,因此你不能简单地将他们移到另一个线程中并且继续在你的线程中使用他们,原因在于,由于事件将被随后线程的事件循环所派发,他们可能同时被两个线程访问:你自己的线程以及已经它们驻足的线程。

是么时候不需要使用线程

If you think you need threads then your processes are too fat.—Rob Pike

计时器

这也许是线程滥用最坏的一种形式。如果我们不得不重复调用一个方法(比如每秒),许多人会这样做:

  1. // 非常之错误
  2. while (condition) {
  3. doWork();
  4. sleep(1); // this is sleep(3) from the C library
  5. }

然后他们发现这会阻塞事件循环,因此决定引入线程:

  1. // 错误
  2. class Thread : public QThread {
  3. protected:
  4. void run() {
  5. while (condition) {
  6. // notice that "condition" may also need volatiness and mutex protection
  7. // if we modify it from other threads (!)
  8. doWork();
  9. sleep(1); // this is QThread::sleep()
  10. }
  11. }
  12. };

一个更好也更简单的获得相同效果的方法是使用timers,即一个QTimer[doc.qt.nokia.com]对象,并设置一秒的超时时间,并让doWork方法成为它的槽:

  1. class Worker : public QObject
  2. {
  3. Q_OBJECT
  4. public:
  5. Worker() {
  6. connect(&timer, SIGNAL(timeout()), this, SLOT(doWork()));
  7. timer.start(1000);
  8. }
  9. private slots:
  10. void doWork() {
  11. /* ... */
  12. }
  13. private:
  14. QTimer timer;
  15. };

所有我们需要做的就是运行一个事件循环,然后doWork()方法将会被每隔秒钟调用一次。

网络/状态机

一个处理网络操作非常之常见的设计模式如下:

  1. socket->connect(host);
  2. socket->waitForConnected();
  3. data = getData();
  4. socket->write(data);
  5. socket->waitForBytesWritten();
  6. socket->waitForReadyRead();
  7. socket->read(response);
  8. reply = process(response);
  9. socket->write(reply);
  10. socket->waitForBytesWritten();
  11. /* ... and so on ... */

不用多说,各种各样的waitFor*()函数阻塞了调用者使其无法返回到事件循环,UI被冻结等等。请注意上面的这段代码并没有考虑到错误处理,否则它会更加地笨重。这个设计中非常错误的地方是我们正在忘却网络编程是异步的设计,如果我们构建一个同步的处理方法,则是自己给自己找麻烦。为了解决这个问题,许多人简单得将这些代码移到另一个线程中。

另一个更加抽象的例子:

  1. result = process_one_thing();
  2. if (result->something())
  3. process_this();
  4. else
  5. process_that();
  6. wait_for_user_input();
  7. input = read_user_input();
  8. process_user_input(input);
  9. /* ... */

它多少反映了网络编程相同的陷阱。

让我们回过头来从更高的角度来想一下我们这里正在构建的代码:我们想创造一个状态机,用以反映某类的输入并相对应的作某些动作。比如,上面的这段网络代码,我们可能想做如下这些事情:

  • 空闲→ 正在连接 (当调用connectToHost());
  • 正在连接→ 已经连接(当connected() 信号被发射);
  • 已经连接→ 发送录入数据 (当我们发送录入的数据给服务器);
  • 发送录入数据 → 录入 (服务器响应一个ACK)
  • 发送录入数据→ 录入错误(服务器响应一个NACK)

以此类推。

现在,有很多种方式来构建状态机(Qt甚至提供了QStateMachine[doc.qt.nokia.com]类),最简单的方式是用一个枚举值(及,一个整数)来记忆当前的状态。我们可以这样重写以下上面的代码:

  1. class Object : public QObject
  2. {
  3. Q_OBJECT
  4. enum State {
  5. State1, State2, State3 /* and so on */
  6. };
  7. State state;
  8. public:
  9. Object() : state(State1)
  10. {
  11. connect(source, SIGNAL(ready()), this, SLOT(doWork()));
  12. }
  13. private slots:
  14. void doWork() {
  15. switch (state) {
  16. case State1:
  17. /* ... */
  18. state = State2;
  19. break;
  20. case State2:
  21. /* ... */
  22. state = State3;
  23. break;
  24. /* etc. */
  25. }
  26. }
  27. };

那么“souce”对象和它的信号“ready()” 究竟是什么? 我们想让它们是什么就是什么:比如说,在这个例子中,我们可能想把我们的槽连接到socket的QAbstractSocket::connected() 以及QIODevice::readyRead()
信号中,当然,我们也可以简单地在我们的用例中加更多的槽(比如一个槽用于处理错误情况,它将会被QAbstractSocket::error() 信号所通知)。这是一个真正的异步的,信号驱动的设计!

分解任务拆成不同的块

假如我们有一个开销很大的计算,它不能够轻易的移到另一个线程中(或者说它根本不能被移动,举个例子,它必须运行在GUI线程中)。如果我们能将计算拆分成小的块,我们就能返回到事件循环,让它来派发事件,并让它激活处理下一个块相应的函数。如果我们还记得queued
connections是怎么实现的,那么会觉得这是很容易能够做到的:一个事件派发到接收者所驻足的线程的事件循环;当事件被传递,相应的槽随之被激活。

我们可以使用特化QMetaObject::invokeMethod() 的激活类型为Qt::QueuedConnection 来得到相同的结果;这需要函数是可激活的。因此它需要一个槽或者用Q_INVOKABLE宏来标识。如果我们同时想给函数中传入参数,他们需要使用Qt元对象类型系统里的qRegisterMetaType()进行注册。请看下面这段代码:

  1. class Worker : public QObject
  2. {
  3. Q_OBJECT
  4. public slots:
  5. void startProcessing()
  6. {
  7. processItem(0);
  8. }
  9. void processItem(int index)
  10. {
  11. /* process items[index] ... */
  12. if (index < numberOfItems)
  13. QMetaObject::invokeMethod(this,
  14. "processItem",
  15. Qt::QueuedConnection,
  16. Q_ARG(int, index + 1));
  17. }
  18. };

因为并没有引入多线程,所以暂停/进行/取消这样的计算并收集回结果变得简单。(结束)

原文出处:

http://developer.qt.nokia.com/wiki/ThreadsEventsQObjects

请尊重原创作品和译文。转载请保持文章完整性,并以超链接形式注明原始作者主站点地址,方便其他朋友提问和指正

【Qt开发】事件循环与线程 二的更多相关文章

  1. 【转】Qt事件循环与线程 二

    转自:http://blog.csdn.net/changsheng230/article/details/6153449 续上文:http://blog.csdn.net/changsheng230 ...

  2. 【Qt开发】事件循环与线程 一

    事件循环与线程 一 初次读到这篇文章,译者感觉如沐春风,深刻体会到原文作者是花了很大功夫来写这篇文章的,文章深入浅出,相信仔细读完原文或下面译文的读者一定会有收获. 由于原文很长,原文作者的行文思路是 ...

  3. 事件循环和线程没有必然关系(就像Windows子线程默认没有消息循环一样),模态对话框和事件循环也没有必然关系(QWidget直接就可以)

    周末天冷,索性把电脑抱到床上上网,这几天看了 dbzhang800 博客关于 Qt 事件循环的几篇 Blog,发现自己对 Qt 的事件循环有不少误解.从来只看到现象,这次借 dbzhang800 的博 ...

  4. 在不开启事件循环的线程中使用QTimer(QThread::run函数自带事件循环,在构造函数里创建线程,是一种很有意思的线程用法) good

    引入 QTimer是Qt自带的定时器类,QTimer运行时是依赖于事件循环的,简单来说,在一个不开启事件循环(未调用exec() )的线程中,QTimer是无法使用的.通过分析Qt源码可发现,调用QT ...

  5. 关于Qt的事件循环以及QEventLoop的简单使用

    1.一般我们的事件循环都是由exec()来开启的,例如下面的例子: 1 QCoreApplicaton::exec() 2 QApplication::exec() 3 QDialog::exec() ...

  6. 关于Qt的事件循环以及QEventLoop的简单使用(QEventLoop::quit()能够终止事件循环,事件循环是可以嵌套的)

    http://www.cnblogs.com/-wang-cheng/p/4973021.html 1.一般我们的事件循环都是由exec()来开启的,例如下面的例子: 1 QCoreApplicato ...

  7. Qt开发中的实用笔记二--中文转码问题和string转换问题:

    一,中文乱码转码问题 1,转码三句话:window下默认是GBK格式,linux下默认是UTF-8,看情况转换UTF-8/GBK QTextCodec::setCodecForTr(QTextCode ...

  8. 分享我用Qt开发的应用程序【二】在Qt应用程序中使用字体图标fontawesome

    为了使用简单,需要先写一个单件类,头文件的代码如下: 其中静态方法Instance保证IconHelper的实例全局唯一 (注意构造函数已经私有化了) #ifndefICONHELPER_H #def ...

  9. Qt 的线程与事件循环——可打印threadid进行观察槽函数到底是在哪个线程里执行,学习moveToThread的使用)

    周末天冷,索性把电脑抱到床上上网,这几天看了 dbzhang800 博客关于 Qt 事件循环的几篇 Blog,发现自己对 Qt 的事件循环有不少误解.从来只看到现象,这次借 dbzhang800 的博 ...

随机推荐

  1. jmeter上传文件tips

    转载:https://blog.csdn.net/aduocd/article/details/87916676 转载:https://www.cnblogs.com/landhu/p/8980195 ...

  2. C语言字符串追加,双色球等案例

    //C语言中没有字符串概念,有的只是字符型数组,以str1[]的值为例,该字符数组的长度为11--->包含了字母,空格,以及结束字符'\0'(斜杠0)//基于上述原因,读取一个字符型数组的有效方 ...

  3. CodeForces-721D-Maxim and Array(优先队列,贪心,分类讨论)

    链接: https://vjudge.net/problem/CodeForces-721D 题意: Recently Maxim has found an array of n integers, ...

  4. 【NOIP2016提高A组模拟8.19】(雅礼联考day2)公约数

    题目 给定一个正整数,在[1,n]的范围内,求出有多少个无序数对(a,b)满足gcd(a,b)=a xor b. 分析 显然a=b是一定不满足, 我们设\(a>b\), 易得gcd(a,b)&l ...

  5. rmq——同步、异步、单向、rocketMQ console、消费模式

    官网上下载:rocketmq-all-4.5.0-bin-release 配置环境变量:

  6. Quantitative Strategies for Achieving Alpha (三)

    chapter 4: Profitability Profitability measures we tested include return on invested capital, return ...

  7. 6424. 【NOIP2019模拟2019.11.13】我的订书机之恋

    题目描述 Description Input Output Sample Input 见下载 Sample Output 见下载 Data Constraint 题解 lj题卡线段树 求出每个右端点往 ...

  8. 为什么要重写hashcode( )和equals( )?

    打个比方,一个名叫张三的人去住酒店,在前台登记完名字就去了99层100号房间,此时警察来前台找叫张三的这个人住在哪间房,经过查询,该酒店住宿的有50个叫张三的,需要遍历查询,查询起来很不方便. 那么就 ...

  9. ROM和RAM的内存详细说明

    1.首先是ROM 的读取是需要提前两个地址的读取,所以要想读取0地址的数据,你需要给地址是2 2.关于宽度,深度的计算 假设我们要存取如下取模的数据,该模的设置口语描述为:这是显示的2个字节,其中一个 ...

  10. 安装原版 Windows 7 后需要安装的微软更新 和 必备系统组件

    Windows 7 SP1 和 Windows Server 2008 R2 SP1 更新历史记录 https://support.microsoft.com/zh-cn/help/4009469 微 ...