C++实现简化 QtBase(5):通过IEventLoopHost扩展实现win32消息循环

在上一篇文章《C++实现简化版Qt的QObject(4):增加简单实用的事件机制》中,我们实现了普通线程的事件机制。

但是事件机制往往需要和操作系统主线程消息循环一起工作。

因此,今天,我们在之前的CEventLoop的实现之上,通过IEventLoopHost扩展,实现一个windows系统的主线程消息循环扩展,使得我们可以在主线程post task。

CEventLoop 代码

首先,昨天我们写的CEventLoop代码如下:

cpp 复制代码
	class CEventLoop {
	public:
		using Clock = std::chrono::steady_clock;
		using TimePoint = Clock::time_point;
		using Duration = Clock::duration;
		using Handler = std::function<void()>;
		struct TimedHandler {
			TimePoint time;
			Handler handler;
			bool operator<(const TimedHandler& other) const {
				return time > other.time;
			}
		};
		class IEventLoopHost {
		public:
			virtual void onPostTask() = 0;
			virtual void onWaitForTask(std::condition_variable& cond, std::unique_lock<std::mutex>& locker) = 0;
			virtual void onEvent(TimedHandler& event) = 0;
			virtual void onWaitForRun(std::condition_variable& cond, std::unique_lock<std::mutex>& locker, const TimePoint& timePoint) = 0;
		};
	private:
		IEventLoopHost* host = nullptr;
		std::priority_queue<TimedHandler> tasks_;
		std::mutex mutex_;
		std::condition_variable cond_;
		std::atomic<bool> running_{ true };

	public:
		void setHost(IEventLoopHost* host) {
			this->host = host;
		}
		void post(Handler handler, Duration delay = Duration::zero()) {
			std::unique_lock<std::mutex> lock(mutex_);
			tasks_.push({ Clock::now() + delay, std::move(handler) });
			cond_.notify_one();
			if (host) {
				host->onPostTask();
			}
		}

		void run() {
			while (running_) {
				std::unique_lock<std::mutex> lock(mutex_);
				if (tasks_.empty()) {
					if (host) {
						host->onWaitForTask(cond_, lock);
					}
					else {
						cond_.wait(lock, [this] { return !tasks_.empty() || !running_; });
					}
				}

				while (!tasks_.empty() && tasks_.top().time <= Clock::now()) {
					auto task = tasks_.top();
					tasks_.pop();
					lock.unlock();
					if (host) {
						host->onEvent(task);
					}
					else {
						task.handler();
					}

					lock.lock();
				}

				if (!tasks_.empty()) {
					if (host) {
						host->onWaitForRun(cond_, lock, tasks_.top().time);
					}
					else {
						cond_.wait_until(lock, tasks_.top().time);
					}
				}
			}
		}

		void stop() {
			running_ = false;
			cond_.notify_all();
		}

	};

实现扩展

在Windows API中,消息循环通常涉及到调用GetMessageTranslateMessageDispatchMessage函数。以下是一个IEventLoopHost的实现,它将Windows消息循环集成到CEventLoop类中:

cpp 复制代码
#include <Windows.h>

class WindowsEventLoopHost : public CEventLoop::IEventLoopHost {
public:
    // 当任务被投递到事件循环时调用
    void onPostTask() override {
        // 可以使用Windows的PostMessage函数发送一个自定义的消息来唤醒消息循环
        PostThreadMessage(GetCurrentThreadId(), WM_USER, 0, 0);
    }

    // 当事件循环需要等待任务时调用
    void onWaitForTask(std::condition_variable& cond, std::unique_lock<std::mutex>& locker) override {
        locker.unlock();
        MSG msg;
        // 使用PeekMessage而不是GetMessage来避免阻塞,以便定时器事件可以被处理
        while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        locker.lock();
    }

    // 当事件循环处理事件时调用
    void onEvent(CEventLoop::TimedHandler& event) override {
        // 这里简单地调用事件处理器
        event.handler();
    }

    // 当事件循环需要等待直到特定时间点运行时调用
    void onWaitForRun(std::condition_variable& cond, std::unique_lock<std::mutex>& locker, const CEventLoop::TimePoint& timePoint) override {
        locker.unlock();
        // 计算需要等待的时间
        auto now = CEventLoop::Clock::now();
        auto delay_ms = std::chrono::duration_cast<std::chrono::milliseconds>(timePoint - now).count();
        if (delay_ms > 0) {
            // 设置Windows计时器
            SetTimer(NULL, 0, (UINT)delay_ms, NULL);
            MSG msg;
            // 等待计时器或其他消息
            while (GetMessage(&msg, NULL, 0, 0)) {
                if (msg.message == WM_TIMER) {
                    break; // 计时器消息,继续事件循环
                }
                TranslateMessage(&msg);
                DispatchMessage(&msg);
            }
            KillTimer(NULL, 0);
        }
        locker.lock();
    }
};

在这个实现中,我们通过IEventLoopHost的四个虚函数,完成与Windows消息循环协同工作:

  • onPostTask 使用 PostThreadMessage 来发送一个自定义的消息,以唤醒可能正在等待的消息循环。
  • onWaitForTask 使用 PeekMessage 来处理消息而不阻塞,这样定时器消息可以在适当的时间被处理。
  • onEvent 简单地调用传入的事件处理器,这里我们没有对Windows消息做特别的处理。
  • onWaitForRun 使用 SetTimer 来创建一个Windows计时器,它会在指定的毫秒数后发送一个 WM_TIMER 消息。我们使用 GetMessage 等待这个消息或者其他消息的到来。如果计时器消息到来,我们就终止循环,返回到事件循环,以便继续处理其他任务。

继续优化

在自测过程中进一步的优化代码结构,优化性能,处理WM_QUIT消息,处理一些其他细节等。

最终代码如下:

cpp 复制代码
#include <Windows.h>
class CWindowsEventLoopHost : public base::CEventLoop::IEventLoopHost {
	static constexpr UINT WM_WAKEUP = WM_USER + 15151515;
	UINT_PTR timerID_ = 0;

public:
	~CWindowsEventLoopHost() {
		if (timerID_) {
			KillTimer(NULL, timerID_);
		}
	}

	void onPostTask() override {
		PostThreadMessage(GetCurrentThreadId(), WM_WAKEUP, 0, 0);
	}

	void onWaitForTask(std::condition_variable& cond, std::unique_lock<std::mutex>& locker) override {
		MSG msg;
		while (GetMessage(&msg, NULL, 0, 0)) {
			if (handleWindowsMessage(msg)) {
				break;
			}
			if (cond.wait_for(locker, std::chrono::milliseconds(0), [] { return true; })) {
				break;
			}
		}
	}

	void onEvent(base::CEventLoop::TimedHandler& event) override {
		event.handler();
	}

	void onWaitForRun(std::condition_variable& cond, std::unique_lock<std::mutex>& locker, const std::chrono::steady_clock::time_point& timePoint) override {
		auto now = std::chrono::steady_clock::now();
		if (now < timePoint) {
			auto delay_ms = std::chrono::duration_cast<std::chrono::milliseconds>(timePoint - now).count();
			timerID_ = SetTimer(NULL, 0, (UINT)delay_ms, NULL);//通过timer事件唤醒
			MSG msg;
			while (GetMessage(&msg, NULL, 0, 0)) {
				if (handleWindowsMessage(msg)) {
					break;
				}
			}
		}
	}

	bool handleWindowsMessage(MSG& msg) {
		if (msg.message == WM_QUIT) {
			if (this->eventLoop) {
				this->eventLoop->stop();//退出消息循环
			}
			return true;
		}
		if (msg.message == WM_TIMER && msg.wParam == timerID_) {
			KillTimer(NULL, timerID_);
			timerID_ = 0;
			return true; // Timer event occurred
		}
		TranslateMessage(&msg);
		DispatchMessage(&msg);
		return false;
	}
};

使用示例

由于setHost函数里面没有加锁,而且可能出现在运行过程中被修改host出现不可预期情况。因此,后来去掉setHost函数,改为在CEventLoop的构造函数传入host,避免误用。

使用示例如下:

cpp 复制代码
// 使用示例
int main() {
	CWindowsEventLoopHost host;
	base::CEventLoop loop(&host);

	loop.post([]() {
		std::cout << "Immediate task\n";
		});//马上执行

	loop.post([]() {
		std::cout << "Delayed task\n";
		}, std::chrono::seconds(1));//延时一秒

	loop.post([]() {
		std::cout << "Delayed task in 3 second\n";
		}, std::chrono::seconds(3));//延时3秒

	loop.post([&]() {
		std::cout << "stop msg loop in 6 second\n";
		loop.stop();
		}, std::chrono::seconds(6));//延时6秒

	if (true) {
		loop.run();//主线程直接run
	}
	else {
		// 或者起一个其他线程run:
		std::thread loop_thread([&loop]() {
			loop.run();
			});
		std::this_thread::sleep_for(std::chrono::seconds(10));
		loop.stop();//停止消息循环
		loop_thread.join();
	}
}

完整代码在:https://github.com/kevinyangli/simple_qt_qobject.git

相关推荐
skyshandianxia2 分钟前
Java面试八股之MySQL中binlog的工作模式有哪些
java·开发语言·面试
Chris-zz5 分钟前
C++:继承
开发语言·c++·算法·学习方法
Narutolxy18 分钟前
如何高效管理和迁移Python开发环境:从Conda到纯Python的转换指南
开发语言·python·conda
kaixin_learn_qt_ing18 分钟前
解析Xml文件并修改QDomDocument的值
xml·数据库·qt
川爻26 分钟前
String类(STL开始)
开发语言·c++
萌狼蓝天27 分钟前
[python]Markdown图片引用格式批处理桌面应用程序
开发语言·python
懒大王爱吃狼33 分钟前
【Python自动化】Python实现微信自动回复(简单上手),60行代码轻松搞定!个性化自动回复机器人,零基础可学!
运维·开发语言·python·数据分析·自动化·学习方法
科学的发展-只不过是读大自然写的代码42 分钟前
Qt 日志输出的选择方案有多少
开发语言·qt
小白学大数据1 小时前
爬虫进阶:Selenium与Ajax的无缝集成
大数据·开发语言·爬虫·selenium·ajax
取加若则_1 小时前
C++入门(C语言过渡)
c语言·开发语言·数据结构·c++·算法