定时器

  • 定时器原理
    • 如何理解定时器
    • 定时器数据结构选取
    • 定时器触发方式
  • 定时器的实现

定时器原理

如何理解定时器

定时器在日常通常被描述为组织大量延时任务的模块,其实从字面意思去理解的话,他就是去处理延时任务的,那么什么是延时任务呢?我们来看一张图:
在这里插入图片描述
在现代开发当中,同时是要处理多个任务的,我们将任务添加进某个数据结构到任务触发之间是存在一段时间间隔的,那么就会引发一个问题,这段时间当前 CPU 核心是在去等么?

这儿肯定是不会让 CPU 核心去等的,因为我们要保证一个原则,不过去占用一个线程,高效的处理定时任务,这也意味着再添加任务到触发任务之间的这一段时刻,我们不可能让线程在这儿进行等待,这样就违背了我们高效的原则了。

所以也就有了定时器的出现,在添加任务到触发任务之间肯定是会存在大量任务的,我们就需要将当前的大量任务组织起来,然后触发最近将要超时的任务,这样,就保证了当前线程并不是一味地在这儿阻塞进行等待,对于将要超时的任务他就会去进行处理。

那么定时器就可以被描述为组织大量延时任务的数据结构和触发最近将要超时任务的机制。

定时器数据结构选取

既然定时器需要组织大量的延时任务,就需要选取一种数据结构,最为合适的几种数据结构分别为:红黑树、最小堆和时间轮。

红黑树

根据上面的描述我们就可以这么去理解定时器,“时间”“任务”,两者之间就成了对应的关系,某个时间点就对应了相应的任务,其实我们就可以看出来这天然的就对应这一种 [key,value] 的关系了,这种关系很容易就让人想到了红黑树这中数据结构。

同时,对于添加的任务来说,肯定是存在时间顺序的,因为定时器总是优先去处理最近将要么超时的任务:
在这里插入图片描述
红黑树是一种自平衡的二叉查找树,同时,他也是一种有序的数据结构,对于插入以及删除的时间复杂度都是O(LogN)级别的,如果要实现一个定时,红黑树肯定是一个可以选择的数据结构,在 C++ 的 STL 库中存在四种红黑树的数据结构:map/set/multimap/multiset,我们后续定时器的实现选取的就是multimap这个结构。

选取multimap这个结构主要是基于两点:

  • 肯定会存在同一时间触发多个任务的场景存在,map的实现是基于一对一的关系的,而multimap是基于一对多的关系的,所以肯定是选multimap的;
  • 我们通常是获取最近将要超时的任务,添加也是添加基于顺序进行添加的,那么基于红黑树这种数据结构,我们会发现第一种操作的的时间复杂度是O(1)。
    在这里插入图片描述
    Ngnix,workflow里面的定时器都是基于红黑树进行实现的。

最小堆

最小堆是一种特殊的完全二叉树数据结构,其中每个父节点的值都小于或等于其子节点的值。这个性质使得最小堆的根节点始终是整个堆中的最小元素。

他的特点就在于:最小堆是一棵完全二叉树,意味着除了最后一层外,其他层都是完全填充的,且最后一层的节点尽可能向左靠拢。而且任意一个子节点页满足最小堆的要求。
在这里插入图片描述
对于最小堆来说他的查找,插入和删除的效率也是很高的,我们如果需要插入一个元素,基于最小堆的性质,肯定是往二叉树最高层沿着最左侧添加一个节点,然后考虑是否需要上升进行调整;删除一个元素也是,先找到这个元素,然后将其与最后一个节点进行交换,然后调整堆结构,这个可以参考之前的一篇文章:堆数据结构详解。

堆的这种理念其实跟定时器也是比较契合的,最小堆堆顶总是最小的那个数据,也就是我们最近需要处理的任务,同时,他的插入和删除的也是比较高的。libev,libevent 里面的定时器是基于最小堆实现的。

时间轮

红黑树,最小堆是按触发时间进行顺序组织,而接下来需要介绍的时间轮是按执行顺序进行组织的。

对于时间轮来说,它是基于钟表的原理来进行实现的:
在这里插入图片描述
在这里插入图片描述
对于Linux内核,Kafka 都是基于时间轮进行实现的。

定时器触发方式

对于服务端来说,驱动服务端业务逻辑的事件包括网络事件、定时事件、以及信号事件;通常网络事件和定时事件会进行协同处理;定时器触发形式通常有两种:

利用 IO 多路复用系统调用最后一个参数(超时),来触发检测定时器

理解定时器以后我们就很容易想到 IO 多路复用的最后一个参数timeout,他的取值会存在以下三种情况:

  • NULL/nullptr:select调用后进行阻塞等待,直到被监视的某个文件描述符上的某个事件就绪。
  • 0:selec调用后t进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,select检测后都会立即返回。
  • 特定的时间值:select调用后在指定的时间内进行阻塞等待,如果被监视的文件描述符上一直没有事件就绪,则在该时间后select进行超时返回。

我们首先的理解 recator 的本质,他是将 IO 转化为对事件的管理,因为我们对于用户端来说,并不知道 IO 什么时候就绪,客户端何时发送数据,什么时候建立连接…,存在很多个不知道的情况,所以就会导致我们不知道什么时候去调用 accept/read/write,而 IO 多路复用解决的就是这个问题,当监听事件就绪就调用 accept 建立连接通路,真正的事件就绪就调用read/write对数据进行读写,他本质上解决的就是一个调用时机的问题。

对于我们的定时器来说,是处理定时任务的,他其实本质上也是一个事件,如果时间到了,就去执行,其实跟 IO 多路复用的这种思想是很相似的,我们都是需要去异步处理它,所以在这儿我们会使用 IO 多路复用的最后一个参数。

利用 timerfd,将定时检测作为 IO 多路复用当中的事件进行处理

timerfd 其实也是基于网络 IO 的思想进行实现的,他主要是依靠以下两个接口来进行实现的:

#include <sys/timerfd.h>int timerfd_create(int clockid, int flags);

timerfd_create 用于创建一个定时器文件描述符(timer file descriptor),它允许应用程序通过文件描述符来监控定时器事件,解析如下:

  • clockid:指定定时器使用的时钟源,常见选项包括:
    CLOCK_REALTIME:系统实时时间(可受系统时间调整影响);
    CLOCK_MONOTONIC:单调时钟(不受系统时间调整影响,适合测量时间间隔);
    CLOCK_BOOTTIME(Linux 2.6.39 后支持):类似 CLOCK_MONOTONIC,但包含系统休眠时间。
  • flags:控制文件描述符的行为,可选值:
    TFD_NONBLOCK:设置文件描述符为非阻塞模式。
    TFD_CLOEXEC:设置文件描述符为 close-on-exec(执行 exec 时自动关闭)。

返回值:

  • 成功时返回一个文件描述符(fd),可用于后续的 timerfd_settime 和 read 操作。
  • 失败时返回 -1,并设置 errno(如 EINVAL 表示无效参数)。
int timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value);

timerfd_settime 用于设置或修改 timerfd 计时器的到期时间和间隔周期。它是 timerfd 系列系统调用的一部分,需要配合 timerfd_create 创建的定时器文件描述符使用,参数解析:

  • fd:由 timerfd_create 创建的定时器文件描述符;
  • flags:控制标志位,可以是以下值之一:
    0:使用相对时间(以当前时间为基准)
    TFD_TIMER_ABSTIME:使用绝对时间(以 Epoch 时间为基准)
  • new_value:指向 itimerspec 结构的指针,指定新的定时器设置;
struct itimerspec {struct timespec it_interval;  /* 定时器间隔周期 */struct timespec it_value;     /* 首次到期时间 */
};struct timespec {time_t tv_sec;                /* 秒 */long tv_nsec;                 /* 纳秒 */
};
  • old_value:指向 itimerspec 结构的指针,用于存储之前的定时器设置(可为 NULL)。

返回值:

  • 成功时返回 0
  • 失败时返回 -1 并设置 errno

定时器的实现

接下来我们通过红黑树的方式来实现一个定时器:

#ifndef __TIMER__
#define __TIMER__#include <map>
#include <functional>
#include <chrono>// TimerNode 结点,延时任务
class TimerNode
{
public:friend class Timer;TimerNode(uint64_t timeout, std::function<void()> cb): timeout_(timeout), callback_(std::move(cb)){}private:uint64_t timeout_;std::function<void()> callback_;
};class Timer
{
public:// 添加延时任务TimerNode* AddTimeout();// 删除延时任务void DelTimeout();// 获取延时时间int WaitTime();// 处理延时任务void HandleTimeout();private:std::multimap<uint64_t, TimerNode *> timer_map_; // 前面代表超时时间,后面代表超时任务
};#endif

添加延时任务、

static uint64_t GetCurrentTime()
{using namespace std::chrono;return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
}
// 添加延时任务
TimerNode *AddTimeout(uint64_t diff, std::function<void()> cb)
{// 根据 diff 创建延时任务auto node = new TimerNode(GetCurrentTime() + diff, std::move(cb));if (timer_map_.empty() || node->timeout_ < timer_map_.rbegin()->first){auto it = timer_map_.insert(std::make_pair(node->timeout_, std::move(node)));return it->second;}else{auto it = timer_map_.emplace_hint(timer_map_.crbegin().base(), std::make_pair(node->timeout_, std::move(node)));return it->second;}
}
  • 我们的延时任务通过 timer_map_ 进行管理,添加延时任务其实就是将延时任务放进 timer_map_ 中进行管理,我们的每一个延时任务都需要被处理,被添加以后我们如何去进行处理就需要使用到回调函数,将回调函数作为 TimerNode 的第二个结点的原因也在这儿,我们可以更为直接的对延时任务进行处理;
  • 对于延时任务,我们以当前时间+延时时间,表示延时的时间,在这儿进行添加的时候我们就需要注意,我们添加的延时任务不一定是按顺序排布的,但是红黑树这个结构肯定是按顺序进行排布的,所以添加的延时任务时我们就可以进行优化,如果对应的添加的延时任务的延时时间是小于红黑树最后一个节点的,我们调用 insert 函数插入,因为要重新进行排序,如果对应的添加的延时任务的延时时间是大于红黑树最后一个节点的,我们直接找到尾结点,调用 emplace_hint 函数直接构造一个节点即可,这样是可以提高效率的。
  • 最终返回值肯定当前的这个延时任务。

删除延时任务

// 删除延时任务
void DelTimeout(TimerNode *node)
{auto it = timer_map_.equal_range(node->timeout_);for (auto iter = it.first; iter != it.second; iter++){if (iter->second == node){timer_map_.erase(iter);break;}}
}

删除延时任务应该注意的地方就在同一时间我们可能会插入多个延时任务,那么再删除的时候就应该将这个时间点对应的已经处理的延时任务删除,就使用到了 equal_range 这个接口,他用于在有序范围内查找等于给定值的所有元素的范围。它返回一个 std::pair,其中 first 指向第一个不小于给定值的元素,second 指向第一个大于给定值的元素。刚好适用于当前场景。

获取延时时间

// 获取延时时间
int WaitTime()
{auto iter = timer_map_.begin();if (iter == timer_map_.end()){return -1;}uint64_t diff = iter->first - GetCurrentTime();return diff > 0 ? diff : 0;
}

获取延时时间其实就是获取到当前延时任务的一个时间,这个就不多做解释了。

处理延时任务

// 处理延时事件
void HandleTimeout()
{auto iter = timer_map_.begin();while (iter != timer_map_.end() && iter->first <= GetCurrentTime()){iter->second->callback_();iter = timer_map_.erase(iter);}
}

处理我们对应的延时任务,就是调用对应的回调函数,处理完成以后将这个延时任务删除就可以了。

整体代码如下:

#ifndef __TIMER__
#define __TIMER__#include <map>
#include <functional>
#include <chrono>// TimerNode 结点,延时任务
class TimerNode
{
public:friend class Timer;TimerNode(uint64_t timeout, std::function<void()> cb): timeout_(timeout), callback_(std::move(cb)){}private:// int id_;uint64_t timeout_;               // 什么时候触发std::function<void()> callback_; // 异步任务,回调函数
};class Timer
{
public:static uint64_t GetCurrentTime(){using namespace std::chrono;return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();}// 添加延时任务TimerNode *AddTimeout(uint64_t diff, std::function<void()> cb){// 根据 diff 创建延时任务auto node = new TimerNode(GetCurrentTime() + diff, std::move(cb));if (timer_map_.empty() || node->timeout_ < timer_map_.rbegin()->first){auto it = timer_map_.insert(std::make_pair(node->timeout_, std::move(node)));return it->second;}else{auto it = timer_map_.emplace_hint(timer_map_.crbegin().base(), std::make_pair(node->timeout_, std::move(node)));return it->second;}}// 删除延时任务void DelTimeout(TimerNode *node){auto it = timer_map_.equal_range(node->timeout_);for (auto iter = it.first; iter != it.second; iter++){if (iter->second == node){timer_map_.erase(iter);break;}}}// 获取延时时间int WaitTime(){auto iter = timer_map_.begin();if (iter == timer_map_.end()){return -1;}uint64_t diff = iter->first - GetCurrentTime();return diff > 0 ? diff : 0;}// 处理延时事件void HandleTimeout(){auto iter = timer_map_.begin();while (iter != timer_map_.end() && iter->first <= GetCurrentTime()){iter->second->callback_();iter = timer_map_.erase(iter);}}private:std::multimap<uint64_t, TimerNode *> timer_map_; // 前面代表超时时间,后面代表超时任务// std::unordered_map<int, TimerNode *> map_; // id与延时任务对应的关系Timer() = default;Timer(const Timer &) = delete;Timer &operator=(const Timer &) = delete;Timer(Timer &&) = delete;Timer &operator=(Timer &&) = delete;~Timer(){for (auto &pair : timer_map_){delete pair.second;}}
};#endif

当前定时器存在一个可以优化的点,就在于我们需要让他能够全局被进行使用,不希望每次不同对象去使用都去进行创建,在这里可以将他设置成为单例的,提供给全局进行使用,优化后代码如下:

#ifndef __TIMER__
#define __TIMER__#include <map>
#include <functional>
#include <chrono>// TimerNode 结点,延时任务
class TimerNode
{
public:friend class Timer;TimerNode(uint64_t timeout, std::function<void()> cb): timeout_(timeout), callback_(std::move(cb)){}private:// int id_;uint64_t timeout_;               // 什么时候触发std::function<void()> callback_; // 异步任务,回调函数
};class Timer
{
public:// 单例模式static Timer* GetInstance(){static Timer instance;return &instance;}static uint64_t GetCurrentTime(){using namespace std::chrono;return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();}// 添加延时任务TimerNode *AddTimeout(uint64_t diff, std::function<void()> cb){// 根据 diff 创建延时任务auto node = new TimerNode(GetCurrentTime() + diff, std::move(cb));if (timer_map_.empty() || node->timeout_ < timer_map_.rbegin()->first){auto it = timer_map_.insert(std::make_pair(node->timeout_, std::move(node)));return it->second;}else{auto it = timer_map_.emplace_hint(timer_map_.crbegin().base(), std::make_pair(node->timeout_, std::move(node)));return it->second;}}// 删除延时任务void DelTimeout(TimerNode *node){auto it = timer_map_.equal_range(node->timeout_);for (auto iter = it.first; iter != it.second; iter++){if (iter->second == node){timer_map_.erase(iter);break;}}}// 获取延时时间int WaitTime(){auto iter = timer_map_.begin();if (iter == timer_map_.end()){return -1;}uint64_t diff = iter->first - GetCurrentTime();return diff > 0 ? diff : 0;}// 处理延时事件void HandleTimeout(){auto iter = timer_map_.begin();while (iter != timer_map_.end() && iter->first <= GetCurrentTime()){iter->second->callback_();iter = timer_map_.erase(iter);}}private:std::multimap<uint64_t, TimerNode *> timer_map_; // 前面代表超时时间,后面代表超时任务// std::unordered_map<int, TimerNode *> map_; // id与延时任务对应的关系Timer() = default;Timer(const Timer &) = delete;Timer &operator=(const Timer &) = delete;Timer(Timer &&) = delete;Timer &operator=(Timer &&) = delete;~Timer(){for (auto &pair : timer_map_){delete pair.second;}}
};#endif

这就是当前整个定时器的设计,他主要就是用来去处理延时任务的,当我们需要处理大量延时任务的时候,使用定时器就会比较合适。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/87153.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/87153.shtml
英文地址,请注明出处:http://en.pswp.cn/bicheng/87153.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

大模型-分布式论文一瞥

1分离式架构 1.1 DistServe DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving 讲的是一个将prefill和decoding分…

02.SpringBoot常用Utils工具类详解

文章目录 1. BeanUtils详解1.1 什么是BeanUtils&#xff1f;1.2 主要的BeanUtils实现1.2.1 Spring BeanUtils1.2.2 Apache Commons BeanUtils1.2.3 其他实现 1.3 Spring BeanUtils详细使用1.3.1 基本用法1.3.2 指定忽略属性1.3.3 批量拷贝&#xff08;列表转换&#xff09; 1.4…

Golang快速开发框架——项目立项与系统配置读取组件viper(一)

Golang快速开发框架——项目立项与系统配置读取组件viper&#xff08;一&#xff09; 背景 知识分享之Golang篇是我在日常使用Golang时学习到的各种各样的知识的记录&#xff0c;将其整理出来以文章的形式分享给大家&#xff0c;来进行共同学习。欢迎大家进行持续关注。 知识分…

打造可观测的 iOS CICD 流程:调试、追踪与质量保障全记录

随着iOS项目复杂度增加&#xff0c;团队越来越依赖自动化构建、自动化测试等CI/CD流程来保证产品质量。但CI/CD环境下&#xff0c;很多线下调试手段无法直接使用&#xff0c;比如&#xff1a; 无法手动连真机跑Instruments测试包只在分发后才能拿到崩溃模拟器上表现和真机不一…

C++11中 <cinttypes>的入门与精通

文章目录 一、<cinttypes> 是什么1. 固定宽度的整数类型2. 整数操作函数3. 格式化输入输出宏 二、深入理解 <cinttypes>1. 固定宽度整数类型的使用2. 整数操作函数的使用3. 格式化输入输出宏的使用 三、实践和技巧1. 使用固定宽度整数类型的最佳实践2. 使用整数操作…

Pytorhc Lightning进阶:一篇实例玩转Pytorhc Lightning 让训练更高效

Pytorhc Lightning进阶&#xff1a;一篇实例玩转Pytorhc Lightning 让训练更高效 Pytorhc Lightning 主要包含以下几大类&#xff0c;主要围绕以下讲解&#xff1a; 模型&#xff0c;PyTorch Lightning 的核心是继承 pl.LightningModule数据&#xff0c;数据模块继承pl.Light…

大模型算法面试笔记——注意力Transformer流程/面试题篇

学习资料来源于字母站大学 1 Transformer架构 基于编码器-解码器的架构来处理序列对。跟使用注意力的seq2seq不同&#xff0c;Transformer是基于纯注意力。 2 注意力 2.1 自注意力机制 使用注意力&#xff1a;需要根据整个序列进行预测&#xff0c;对于同一input&#xf…

Rust 定义与实例化结构体

文章目录 Rust 定义与实例化结构体5.1 结构体的定义与意义5.2 结构体实例化5.2.1 基本实例化5.2.2 可变性规则5.2.3 字段初始化简写5.2.4 结构体更新语法 5.3 特殊结构体类型5.3.1 元组结构体&#xff08;Tuple Struct&#xff09;5.3.2 类单元结构体&#xff08;Unit-Like Str…

ELK日志分析系统(filebeat+logstash+elasticsearch+kibana)

一、ELK 平台介绍 1、ELK 概述 日志主要包括系统日志、应用程序日志和安全日志。系统运维和开发人员可以通过日志了解服务器软硬件信息、检查配置过程中的错误及错误发生的原因。经常分析日志可以了解服务器的负荷&#xff0c;性能安全性&#xff0c;从而及时采取措施纠正错误。…

JS基础4—jQuery

jQuery常用内容 jQuery 介绍jQuery 获取方式基本选择器 (最常用)层级选择器 (基于元素间关系)过滤选择器 (基于特定条件) jQuery事件绑定jQuery 方法调用jQuery遍历jQuery 获取与设置jQuery 添加与删除jQuery CSS 类jQuery - AJAX 总结 jQuery 介绍 jQuery 是一个轻量级、快速…

时钟周期是什么?

时钟周期&#xff08;Clock Cycle&#xff09;是什么&#xff1f; 时钟周期&#xff08;Clock Cycle&#xff09;是计算机系统中一个最基础的时间单位&#xff0c;也称为时钟节拍或时钟周期时间&#xff08;Clock Period&#xff09;。它由系统时钟发生器产生的一个周期性脉冲…

如何用SEO优化长尾关键词?

内容概要 在SEO优化领域&#xff0c;长尾关键词扮演着至关重要的角色&#xff0c;它们能有效提升网站在搜索引擎中的可见度和流量转化率。本文将全面解析如何通过系统方法优化长尾关键词&#xff0c;涵盖从基础理论到实战应用的完整流程。核心内容包括利用专业工具进行关键词挖…

电子面单系统开发全解析

一、如果要做电子面单系统&#xff0c;怎么做&#xff1f; 开发电子面单系统是一项复杂且涉及多方面考量的工程&#xff0c;涵盖需求分析、系统架构设计、技术选型、接口对接、安全性保障、第三方服务选择以及部署与维护等关键环节。 电子面单系统开发步骤 需求分析&#xf…

UE5 - 制作《塞尔达传说》中林克的技能 - 18 - 磁力抓取器

让我们继续《塞尔达传说》中林克技能的制作!!! UE版本:5.6.0 VS版本:2022 本章节的核心目标:磁力抓取器 先让我们看一下完成后的效果: 18_磁力抓取器 大纲如下: 引言功能架构与核心逻辑物理材质与场景配置代码实现:从识别到操控操作说明1.引言 在《塞尔达传说》中,林…

基于ApachePOI实现百度POI分类快速导入PostgreSQL数据库实战

目录 前言 一、百度POI分类简介 1、数据表格 2、分类结构 二、从Excel导入到PG数据库 1、Excel解析流程 2、数据入库 3、入库成果及检索 三、总结 前言 在上一篇博文中&#xff0c;我们对高德POI分类进行了深入剖析 并对Excel 中 POI 分类数据的存储结构特点进行了详细介…

学习经验分享【41】YOLOv13:基于超图增强自适应视觉感知的实时目标检测

YOLO算法更新速度很快&#xff0c;已经出到V13版本&#xff0c;后续大家有想发论文或者搞项目可更新自己的baseline了。 摘要&#xff1a;YOLO 系列模型凭借其卓越的精度和计算效率&#xff0c;在实时目标检测领域占据主导地位。然而&#xff0c;YOLOv11 及早期版本的卷积架构&…

Handling outliers in non-blind image deconvolution论文阅读

Handling outliers in non-blind image deconvolution 1. 研究目标与实际意义2. 创新方法:基于EM的异常值建模2.1 新模糊模型2.1.1 目标函数2.2 EM框架:迭代优化二元掩码2.2.1 E步:计算后验权重 E [ m x ] E[m_x] E[mx​]2.2.2 M步:加权正则化反卷积2.3 优化加速技术2.3.1…

Redis 功能扩展:Lua 脚本对 Redis 的扩展

Redis 是一个高性能的内存数据库&#xff0c;支持多种数据结构&#xff0c;如字符串、哈希、列表、集合和有序集合。为了增强其功能&#xff0c;Redis 引入了 Lua 脚本支持&#xff0c;使开发者可以编写自定义的脚本&#xff0c;确保操作的原子性并提高复杂操作的性能。本文将详…

七天学完十大机器学习经典算法-06.支持向量机(SVM):分类边界的艺术——深入浅出指南

接上一篇《七天学完十大机器学习经典算法-05.从投票到分类&#xff1a;K近邻(KNN)算法完全指南》 想象你要在操场上为两个班级划活动区域&#xff0c;如何画出一条最公平的分界线&#xff1f;这条线不仅要分开两班学生&#xff0c;还要让两个班都离分界线尽可能远——这就是支持…

python如何安装PyQt6-stubs依赖包

PyQt6-stubs 是为 PyQt6 提供类型提示&#xff08;Type Hints&#xff09;和 IDE 智能补全支持的第三方补丁包&#xff0c;特别适用于 PyCharm、VS Code 等现代 IDE。它对开发者在编码时帮助极大。 一、安装方法 需要提前安装好git&#xff0c;然后克隆PyQt6-stubs源码&#xf…