徐州IT兼职网站开发,中国做陶壶的网站有哪些,简单网站搭建,主题 外贸网站 模板下载C加入了协程 coroutine的特性#xff0c;一直没有动手实现过。看了网上很多文章#xff0c;已经了解了协程作为“可被中断和恢复的函数”的一系列特点。在学习过程中#xff0c;我发现大多数网上的例子#xff0c;要不就是在main()函数的控制台程序里演示yeild,await, resu…C加入了协程 coroutine的特性一直没有动手实现过。看了网上很多文章已经了解了协程作为“可被中断和恢复的函数”的一系列特点。在学习过程中我发现大多数网上的例子要不就是在main()函数的控制台程序里演示yeild,await, resume的特性要不就是讲述很多概念很少有演示协程究竟如何把异步变成同步调用的。本次我们就通过一个简单的计算文件哈希值的例子来演示如何进行协程操作。
1. 原始的哈希值计算
假设存在一个最简单的哈希计算需求要计算一个大文件的指纹。我们很容易实现一个演示算法
void DlgCT::on_pushButton_normal_clicked(){QFile fp(filename);char buf[1024];unsigned long long hashfile 0;if (fp.open(QIODevice::ReadOnly)){int rlen fp.read(buf,1024);while (rlen0){for (int i0;irlen;i){unsigned char c hashfile56;hashfile 8;hashfile ^ (buf[i] ^ c );}rlen fp.read(buf,1024);//假多线程也可以看做是Qt的有栈协程人为释放资源QCoreApplication::processEvents();}}
}上面的代码在文件比较大时如果没有‘’QCoreApplication::processEvents();”显然会阻塞界面导致按钮弹不起来界面卡死。当然可以通过适时调用QCoreApplication::processEvents();保持消息循环。这是一种假多线程也可以看做是Qt的有栈协程人为释放资源让给其他消息。
2. 异步计算改造
为了不阻塞主界面传统上喜欢使用另一个线程来处理算法并在完成后通知主线程。有一个处理类
class fileDealer : public QObject
{Q_OBJECT
public:explicit fileDealer(QObject *parent nullptr);//dealFile 计算哈希存储在 result 里void dealFile(QString filename);
public:QByteArray result;
private:std::thread * m_pThread nullptr;
signals:void sig_done();
};void fileDealer::dealFile(QString filename)
{m_pThread new std::thread([filename,this]()-void{ QFile fp(filename);char buf[1024];unsigned long long hashfile 0;if (fp.open(QIODevice::ReadOnly)){ int rlen fp.read(buf,1024);while (rlen0){for (int i0;irlen;i){unsigned char c hashfile56;hashfile 8;hashfile ^ (buf[i] ^ c );} rlen fp.read(buf,1024);}}emit sig_done(); });
}这个类会开启一个独立的线程做完后触发信号sig_done。上述代码是主干功能相应的new,delete维护部分略去。如此一来则需要在按钮响应函数里改造异步调用
void DlgCT::on_pushButton_thread_clicked()
{fileDealer * dealer new fileDealer(this);connect(dealer,fileDealer::sig_done,[dealer,this]()-void{dealer-deleteLater();});dealer-dealFile(ui-lineEdit_file-text());
}
即可完成非阻塞处理。
3. 使用协程 co_await 同步风格编程
如果使用C协程当然希望直接可以实现同步风格的异步调用
void DlgCT::on_pushButton_file_clicked()
{dealFile(ui-lineEdit_file-text());
}
FileTask DlgCT::dealFile(QString filename)
{QByteArray res co_await awDealFile(filename);//注意若协程库开发不周到此时有可能已经不是在主界面线程了一定注意操作界面控件的线程安全性。showMsg(res);
}在 co_await 语句后返回主消息循环此时定时器等依旧顺利工作。直到文件计算完毕后才返回 showMsg(res);。为了达到上述效果需要如下两步骤
3.1 添加协程代码
首先添加协程返回对象结构体. 本示例只使用 co_await关键词所以大部分的必备函数入口都是默认值啥也不做。
/*!* \brief The FileTask class 协程结构体*/
struct FileTask
{struct promise_type;using handle_type std::coroutine_handlepromise_type;FileTask(handle_type h){}FileTask(FileTask s){}struct promise_type {promise_type() default;~promise_type() default;auto get_return_object() noexcept {return FileTask{handle_type::from_promise(*this)};}auto initial_suspend() noexcept {//一创建立刻执行return std::suspend_never{};}auto final_suspend() noexcept {return std::suspend_always{};}void unhandled_exception() {exit(1);}void return_void(){}};};3.2 创建 await 辅助类
关键实现await功能的就是下面这个类
/*!* \brief The awDealFile class 协程 await 对象*/
class awDealFile : public QObject
{Q_OBJECT
public:awDealFile(QString filename, QObject *parent nullptr):QObject(parent),m_fn(filename),m_pDealer(new fileDealer){//处理完毕的信号会在处理线程里发出所以用QueuedConnection确保协程返回时保持线程不变。QObject::connect(m_pDealer,fileDealer::sig_done,this, awDealFile::slot_done,Qt::QueuedConnection);}~awDealFile(){if (m_pDealer)m_pDealer-deleteLater();m_pDealer nullptr;}bool await_ready() { return false; }/*!* \brief await_resume 这个函数的返回值决定了 await 关键词可以返回什么类型的东西* \return 哈希结果*/QByteArray await_resume() {return m_pDealer-result;}/*!* \brief await_suspend co_await 时会调用这个函数。此时启动处理并在处理完毕后resume* \param h*/void await_suspend(FileTask::handle_type h) {hd h;//处理m_pDealer-dealFile(m_fn);}
private slots:void slot_done(){if (hd) hd.resume();}
private:QString m_fn;fileDealer * m_pDealer nullptr;FileTask::handle_type hd;
};
有了上述代码则可实现同步调用。
4. 关于线程切换的风险
协程的co_await 实际上提供了一个无栈的暂停-恢复框架。关键是要在确保处理完毕后及时调用 resume 恢复执行。值得注意的是对于从一个 std::thread内直接 resume的方法会导致线程切换此行为务必引起重视。在哪个线程调用的resume协程函数恢复后就回到哪个线程。这对操作GUI控件的代码带来了隐晦的风险
可以看到在例子里使用Qt的跨线程队列槽 (Qt::QueuedConnection)确保恢复后的协程执行序依旧位于主线程。虽然在实验中多线程操作控件似乎也没有报错但这不是推荐的控件操作方法。 //处理完毕的信号会在处理线程里发出所以用QueuedConnection确保协程返回时保持线程不变。QObject::connect(m_pDealer,fileDealer::sig_done,this, awDealFile::slot_done,Qt::QueuedConnection);void slot_done(){if (hd) hd.resume();}5. 范例代码
范例代码参考
https://gitcode.net/coloreaglestdio/qtcpp_demo/-/tree/master/qt_coro_test
在 MSYS2 Qt6 /Linux下编译通过。
6. 体会-协程用的香协程库开发一点也不简单
上述把一个异步操作变成同步其实就是一个语法糖背后还是多线程。如果一下处理1000个文件开启1000个线程是不合理的需要管理一个线程池并管理请求队列保证机械硬盘在一个合理的并发规模下运转。
推而广之协程能够发挥co_await的功效仰赖于协程库背后的管理机制如系统层面的异步回调如socket、库层面的线程池。一个简单的 co_await背后的代码量不容小觑。
比较全面的协程改造的例子参考这个基于Qt 的协程库 https://qcoro.dvratil.cz/可以看见为了这一句“co_await”库开发者要做的工作。
此外作为使用者要搞清楚语法糖背后创建了哪些对象生命周期如何前后线程是不是一致才能不踩坑。越是表面看起来无比清晰的代码踩坑越是惊心动魄。所以如果是基于Qt这样的成熟框架有Lambda槽回调大可不必在生产环境激进地尝试协程。