跨线程通信消息组件的实现

85 阅读12分钟

大家在项目中肯定都碰到过在子线程中需要刷新 UI 界面的需求,对于大多数实现了跨线程消息通信的 GUI 框架来说,开发者要实现这点就很容易,但当 GUI 没有提供跨线程消息机制的时候,就需要我们自己造一个了。

下面是我最近在项目中实现的一个跨线程通信消息队列,主要设计参考了 Android 的 Handler。不同点是 Android 主线程是完全消息驱动的,在没有消息时会阻塞,而我们的项目中主线程除了管理 UI 外,还有其他工作,所以不能采用这种阻塞模型,而是使用的定时询问。

好了,下面我们一起看看具体实现吧。

1、设计目的及思路

该消息队列是为了实现跨线程消息通信。考虑如下场景:

现有线程 1 和线程 2 两个线程,线程 1 负责刷新 UI,现在线程 2 也想刷新 UI,然而它不能自己去刷新,得委托线程 1 来干这个活。没问题,但线程 1 不知道 UI 具体有哪些变动,只有线程 2 才清楚,所以怎么刷还得线程 2 去定义,线程 1 只负责调用。

添加图片注释,不超过 140 字(可选)

这个刷新 UI 的过程可以总结为:

  1. 线程 2 发送刷新 UI 的消息给线程 1;
  2. 线程 1 调用线程 2 定义的方法来刷新。

需求就是这么简单一回事,怎么实现呢?

  1. 我们需要一个组件发消息,线程 2 通过这个组件能把消息发送给线程 1;
  2. 消息里面有什么,肯定得有消息 id,然后还得告诉线程 1 具体的刷法,所以可以一并放在消息里。
  3. 线程 1 有一个处理消息的组件,什么时候处理消息呢?可以一直等待着,有消息过来就处理,没有就干等;还可以定期检查是否有消息过来,有就进行处理,没有就继续干别的事。这里就要按自身的需求选择了,我们的线程 1 还有别的活,所以不能干等,得采用定期检查的方式。

我是参照的 Android Handler 的设计,命名也就直接延用过来。

  1. 发送消息的组件叫做 Handler;
  2. 处理消息的组件叫做 Looper;
  3. 消息就叫 Message,哪都是。

2、具体的设计

2.1 Message 的设计

我们刚刚已经分析出,Message 需要包括消息 id 和该消息的具体处理方法两部分。大部分时候是够用的,但有时间用户可能想放点数据,在处理的时候使用,所以还要添加一项 userDate。

好,Message 的主要部分就这些东西了。

实际项目中,为了避免内存的频繁创建和释放,尤其是消息这种短生命周期的对象,常常会采用消息池的设计。我们也搞个简单的消息池,就直接在 Message 里定义两个静态方法就好了,obtainMessage() 用于拿取消息对象,freeMessage() 用于释放消息对象,释放不是真释放,只是打个 flag 而已,那么显然,我们的消息还需要加上一个 flag 成员用于表示是否释放。

添加图片注释,不超过 140 字(可选)

消息类的定义如上,有同学可能有疑问了。

不是说消息类要包含对该消息的具体处理方法嘛,上面似乎没有?

再看看,你们肯定也发现了有一个 Handler* 类型的成员变量,我们之前说过 Handler 是消息的发送者,并且它知道具体怎么处理消息。是不是明白了,放一个 Handler 就已经能知道怎么处理该消息了。

Message.h

#ifndef MESSAGE_H
#define MESSAGE_H

#include <list>
#include <mutex>

class Handler;
class Message {
public:
    Message();
    static Message* obtainMessage();
    static void freeMessage(Message* msg);

    int id;
    void* userDate;
    Handler* mHandler;
private:
    bool free;

    static std::list<Message*> msgPool;
    static int poolSize;
    static std::mutex poolMutex;
};

#endif // MESSAGE_H

Message.cpp

#include "Message.h"
#include <iostream>

std::list<Message*> Message::msgPool;
int Message::poolSize = 20;
std::mutex Message::poolMutex;

Message::Message() : free(true) {

}

Message* Message::obtainMessage() {
    poolMutex.lock();
    auto it = msgPool.begin();
    for (; it != msgPool.end(); it++) {
        if ((*it)->free) {
            (*it)->free = false;
            poolMutex.unlock();
            return *it;
        }
    }
    if (msgPool.size() < poolSize) {
        Message *msg = new Message();
        msg->free = false;
        msgPool.emplace_back(msg);
        poolMutex.unlock();
        return msg;
    }
    poolMutex.unlock();
    printf("no free msg, obtain fail");
    return nullptr;
}

void Message::freeMessage(Message* msg) {
    poolMutex.lock();
    msg->free = true;
    poolMutex.unlock();
}

这里的内存池大小为 32,是因为我在 Looper 中定义的消息队列最大长度为 16,32 能够让两个线程同时拥有全满的消息队列,在实践中已经足够了。

2.2、Handler 的设计

你可能在前面就想知道 Handler 是怎么设计的了,之前我们分析过,Handler 有两个主要作用。

  1. 用来发送消息;
  2. 管理具体怎么处理消息。

所以 Handler 的成员方法里肯定有一个用来发送消息的方法,还有一个用来处理消息的方法。Handler 怎么把消息发送给接收者 Looper 呢?那必然得持有一个 Looper 了,这样就知道发送给谁了。

添加图片注释,不超过 140 字(可选)

Handler.h

#ifndef HANDLER_H
#define HANDLER_H

#include "Looper.h"

class Handler {
public:
    Handler();
    Handler(Looper* looper);
    virtual ~Handler();
    void sendMessage(int id, void* userDate);
    virtual void handleMessage(int id, void* userDate) = 0;
private:
    Looper* mLooper;
};

#endif // HANDLER_H

Handler.cpp

#include "Handler.h"
#include "Message.h"
#include <iostream>
#include <thread>

Handler::Handler() : mLooper(nullptr) {
    Looper* looper = Looper::getLooper();
    if (!looper) {
        printf("[%s] fail, cur thread dose not exist looper\n", __func__);
    } else {
        mLooper = looper;
    }
}

Handler::Handler(Looper* looper) : mLooper(looper) {

}

Handler::~Handler() {}

void Handler::sendMessage(int id, void* userDate) {
    std::cout<<"["<<__func__<<"]"<<"sendMessage:"<<id
            <<", thread:"<<std::this_thread::get_id()<<std::endl;
    if (!mLooper) {
        printf("fail, no looper");
        return;
    }
    Message* msg = Message::obtainMessage();
    if (!msg) {
        printf("obtain message fail");
        return;
    }
    msg->id = id;
    msg->userDate = userDate;
    msg->mHandler = this;
    mLooper->addMsg(msg);
}

有几个点值得说一下:

  1. Handler 有两个构造方法。当传入一个 Looper 对象的时候,Handler 将和传入的 Looper 绑定,后续发消息就会发给这个 Looper;而当未传人参数时,Handler 就会绑定当前线程的 Looper,如果当前线程不存在 Looper,就会绑定失败;
  2. handleMessage() 就是具体的怎么处理消息的方法,但在这里是个纯虚方法,具体实现要交给子类来完成。因为我们不知道使用者需要处理什么消息,以及怎么处理每个消息,所以使用者需要定义自己的 Handler 继承自当前类,并重写 handleMessage() 方法。

我们看下我实现的 Handler 具体类。

MyHandler.h

#ifndef MYHANDLER_H
#define MYHANDLER_H

#include "Handler.h"

class MyHandler : public Handler {
public:
    MyHandler();
    MyHandler(Looper* looper);
    void handleMessage(int id, void* userDate) override;
};


#endif //MYHANDLER_H

MyHandler.cpp

#include "MyHandler.h"
#include <iostream>

MyHandler::MyHandler() : Handler() {}

MyHandler::MyHandler(Looper* looper) : Handler(looper) {}

void MyHandler::handleMessage(int id, void* userDate) {
    std::cout<<"["<<__func__<<"]"<<"handleMessage:"<<id
            <<", thread:"<<std::this_thread::get_id()<<std::endl;
}

我们重写了 handleMessage() 这个方法,在里面打印了消息的 id,具体实践中当然可以根据消息 id 作出不同的处理逻辑。

2.3 Looper 的设计

我们说 Looper 是处理消息的组件,并采用定时查询的方式,处理 Handler 传过来的消息,那一个 Looper 必须要包含以下几点:

  1. 一个保存消息的队列,Handler 把消息放到这个队列里,当 Looper 定时查看发现队列里有消息就会去处理;
  2. 刚刚说 Handler 把消息放到队列里,那怎么放呢?这,显然是 Looper 要提供一个把消息放进队列里的方法,不然 Handler 怎么放呢。
  3. 定时查询怎么做呢?最简单的就是放一个 timer 成员,每隔一定时间就去处理消息队列里的消息。至于这个 timer 怎么搞,如果你使用的 GUI 提供了这个机制就很方便,如果没有呢?那我们有机会再聊怎么自己做一个,跟本文的主旨关系不是那么密切。
  4. 好,到此 Looper 包含的要素已经齐全了。作的好一点,我们可以再包装一个处理消息队列里消息的方法,这样 timer 每次到时间后就调用这个方法就好了。
  5. 有没有什么遗漏呢?我们的这套机制提出来是用来做跨线程通信的,接收消息的线程需要实现这么一个 Looper,怎么实现呢?用户并不想关心怎么实现,我们得提供这样一个接口;此外,发送消息的 Handler 也需要一个 Looper,我们得提供另一个接口用来获取已经实现的 Looper(并不是创建新的 Looper)。
  6. 补充,本条写在全文完结后。每个线程可以开启 Looper 循环,那么也可以选择结束 Looper,所以还应该提供一个静态方法,让线程注销其 Looper,操作就是从 map 中移除该线程对应的 Looper,当然别忘了释放相应的内存。我就偷个懒,只在这里说明一下吧,全文就不动了。

添加图片注释,不超过 140 字(可选)

Looper.h

#ifndef LOOPER_H
#define LOOPER_H

#include "Message.h"
#include <deque>
#include <unordered_map>
#include <thread>
#include <mutex>

class Looper {
public:
    void addMsg(Message* msg);

    static void prepareLooper(bool isUiLooper = false);
    static Looper* getMainLooper();
    static Looper* getLooper();

private:
    void dispatchMessage();
    void prePare();

    std::deque<Message*> msgQue;
    std::mutex mMutex;
    static int queMaxsize;
    static std::mutex looperMutex;
    static std::unordered_map<std::thread::id, Looper*> threadLooper;
    static std::thread::id mainThread;
};

#endif // LOOPER_H

Looper.cpp

#include "Looper.h"
#include "Handler.h"
#include <iostream>
#include <chrono>

int Looper::queMaxsize = 16;
std::mutex Looper::looperMutex;
std::unordered_map<std::thread::id, Looper*> Looper::threadLooper;
std::thread::id Looper::mainThread;

void Looper::dispatchMessage() {
    while (!msgQue.empty()) {
        mMutex.lock();
        auto msg = msgQue.front();
        msgQue.pop_front();
        mMutex.unlock();
        msg->mHandler->handleMessage(msg->id, msg->userDate);
        Message::freeMessage(msg);
    }
    printf("[%s] messageque is empty\n",__func__);
}

void Looper::addMsg(Message* msg) {
    mMutex.lock();
    if (msgQue.size() == queMaxsize) {
        printf("[%s] drop message : %d\n",__func__, msgQue.front()->id);
        Message::freeMessage(msgQue.front());
        msgQue.pop_front();
    }
    msgQue.emplace_back(msg);
    mMutex.unlock();
}

void Looper::prepareLooper(bool isUiLooper) {
    looperMutex.lock();
    if (threadLooper.find(std::this_thread::get_id()) != threadLooper.end()) {
        printf("[%s]the looper is already in work", __func__);
        looperMutex.unlock();
        return;
    }
    Looper* looper = new Looper();
    threadLooper.emplace(std::pair<std::thread::id, Looper*>(std::this_thread::get_id(), looper));
    if (isUiLooper) {
        mainThread = std::this_thread::get_id();
    }
    looperMutex.unlock();
    looper->prePare();
}

void Looper::prePare() {
    printf("[%s] succeed, looper is in work now\n", __func__);
    /*
     * 由于 C++ 没有现成的 timer,所以这里就用 sleep 模拟每隔一段时间的轮询
     * 1600 ms,这里是为了后面展示才设慢的。
     */
    while (true) {
        std::this_thread::sleep_for(std::chrono::milliseconds(1600));
        dispatchMessage();
    }
}

Looper* Looper::getMainLooper() {
    looperMutex.lock();
    auto looper = threadLooper.find(mainThread)->second;
    looperMutex.unlock();
    return looper;
}

Looper* Looper::getLooper() {
    looperMutex.lock();
    auto looperPair = threadLooper.find(std::this_thread::get_id());
    if (looperPair == threadLooper.end()) {
        looperMutex.unlock();
        return nullptr;
    }
    looperMutex.unlock();
    return looperPair->second;
}
  1. prepareLooper() 函数提供了给当前线程创建 Looper 对象的接口,创建后存放到 unordered_map 中,并能保证一个线程最多只有一个 Looper 存在。此处有一个 isUiLooper 参数,用来区分是否为创建主线程 Looper。
  2. getMainLooper() 函数获取主线程的 Looper,getLooper() 获取当前线程的 Looper。
  3. prePare() 方法中采用了 sleep() 的方式来模拟每隔一段时间(1.6 秒) Looper 进行一次消息查询,这里本应该是创建一个 timer,在 timer 里周期性的查询,所以只是为了简单才使用了 sleep()。后续有机会的话,我们会讨论怎么自己实现一个 timer。

2.4 总的设计

添加图片注释,不超过 140 字(可选)

从上图看,这三个类成了相互持有的关系,那我们在每个类析构的时候应该做些什么呢?

  1. 当 Message 类对象析构时,它虽然持有了 Handler 类对象的指针,但 Handler 对象是否释放,是由使用者来决定的,不能因为消息没了,就要把发送消息的人也干掉,那不科学。那是否应该通知持有它的 Looper 呢?也不用,其实,我们使用了消息池来管理消息,压根不用考虑消息的释放问题,每次都是改变 free 这个标识,消息的真正释放是在整个系统结束时,此时 Looper 和 Handler 会先一步释放。
  2. 当 Handler 类对象析构时,要考虑到 Handler 是被 Message 类持有的,如果 Handler 对象已经释放了,但是消息队列中还存放有需要该对象去处理的消息,那么当调用到该消息的 Handler 对象时,就会发生地址错误。所以,当 Handler 对象释放时,需要通知到它持有的 Looper 对象,对应的 Looper 对象就会去检查它的消息队列,将含有要释放的 Handler 的消息全部标记为 free,也就是已经被释放。
  3. 当 Looper 对象析构时,它要清空它的消息队列。不仅如此,它还要通知到它通过消息队列间接持有的 Handler 对象,它要释放了,以后就别发消息给它了。Handler 收到后,会将其的 mLooper 指针置为空,后续可能会选择紧跟着析构。

上面所考虑的情形,我并没有在代码中去实现,不然篇幅有点过长,重点就不突出了,毕竟我们主要讲的是跨线程消息通信,有兴趣的朋友可以试一下。

3、跨线程消息组件的测试

#include "Handler.h"
#include "MyHandler.h"
#include <iostream>
#include <thread>
#include <chrono>

void uiThread() {
    std::cout<<"ui thread:"<<std::this_thread::get_id()<<std::endl;
    Looper::prepareLooper(true);
}

void taskThread() {
    std::cout<<"task thread:"<<std::this_thread::get_id()<<std::endl;
    Handler* handler = new MyHandler(Looper::getMainLooper());
    while (true) {
        static int id = 1;
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
        handler->sendMessage(id, nullptr);
        id++;
    }
}

int main() {
    printf("messageque test\n");
    std::thread ui(uiThread);
    std::this_thread::sleep_for(std::chrono::milliseconds(5000));
    std::thread task(taskThread);
    ui.join();
    task.join();
}

我们在 main() 方法里先创建了 ui 线程,UI 线程启动后会初始化 Looper,准备接收来自其他线程的消息;

然后在 5 秒之后,创建了一个 task 线程,在该线程里会创建一个和 ui 线程绑定的 Handler,每隔 50 ms 向 ui 线程发送一次消息。

我们看一下跑的结果。

添加图片注释,不超过 140 字(可选)

发现确实做到了在 task 线程发送消息,而在 ui 线程中处理消息。当然上述测试只用了一个 task 线程,也可以用多个 task 线程向主线程发送消息进行测试。

4、总结

本文实现了一个跨线程通信的消息组件,设计目的是在子线程中更像 ui 界面,该组件主要由发送者(Handler)、接收者兼消息循环者(Looper)、消息(Message)三部分组成。Handler 负责在子线程中发送消息,并且知道怎么具体处理某个消息;Looper 会定时检查消息队列中的消息,并把该消息交给对应的 Handler 来处理;Message 就是信息的载体。

本文已经同步至个人公众号 难念的码,欢迎关注交流。