Qt源码阅读(四) 事件循环
事件系统
文章为本人理解,如有理解不到位之处,烦请各位指正。
文章目录
- 事件系统
- 什么是事件循环?
- 事件是如何产生的?
- sendEvent
- postEvent
- 事件是如何处理的?
- 事件循环是怎么遍历的?
- 事件过滤器
- event
- 夹带私货时间
Qt的事件循环,应该是所有Qter都避不开的一个点,所以,这篇博客,咱们来了解源码中一些关于Qt中事件循环的部分。
先抛出几个疑问,根据源代码,下面一一进行解析。
- 事件循环是什么?
- 事件是怎么产生的?
- 事件是如何处理的?
什么是事件循环?
对于Qt事件循环个人理解是,事件循环是一个队列去循环处理事件。当队列中有事件时,则去处理事件,如果没有事件时,则会阻塞等待。
事件是如何产生的?
事件的产生可以分为两种:
- 程序外部产生
- 程序内部产生
程序外部所产生的事件主要是指系统产生的事件,比如说鼠标按下(MouseButtonPress)、按键按下(KeyPress)等,Qt捕捉系统的事件,然后将系统事件封装成自己的QEvent类,再将事件发送出去。
程序内部产生的事件主要指我们在代码里,手动创建一个事件,然后将事件通过sendEvent/postEvent,来发送到事件循环中。而sendEvent和postEvent区别又在于一个是阻塞的(sendEvent)一个是非阻塞的(postEvent)。
我们结合源码分析,看一下sendEvent和postEvent分别干了什么导致一个是阻塞的一个是非阻塞的。
sendEvent
完整源码如下:
bool QCoreApplication::sendEvent(QObject *receiver, QEvent *event)
{// sendEvent是阻塞调用Q_TRACE(QCoreApplication_sendEvent, receiver, event, event->type());if (event)event->spont = false;return notifyInternal2(receiver, event);
}
可以看到,sendEvent是调用了notifyInternal2这个函数
bool QCoreApplication::notifyInternal2(QObject *receiver, QEvent *event)
{...// Qt enforces the rule that events can only be sent to objects in// the current thread, so receiver->d_func()->threadData is// equivalent to QThreadData::current(), just without the function// call overhead.// 事件只能在同一个线程被sendQObjectPrivate *d = receiver->d_func();QThreadData *threadData = d->threadData;QScopedScopeLevelCounter scopeLevelCounter(threadData);if (!selfRequired)return doNotify(receiver, event);return self->notify(receiver, event);
}
进一步跟踪到其doNotify函数
static bool doNotify(QObject *receiver, QEvent *event)
{if (receiver == nullptr) { // serious errorqWarning("QCoreApplication::notify: Unexpected null receiver");return true;}#ifndef QT_NO_DEBUG// 检查接受线程与当前是否同线程QCoreApplicationPrivate::checkReceiverThread(receiver);
#endif// QWidget类必须用QApplicationreturn receiver->isWidgetType() ? false : QCoreApplicationPrivate::notify_helper(receiver, event);
}
再到QCoreApplicationPrivate::notify_helper
bool QCoreApplicationPrivate::notify_helper(QObject *receiver, QEvent * event)
{// Note: when adjusting the tracepoints in here// consider adjusting QApplicationPrivate::notify_helper too.Q_TRACE(QCoreApplication_notify_entry, receiver, event, event->type());bool consumed = false;bool filtered = false;Q_TRACE_EXIT(QCoreApplication_notify_exit, consumed, filtered);// send to all application event filters (only does anything in the main thread)if (QCoreApplication::self&& receiver->d_func()->threadData.loadRelaxed()->thread.loadAcquire() == mainThread()&& QCoreApplication::self->d_func()->sendThroughApplicationEventFilters(receiver, event)) {filtered = true;return filtered;}// send to all receiver event filtersif (sendThroughObjectEventFilters(receiver, event)) {filtered = true;return filtered;}// deliver the event// 直接调用对象的event函数,所以是阻塞的consumed = receiver->event(event);return consumed;
}
然后我们可以看到主要有几个流程:
-
判断QCoreApplication有没有安装事件过滤器,有就把信号发送到事件过滤器里,由事件过滤器对事件进行处理。
// send to all application event filters (only does anything in the main thread) if (QCoreApplication::self&& receiver->d_func()->threadData.loadRelaxed()->thread.loadAcquire() == mainThread()&& QCoreApplication::self->d_func()->sendThroughApplicationEventFilters(receiver, event)) {filtered = true;return filtered; } -
判断事件接受对象,有没有安装事件过滤器,有就将信号发送到事件过滤器。
// send to all receiver event filters if (sendThroughObjectEventFilters(receiver, event)) {filtered = true;return filtered; }具体遍历事件接受对象所安装的事件过滤器的代码如下:
bool QCoreApplicationPrivate::sendThroughObjectEventFilters(QObject *receiver, QEvent *event) {if (receiver != QCoreApplication::instance() && receiver->d_func()->extraData) {for (int i = 0; i < receiver->d_func()->extraData->eventFilters.size(); ++i) {QObject *obj = receiver->d_func()->extraData->eventFilters.at(i);if (!obj)continue;if (obj->d_func()->threadData != receiver->d_func()->threadData) {qWarning("QCoreApplication: Object event filter cannot be in a different thread.");continue;}if (obj->eventFilter(receiver, event))return true;}}return false; }我们可以看到,只要事件被一个事件过滤器所成功处理,那么后续的事件过滤器就不会被响应。同时,参看Qt帮助手册中有提及到:
If multiple event filters are installed on a single object, the filter that was installed last is activated first.
后插入的事件过滤器会被优先响应。 具体安装事件过滤器,我们在后面进行分析。
-
直接调用事件接受对象的
event函数进行处理。因为是直接调用的对象的event,所以说,sendEvent函数会阻塞等待。// deliver the event// 直接调用对象的event函数,所以是阻塞的consumed = receiver->event(event);return consumed
postEvent
完整代码如下:
void QCoreApplication::postEvent(QObject *receiver, QEvent *event, int priority)
{Q_TRACE_SCOPE(QCoreApplication_postEvent, receiver, event, event->type());// 事件的接收者不能为空if (receiver == nullptr) {qWarning("QCoreApplication::postEvent: Unexpected null receiver");delete event;return;}// 对事件接受对象所在线程的事件处理列表上锁auto locker = QCoreApplicationPrivate::lockThreadPostEventList(receiver);if (!locker.threadData) {// posting during destruction? just delete the event to prevent a leakdelete event;return;}QThreadData *data = locker.threadData;// if this is one of the compressible events, do compression// 将重复的事件,进行压缩if (receiver->d_func()->postedEvents&& self && self->compressEvent(event, receiver, &data->postEventList)) {Q_TRACE(QCoreApplication_postEvent_event_compressed, receiver, event);return;}if (event->type() == QEvent::DeferredDelete)receiver->d_ptr->deleteLaterCalled = true;if (event->type() == QEvent::DeferredDelete && data == QThreadData::current()) {// remember the current running eventloop for DeferredDelete// events posted in the receiver's thread.// Events sent by non-Qt event handlers (such as glib) may not// have the scopeLevel set correctly. The scope level makes sure that// code like this:// foo->deleteLater();// qApp->processEvents(); // without passing QEvent::DeferredDelete// will not cause "foo" to be deleted before returning to the event loop.// If the scope level is 0 while loopLevel != 0, we are called from a// non-conformant code path, and our best guess is that the scope level// should be 1. (Loop level 0 is special: it means that no event loops// are running.)int loopLevel = data->loopLevel;int scopeLevel = data->scopeLevel;if (scopeLevel == 0 && loopLevel != 0)scopeLevel = 1;static_cast<QDeferredDeleteEvent *>(event)->level = loopLevel + scopeLevel;}// delete the event on exceptions to protect against memory leaks till the event is// properly owned in the postEventListQScopedPointer<QEvent> eventDeleter(event);Q_TRACE(QCoreApplication_postEvent_event_posted, receiver, event, event->type());data->postEventList.addEvent(QPostEvent(receiver, event, priority));eventDeleter.take();event->posted = true;++receiver->d_func()->postedEvents;data->canWait = false;locker.unlock();QAbstractEventDispatcher* dispatcher = data->eventDispatcher.loadAcquire();if (dispatcher)dispatcher->wakeUp();
}
-
判断事件接收对象是否为空
// 事件的接收者不能为空 if (receiver == nullptr) {qWarning("QCoreApplication::postEvent: Unexpected null receiver");delete event;return; } -
将事件接收对象所在线程的post事件列表上锁,如果已经被锁了,就把事件删除掉,并返回,防止泄露。
// 对事件接受对象所在线程的事件处理列表上锁 auto locker = QCoreApplicationPrivate::lockThreadPostEventList(receiver); if (!locker.threadData) {// posting during destruction? just delete the event to prevent a leakdelete event;return; } -
将一些可以压缩的事件进行压缩,及多个事件压缩成只推送最后的一个事件。Qt界面的
update就是这个操作,为了防止多次刷新导致卡顿,短时间内多次的调用update可能只会刷新一次// if this is one of the compressible events, do compression // 将重复的事件,进行压缩 if (receiver->d_func()->postedEvents&& self && self->compressEvent(event, receiver, &data->postEventList)) {Q_TRACE(QCoreApplication_postEvent_event_compressed, receiver, event);return; } -
将事件插入接收对象所在线程的post事件列表中,并唤醒线程的事件调度器,来进行事件的处理。所以
postEvent是非阻塞的,因为其只是把事件插入了线程的事件列表,唤醒事件调度器之后便返回。// delete the event on exceptions to protect against memory leaks till the event is// properly owned in the postEventListQScopedPointer<QEvent> eventDeleter(event);Q_TRACE(QCoreApplication_postEvent_event_posted, receiver, event, event->type());data->postEventList.addEvent(QPostEvent(receiver, event, priority));eventDeleter.take();event->posted = true;++receiver->d_func()->postedEvents;data->canWait = false;locker.unlock();QAbstractEventDispatcher* dispatcher = data->eventDispatcher.loadAcquire();if (dispatcher)dispatcher->wakeUp();
事件是如何处理的?
在Qt中,事件的接收者都是QObject,而QObject中事件处理是调用event函数。如果当时对象不处理某个事件,就会将其转发到父类的event进行处理。
而事件的处理,主要分为三个部分:
- 先是由事件循环遍历事件
- 然后判断事件接受对象有没有安装事件过滤器(
installEventFilter),有安装的话,就把事件丢给事件过滤器(eventFilter)进行处理。 - 如果没有安装事件过滤器或者事件过滤器对该事件不进行处理的话,那么,事件将会进一步转发到
event函数里进行处理。
所以,在这一章节,我们同样一步一步的分析这三个点。
事件循环是怎么遍历的?
int main(int argc, char *argv[])
{QApplication a(argc, argv);MainWindow w;w.show();return a.exec();
}
上面是一个经典的QtGUI程序的main函数,调用a.exec()
int QCoreApplication::exec()
{...threadData->quitNow = false;QEventLoop eventLoop;self->d_func()->in_exec = true;self->d_func()->aboutToQuitEmitted = false;int returnCode = eventLoop.exec();...
}
而看QApplication::exec的源码,实际上就是开启了一个事件循环(QEventLoop)。同样,我们去看QEventLoop::exec的源码,进一步看处理事件的步骤是什么。
int QEventLoop::exec(ProcessEventsFlags flags)
{...while (!d->exit.loadAcquire())processEvents(flags | WaitForMoreEvents | EventLoopExec);ref.exceptionCaught = false;return d->returnCode.loadRelaxed();
}
上面可以看到,QEvenLoop::exec里,是一个while循环,循环的去调用processEvent,而且设置了WaitForMoreEvents就是说,如果没有事件,就阻塞等待。
void QCoreApplication::processEvents(QEventLoop::ProcessEventsFlags flags, int ms)
{// ### Qt 6: consider splitting this method into a public and a private// one, so that a user-invoked processEvents can be detected// and handled properly.QThreadData *data = QThreadData::current();if (!data->hasEventDispatcher())return;QElapsedTimer start;start.start();while (data->eventDispatcher.loadRelaxed()->processEvents(flags & ~QEventLoop::WaitForMoreEvents)) {if (start.elapsed() > ms)break;}
}
阅读processEvent,其调用了线程的事件调度器QAbstrctEventDispatcher,而这个类是一个抽象基类,根据不同的平台,有不同的实现,我们以windows下(QEventDispatcherWin32)的为例,接着分析事件处理的流程。
bool QEventDispatcherWin32::processEvents(QEventLoop::ProcessEventsFlags flags)
{Q_D(QEventDispatcherWin32);...// To prevent livelocks, send posted events once per iteration.// QCoreApplication::sendPostedEvents() takes care about recursions.sendPostedEvents();...
}void QEventDispatcherWin32::sendPostedEvents()
{Q_D(QEventDispatcherWin32);if (d->sendPostedEventsTimerId != 0)KillTimer(d->internalHwnd, d->sendPostedEventsTimerId);d->sendPostedEventsTimerId = 0;// Allow posting WM_QT_SENDPOSTEDEVENTS message.d->wakeUps.storeRelaxed(0);QCoreApplicationPrivate::sendPostedEvents(0, 0, d->threadData.loadRelaxed());
}
可以看到,事件调度器最终还是调用了QCoreApplication的sendPostEvents
void QCoreApplicationPrivate::sendPostedEvents(QObject *receiver, int event_type,QThreadData *data)
{if (event_type == -1) {// we were called by an obsolete event dispatcher.event_type = 0;}if (receiver && receiver->d_func()->threadData != data) {qWarning("QCoreApplication::sendPostedEvents: Cannot send ""posted events for objects in another thread");return;}...// Exception-safe cleaning up without the need for a try/catch blockstruct CleanUp {QObject *receiver;int event_type;QThreadData *data;bool exceptionCaught;inline CleanUp(QObject *receiver, int event_type, QThreadData *data) :receiver(receiver), event_type(event_type), data(data), exceptionCaught(true){}inline ~CleanUp(){if (exceptionCaught) {// since we were interrupted, we need another pass to make sure we clean everything updata->canWait = false;}--data->postEventList.recursion;if (!data->postEventList.recursion && !data->canWait && data->hasEventDispatcher())data->eventDispatcher.loadRelaxed()->wakeUp();// clear the global list, i.e. remove everything that was// delivered.if (!event_type && !receiver && data->postEventList.startOffset >= 0) {const QPostEventList::iterator it = data->postEventList.begin();data->postEventList.erase(it, it + data->postEventList.startOffset);data->postEventList.insertionOffset -= data->postEventList.startOffset;Q_ASSERT(data->postEventList.insertionOffset >= 0);data->postEventList.startOffset = 0;}}};CleanUp cleanup(receiver, event_type, data);while (i < data->postEventList.size()) {...// first, we diddle the event so that we can deliver// it, and that no one will try to touch it later.pe.event->posted = false;QEvent *e = pe.event;QObject * r = pe.receiver;--r->d_func()->postedEvents;Q_ASSERT(r->d_func()->postedEvents >= 0);// next, update the data structure so that we're ready// for the next event.const_cast<QPostEvent &>(pe).event = nullptr;locker.unlock();const auto relocker = qScopeGuard([&locker] { locker.lock(); });QScopedPointer<QEvent> event_deleter(e); // will delete the event (with the mutex unlocked)// after all that work, it's time to deliver the event.QCoreApplication::sendEvent(r, e);// careful when adding anything below this point - the// sendEvent() call might invalidate any invariants this// function depends on.}cleanup.exceptionCaught = false;
}
我们一个一个的分块分析:
-
判断是否在一个线程
if (receiver && receiver->d_func()->threadData != data) {qWarning("QCoreApplication::sendPostedEvents: Cannot send ""posted events for objects in another thread");return; } -
一个有意思的异常安全的处理,不需要try/catch块
// Exception-safe cleaning up without the need for a try/catch block struct CleanUp {QObject *receiver;int event_type;QThreadData *data;bool exceptionCaught;inline CleanUp(QObject *receiver, int event_type, QThreadData *data) :receiver(receiver), event_type(event_type), data(data), exceptionCaught(true){}inline ~CleanUp(){if (exceptionCaught) {// since we were interrupted, we need another pass to make sure we clean everything updata->canWait = false;}--data->postEventList.recursion;if (!data->postEventList.recursion && !data->canWait && data->hasEventDispatcher())data->eventDispatcher.loadRelaxed()->wakeUp();// clear the global list, i.e. remove everything that was// delivered.if (!event_type && !receiver && data->postEventList.startOffset >= 0) {const QPostEventList::iterator it = data->postEventList.begin();data->postEventList.erase(it, it + data->postEventList.startOffset);data->postEventList.insertionOffset -= data->postEventList.startOffset;Q_ASSERT(data->postEventList.insertionOffset >= 0);data->postEventList.startOffset = 0;}} }; CleanUp cleanup(receiver, event_type, data);
定义了一个结构体CleanUp,结构体的析构函数(~CleanUp)保存了函数退出时需要执行的清理操作。然后在栈上创建了一个结构体对象,遍历事件列表时,异常退出,那么就会调用自动调用~CleanUp的析构函数。
-
将事件发送出去(
sendEvent)while (i < data->postEventList.size()) {...// first, we diddle the event so that we can deliver// it, and that no one will try to touch it later.pe.event->posted = false;QEvent *e = pe.event;QObject * r = pe.receiver;--r->d_func()->postedEvents;Q_ASSERT(r->d_func()->postedEvents >= 0);// next, update the data structure so that we're ready// for the next event.const_cast<QPostEvent &>(pe).event = nullptr;locker.unlock();const auto relocker = qScopeGuard([&locker] { locker.lock(); });QScopedPointer<QEvent> event_deleter(e); // will delete the event (with the mutex unlocked)// after all that work, it's time to deliver the event.QCoreApplication::sendEvent(r, e);// careful when adding anything below this point - the// sendEvent() call might invalidate any invariants this// function depends on.}
可以看到,核心还是调用sendEvent将事件发送出去,而前面我们对sendEvent的源码分析我们可以看到,事件先是经过事件过滤器,再经过对象的event函数,来进行事件的处理。所以就引出我们的下一个话题:事件过滤器
事件过滤器
在实际应用中,我们经常要将某一个窗口部件的某个事件如鼠标滑轮滚动拦截,然后执行我们自己想要的操作。这个时候,我们就可以用到事件过滤器(EventFilter**) **
首先,我们需要自己编写一个eventFilter函数,
bool Class::eventFilter(QObject* watcher, QEvent* event)
{//以过滤鼠标滚轮事件为例if (object == m_watcherObject && event->type() == QEvent::Wheel) {// do somethingreturn true; }QWidget::eventFilter(watcher, event);
}
然后,我们需要为要拦截的某个窗口部件,安装事件过滤器
void Class::initUI()
{QWidget* m_watcherObject = new QWidget(this);// 为对象安装一个事件过滤器m_watcherObject->installEventFilterr(this);
}initUI();
那么一个对象安装的多个事件过滤器,会以什么样的顺序触发呢?我们在前面的讲过,后安装的事件过滤器会先触发,这一点,我们可以在源码里得到佐证:
void QObject::installEventFilter(QObject *obj)
{Q_D(QObject);if (!obj)return;if (d->threadData != obj->d_func()->threadData) {qWarning("QObject::installEventFilter(): Cannot filter events for objects in a different thread.");return;}if (!d->extraData)d->extraData = new QObjectPrivate::ExtraData;// clean up unused items in the listd->extraData->eventFilters.removeAll((QObject*)nullptr);d->extraData->eventFilters.removeAll(obj);d->extraData->eventFilters.prepend(obj);
}
可以清楚的看到,事件过滤器,是以prepend的形式被添加进事件过滤器列表的。
那么,当有鼠标滚轮事件触发的时候,我们可以看到sendEvent会优先走到事件过滤器里,如果eventFilter返回一个true,那么事件就不会被继续派发,否则,将会将事件发送到其他的事件过滤器里进行处理,如果其他的事件过滤器均对该事件不进行处理,那么事件将会继续往下派发,走到事件的处理函数event
event
接下来,就到了事件处理的最后一站,event函数,这个函数比较简单,我们可以自己重写这个函数,对事件进行自定义的处理。
bool Class::event(QEvent *e)
{switch (e->type()) {case QEvent::Whell:// do somethingreturn true;default:if (e->type() >= QEvent::User) {customEvent(e);break;}return false;}return true;
}
夹带私货时间
- 之前有说到
processEvent,添加一个小经验。当我们有时候不得不在主线程循环执行很耗时的操作的时候,这个时候,界面就会刷新不过来,就会导致界面卡顿,影响使用。但是,我们可以在这个循环里,手动调用qApp->processEvent(),这样就可以手动调用处理掉所有的事件,就可以解决卡顿的问题。
相关文章:
Qt源码阅读(四) 事件循环
事件系统 文章为本人理解,如有理解不到位之处,烦请各位指正。 文章目录事件系统什么是事件循环?事件是如何产生的?sendEventpostEvent事件是如何处理的?事件循环是怎么遍历的?事件过滤器event夹带私货时间Q…...
银行数字化转型导师坚鹏:银行数字化领导力提升之道
银行数字化领导力提升之道 ——融合中西智慧,践行知行合一思想,实现知行果合一 课程背景: 很多银行存在以下问题:不知道如何领导数字员工?不清楚银行数字化领导力模型的内涵?不知道如何开展银行数字化…...
Vue2 -- 自定义单选内容的单选框组件
自定义单选内容的单选框组件 之前做的一个项目,在项目中有一个关于人员权限分配的功能,给人员指定各个模块的权限信息,分为 write 可写权限read 可读权限none 没有权限 项目要求画面中只显示 W R 两个按钮控制指定权限信息,都不…...
让PyTorch训练速度更快,你需要掌握这17种方法
掌握这 17 种方法,用最省力的方式,加速你的 Pytorch 深度学习训练。近日,Reddit 上一个帖子热度爆表。主题内容是关于怎样加速 PyTorch 训练。原文作者是来自苏黎世联邦理工学院的计算机科学硕士生 LORENZ KUHN,文章向我们介绍了在…...
LeetCode-309. 最佳买卖股票时机含冷冻期
目录题目思路动态规划题目来源 309. 最佳买卖股票时机含冷冻期 题目思路 每天最多只可能有三种状态中的一种 0表示当前处于买入状态(持有股票) 1表示当前处于卖出状态(不持有股票) 2表示当前处于冷冻状态 设dp[i][j]表示i - 1天状态为j时所拥有的最大现金 dp[i][0] Math.ma…...
AUTOSAR知识点Com(七):CANSM初认知
目录 1、概述 2、CanSM主要做什么 2.1、CAN控制器状态管理 2.2、CAN收发器状态管理 2.3、Busoff检测 1、概述 CANSM(Controller Area Network State Manager)是AUTOSAR(Automotive Open System Architecture)标准中的一个模块…...
递归:斐波那契数列、递归实现指数型枚举、递归实现排列型枚举
递归:O(2^n) 调用自己 例题及代码模板: 斐波那契数列 输入一个整数 n ,求斐波那契数列的第 n 项。 假定从 0 开始,第 0 项为 0。 数据范围 0≤n≤39 样例 输入整数 n5 返回 5 #include <iostream> #include <cstring&g…...
oracle模糊查询时字段内容包含下划线的解决办法
最近项目中遇到一个关于模糊查询问题。表tabA中的字段name的值有下划线的情况,在模糊查询时发现查询的记录不对。 表的结构 表名:tabA id name sex 1 test_601 1 2 test_602 2 3 test16 1 4 t…...
C++:explicit关键字
C中的explicit关键字只能用于修饰只有一个参数的类构造函数,它的作用是表明该构造函数是显示的,而非隐式的,跟它相对应的另一个关键字是implicit,意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式)。那么显示声…...
【C5】bmc wtd,post
文章目录1.bmc_wtd_cpld:syscpld.c中wd_en和wd_kick节点对应寄存器,crontab,FUNCNAME2.AST芯片WDT切换主备:BMC用WDT2作为主备切换的控制器2.1 AC后读取:bmc处于主primary flash(设完后:实际主&…...
200.Spark(七):SparkSQL项目实战
一、启动环境 需要启动mysql,hadoop,hive,spark。并且能让spark连接上hive(上一章有讲) #启动mysql,并登录,密码123456 sudo systemctl start mysqld mysql -uroot -p#启动hive cd /opt/module/ myhadoop.sh start#查看启动情况 jpsall#启动hive cd /opt/module/hive/…...
区块链系统:挖矿原理
在比特币的P2P网络中,有一类节点,它们时刻不停地进行计算,试图把新的交易打包成新的区块并附加到区块链上,这类节点就是矿工。因为每打包一个新的区块,打包该区块的矿工就可以获得一笔比特币作为奖励。所以,…...
【博弈】【清华冬令营2018模拟】取石子
写完敢说全网没有这么详细的题解了。 注意:题解长是为了方便理解,所以读起来速度应该很快。 题目描述 有 nnn 堆石子,第 iii 堆有 xix_ixi 个。 AliceAliceAlice 和 BobBobBob 轮流去石子(先后手未定), …...
嵌入式:BSP的理解
BSP概念总结BSP定义BSP的特点BSP的主要工作BSP在嵌入式系统和Windowsx系统中的不同BSP和PC机主板上的BIOS区别BSP与 HAL关系嵌入式计算机系统主要由 硬件层,中间层,系统软件层和应用软件层四层组成。硬件层:包含CPU,存储器(SDRAM&…...
Linux主机Tcpdump使用-centos实例
1、安装前系统信息 ifconfig查看系统网络接口情况。这里可以看到3个interface,ens160是正常使用的网口,lo是主机的loopback地址127.0.0.1。另外,由于centos安装在虚拟主机上,virbr0是KVM默认创建的一个Bridge,其作用是为连接其上的…...
线性DP——AcWing 898. 数字三角形、AcWing 895. 最长上升子序列
AcWing 898. 数字三角形 1.题目 898. 数字三角形 2.思路 DP问题首先考虑状态转移方程,定义一个集合f ( i , j) ,表示从第一个数字(1,1)走到第 i行,第 j列(i , j)的所有方案的集合,…...
SpringMVC
SpringMVC配置 引入Maven依赖 (springmvc)web.xml配置DispatcherServlet配置 applicationContext 的 MVC 标记开发Controller控制器 几点注意事项: 在web.xml中 配置<load-on-startup> 0 </load-on-startup> 会自动创建Spring…...
C++模板基础(二)
函数模板(二) ● 模板实参的类型推导 – 如果函数模板在实例化时没有显式指定模板实参,那么系统会尝试进行推导 template<typename T> void fun(T input, T input2) {std::cout << input << \t << input2 << …...
什么是linux内核态、用户态?
目录标题为什么需要区分内核空间与用户空间内核态与用户态如何从用户空间进入内核空间整体结构为什么需要区分内核空间与用户空间 在 CPU 的所有指令中,有些指令是非常危险的,如果错用,将导致系统崩溃,比如清内存、设置时钟等。如…...
day8—选择题
文章目录1.Test.main() 函数执行后的输出是(D)2. JUnit主要用来完成什么(D)3.下列选项中关于Java中super关键字的说法正确的是(A)1.Test.main() 函数执行后的输出是(D) public clas…...
【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型
摘要 拍照搜题系统采用“三层管道(多模态 OCR → 语义检索 → 答案渲染)、两级检索(倒排 BM25 向量 HNSW)并以大语言模型兜底”的整体框架: 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后,分别用…...
shell脚本--常见案例
1、自动备份文件或目录 2、批量重命名文件 3、查找并删除指定名称的文件: 4、批量删除文件 5、查找并替换文件内容 6、批量创建文件 7、创建文件夹并移动文件 8、在文件夹中查找文件...
家政维修平台实战20:权限设计
目录 1 获取工人信息2 搭建工人入口3 权限判断总结 目前我们已经搭建好了基础的用户体系,主要是分成几个表,用户表我们是记录用户的基础信息,包括手机、昵称、头像。而工人和员工各有各的表。那么就有一个问题,不同的角色…...
NLP学习路线图(二十三):长短期记忆网络(LSTM)
在自然语言处理(NLP)领域,我们时刻面临着处理序列数据的核心挑战。无论是理解句子的结构、分析文本的情感,还是实现语言的翻译,都需要模型能够捕捉词语之间依时序产生的复杂依赖关系。传统的神经网络结构在处理这种序列依赖时显得力不从心,而循环神经网络(RNN) 曾被视为…...
【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分
一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计,提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合:各模块职责清晰,便于独立开发…...
Java面试专项一-准备篇
一、企业简历筛选规则 一般企业的简历筛选流程:首先由HR先筛选一部分简历后,在将简历给到对应的项目负责人后再进行下一步的操作。 HR如何筛选简历 例如:Boss直聘(招聘方平台) 直接按照条件进行筛选 例如:…...
Redis数据倾斜问题解决
Redis 数据倾斜问题解析与解决方案 什么是 Redis 数据倾斜 Redis 数据倾斜指的是在 Redis 集群中,部分节点存储的数据量或访问量远高于其他节点,导致这些节点负载过高,影响整体性能。 数据倾斜的主要表现 部分节点内存使用率远高于其他节…...
JAVA后端开发——多租户
数据隔离是多租户系统中的核心概念,确保一个租户(在这个系统中可能是一个公司或一个独立的客户)的数据对其他租户是不可见的。在 RuoYi 框架(您当前项目所使用的基础框架)中,这通常是通过在数据表中增加一个…...
Web中间件--tomcat学习
Web中间件–tomcat Java虚拟机详解 什么是JAVA虚拟机 Java虚拟机是一个抽象的计算机,它可以执行Java字节码。Java虚拟机是Java平台的一部分,Java平台由Java语言、Java API和Java虚拟机组成。Java虚拟机的主要作用是将Java字节码转换为机器代码&#x…...
PHP 8.5 即将发布:管道操作符、强力调试
前不久,PHP宣布了即将在 2025 年 11 月 20 日 正式发布的 PHP 8.5!作为 PHP 语言的又一次重要迭代,PHP 8.5 承诺带来一系列旨在提升代码可读性、健壮性以及开发者效率的改进。而更令人兴奋的是,借助强大的本地开发环境 ServBay&am…...
