手撕定时器:从零开始

目录

1. 定时器概述

1.1 容器

1.2 检测触发机制

2. 定时任务的执行

2.1 异步执行

2.2 触发时间

2.3 回调函数

3. 定时任务的容器结构

3.1 按触发时间排序的结构

3.1.1 红黑树(如 map、set、multimap、multiset)

3.1.2 最小堆

3.2 按执行序排列的结构:时间轮(Timing Wheel)

4. 检测和触发机制

4.1 基于IO多路复用的检测

4.1.1 阻塞时间

4.1.2 应用实例

4.2 timerfd

4.2.1 将定时任务转化为IO

4.2.2 Workflow

4.3 解决的核心问题

4.3.1 阻塞与调度

4.3.2 内核级检测

5. 定时器的实现

5.1 封装

5.2 实现

5.2.1 容器

5.2.2 检测触发机制

5.3 测试

5.4 优化

5.4.1 针对大量间隔时间相同的任务的优化

5.4.2 拆分思想

 5.5 代码实践

5.5.1 epoll_wait第4个参数驱动

1. 定义基本定时器节点

2. 定义定时器节点

3. 定义比较运算符 

4. 定义定时器类

5. 添加定时器

6. 删除定时器

7. 处理定时器

8. 计算休眠时间

9. ID 生成器

10. 主函数

11. 添加定时任务

12. 删除定时任务

13. 进入事件循环

总结


1. 定时器概述

定时器是用于管理和调度大量定时任务的模块。其核心组件包括容器检测触发机制

1.1 容器

容器负责组织和存储所有的定时任务。高效的容器结构能够支持快速插入、删除和查找任务,确保系统在处理大量定时任务时依然高效。

1.2 检测触发机制

检测触发机制的主要职责是监控定时任务,及时触发即将到期的任务。它需要具备高效的查找和调度能力,以确保最近要触发的任务能够被优先处理。

2. 定时任务的执行

2.1 异步执行

定时任务通常是异步执行的,这意味着它们不会阻塞主线程或过度占用线程资源。通过异步机制,可以在任务触发时高效地执行回调函数,而不影响系统的整体性能。

2.2 触发时间

每个定时任务都有一个触发时间,通常由当前时间加上一个间隔时间计算得出。这确保了任务在预定的时间点被准确地执行。

2.3 回调函数

当定时任务被触发时,会执行预先定义的回调函数。这些回调函数可以执行各种操作,如处理数据、发送通知或执行其他业务逻辑。

3. 定时任务的容器结构

选择合适的数据结构来存储和管理定时任务对于定时器的性能至关重要。以下是常见的几种容器结构:

3.1 按触发时间排序的结构

3.1.1 红黑树(如 mapsetmultimapmultiset

红黑树是一种自平衡的二叉查找树,能够在对数时间内进行插入、删除和查找操作。它适用于需要频繁动态调整任务的场景。

优点:

  • 动态性强,适合频繁添加和删除任务。
  • 可以快速查找最早要触发的任务。

缺点:

  • 相比其他数据结构,内存占用可能更高。
3.1.2 最小堆

最小堆是一种优先队列,能够在常数时间内找到最小元素(即最早触发的任务),并在对数时间内进行插入和删除操作。

优点:

  • 插入和删除操作效率高。
  • 适合需要快速访问最早任务的场景。

缺点:

  • 不支持高效的中间元素查找和删除。

3.2 按执行序排列的结构:时间轮(Timing Wheel)

时间轮是一种基于环形数据结构的定时器实现,适用于处理大量定时任务,尤其是那些具有相似触发时间间隔的任务。

优点:

  • 内存和计算效率高,适合处理高并发定时任务。
  • 能够以恒定时间复杂度处理任务。

缺点:

  • 实现相对复杂。
  • 对于长时间间隔的任务,可能需要多级时间轮。

4. 检测和触发机制

4.1 基于IO多路复用的检测

IO多路复用(如 selectpollepoll)允许在单线程中同时处理多个IO事件。定时任务的检测可以集成到IO多路复用的机制中,通过设置一个超时参数来实现。

4.1.1 阻塞时间

当没有网络事件时,IO多路复用函数会阻塞等待一定时间。这段阻塞时间可以设置为当前时间到最近要触发任务的时间间隔,确保在任务到期前能够被及时唤醒。

4.1.2 应用实例

RedisNginx 等高性能服务器广泛使用这种机制来高效地处理网络和定时任务。

4.2 timerfd

timerfd 是 Linux 提供的一种机制,将定时器事件转化为文件描述符,可以与IO多路复用机制无缝集成。

4.2.1 将定时任务转化为IO

通过 timerfd_create 创建一个定时器文件描述符,并将定时任务的触发时间设置到该文件描述符上。这样,IO多路复用就可以监控定时任务的触发,统一处理网络和定时事件。

4.2.2 Workflow
  1. 创建 timerfd 并设置触发时间。
  2. timerfd 添加到 IO 多路复用的监控列表中。
  3. 当定时器到期时,IO 多路复用会检测到 timerfd 的可读事件,触发相应的回调函数。

4.3 解决的核心问题

主要解决从当前时间到最近要过期任务的时间节点之间,当前线程如何高效处理的问题。

4.3.1 阻塞与调度

通过阻塞当前线程,释放执行权,让操作系统调度其他任务。这可以避免线程在等待过程中浪费资源。

4.3.2 内核级检测

将定时任务的检测需求交给内核,当前线程可以继续执行其他操作。当内核检测到定时任务到期时,通知当前线程进行处理。这种方式能够提高系统的整体效率和响应速度。

5. 定时器的实现

5.1 封装

定时器的封装通常涉及将定时器的功能模块化,以便在不同的上下文中重用。封装的目标是提供一个简洁的接口,使用户能够方便地创建和管理定时任务。

封装的要点:

  • 接口设计:定义清晰的API,包括任务的添加、删除、启动和停止等功能。
  • 内部实现:将容器和检测机制的具体实现细节隐藏在封装内部,用户只需关注如何使用接口。
  • 错误处理:确保在任务添加或执行过程中能够处理异常情况,提供必要的错误反馈。

5.2 实现

5.2.1 容器

在实现中选择合适的容器结构是关键。可以结合前面提到的红黑树、最小堆和时间轮等结构。具体选择可以基于任务特性和性能需求。

示例结构:

  • 红黑树:适用于需要频繁动态插入和删除的任务。
  • 最小堆:适合任务量较大时快速访问最近要触发的任务。
  • 时间轮:适合处理大量具有相似触发时间间隔的任务。
5.2.2 检测触发机制

实现检测触发机制时,可以采用 IO 多路复用和 timerfd 等技术来高效监控定时任务的触发。确保机制能够及时响应即将到期的任务。

实现思路:

  • 使用 IO 多路复用来等待定时器和其他IO事件。
  • 当定时器到期时,通过回调函数处理任务。

5.3 测试

测试是验证定时器实现是否符合预期的重要环节。可以进行以下类型的测试:

  • 单元测试:针对各个模块(如容器、检测机制)进行单元测试,确保其功能正确。
  • 集成测试:测试定时器的整体功能,包括任务的添加、执行和删除。
  • 性能测试:在高并发和高任务量的情况下测试定时器的性能,验证其响应时间和资源使用情况。

5.4 优化

随着系统负载的增加,定时器的性能可能会受到影响,因此需要考虑优化策略。

5.4.1 针对大量间隔时间相同的任务的优化

在大量定时任务的情况下,可以考虑使用 insert_hint 技术,从 O(log n) 优化到 O(1)。

具体实现:

  • 在任务添加时,可以通过维护一个指向合适插入位置的指针,减少不必要的比较和移动操作。
  • 在插入相同触发时间的任务时,可以直接将任务插入到指针指向的位置,从而降低复杂度。
5.4.2 拆分思想

拆分思想可以有效提升定时器的性能,特别是在高并发场景中。

实现方式:

  • 单线程多容器:在单个线程中使用多个容器来处理不同类型或优先级的定时任务。可以避免一个容器中任务的增加导致性能瓶颈。
  • 多线程多容器:在多线程环境中,每个线程维护自己的容器和检测机制,这样可以并行处理任务,提高整体处理能力。

综合效果:

  • 减少单个容器和线程的负载,提高系统的响应速度。
  • 通过合理的任务分配和资源调度,优化资源利用率,提升系统整体性能。

 5.5 代码实践

5.5.1 epoll_wait第4个参数驱动
#include <sys/epoll.h>
#include <functional>
#include <chrono>
#include <set>
#include <memory>
#include <iostream>using namespace std;struct TimerNodeBase {time_t expire;int64_t id;
};struct TimerNode : public TimerNodeBase {using Callback = std::function<void(const TimerNode &node)>;Callback func;TimerNode(int64_t id, time_t expire, Callback func) : func(func) {this->expire = expire;this->id = id;}
};bool operator < (const TimerNodeBase &lhd, const TimerNodeBase &rhd) {if (lhd.expire < rhd.expire)return true;else if (lhd.expire > rhd.expire) return false;return lhd.id < rhd.id;
}class Timer {
public:static time_t GetTick() {auto sc = chrono::time_point_cast<chrono::milliseconds>(chrono::steady_clock::now());auto temp = chrono::duration_cast<chrono::milliseconds>(sc.time_since_epoch());return temp.count();}TimerNodeBase AddTimer(time_t msec, TimerNode::Callback func) {time_t expire = GetTick() + msec;if (timeouts.empty() || expire <= timeouts.crbegin()->expire) {auto pairs = timeouts.emplace(GenID(), expire, std::move(func));return static_cast<TimerNodeBase>(*pairs.first);}auto ele = timeouts.emplace_hint(timeouts.crbegin().base(), GenID(), expire, std::move(func));return static_cast<TimerNodeBase>(*ele);}bool DelTimer(TimerNodeBase &node) {auto iter = timeouts.find(node);if (iter != timeouts.end()) {timeouts.erase(iter);return true;}return false;}void HandleTimer(time_t now) {auto iter = timeouts.begin();while (iter != timeouts.end() && iter->expire <= now) {iter->func(*iter);iter = timeouts.erase(iter);}}time_t TimeToSleep() {auto iter = timeouts.begin();if (iter == timeouts.end()) {return -1;}time_t diss = iter->expire - GetTick();return diss > 0 ? diss : 0;}
private:static int64_t GenID() {return gid++;}static int64_t gid;set<TimerNode, std::less<>> timeouts;
};
int64_t Timer::gid = 0;int main() {int epfd = epoll_create(1);unique_ptr<Timer> timer = make_unique<Timer>();int i =0;timer->AddTimer(1000, [&](const TimerNode &node) {cout << Timer::GetTick() << " node id:" << node.id << " revoked times:" << ++i << endl;});timer->AddTimer(1000, [&](const TimerNode &node) {cout << Timer::GetTick() << " node id:" << node.id << " revoked times:" << ++i << endl;});timer->AddTimer(3000, [&](const TimerNode &node) {cout << Timer::GetTick() << " node id:" << node.id << " revoked times:" << ++i << endl;});auto node = timer->AddTimer(2100, [&](const TimerNode &node) {cout << Timer::GetTick() << " node id:" << node.id << " revoked times:" << ++i << endl;});timer->DelTimer(node);cout << "now time:" << Timer::GetTick() << endl;epoll_event ev[64] = {0};while (true) {int n = epoll_wait(epfd, ev, 64, timer->TimeToSleep());time_t now = Timer::GetTick();for (int i = 0; i < n; i++) {/**/}/* 处理定时事件*/timer->HandleTimer(now);}return 0;
}
1. 定义基本定时器节点
struct TimerNodeBase {time_t expire;int64_t id;
};

TimerNodeBase 结构体存储了定时任务的过期时间和唯一标识符(ID)。

2. 定义定时器节点
struct TimerNode : public TimerNodeBase {using Callback = std::function<void(const TimerNode &node)>;Callback func;TimerNode(int64_t id, time_t expire, Callback func) : func(func) {this->expire = expire;this->id = id;}
};

TimerNode 结构体继承自 TimerNodeBase,并且包含一个回调函数类型 Callback,用于在定时器到期时执行。构造函数初始化过期时间和 ID。

3. 定义比较运算符 
bool operator < (const TimerNodeBase &lhd, const TimerNodeBase &rhd) {if (lhd.expire < rhd.expire)return true;else if (lhd.expire > rhd.expire) return false;return lhd.id < rhd.id;
}

这个运算符重载用于将 TimerNodeBase 对象进行排序,首先比较过期时间,若相同则比较 ID。这对于在容器中管理定时任务的顺序至关重要。

4. 定义定时器类
class Timer {
public:static time_t GetTick() {auto sc = chrono::time_point_cast<chrono::milliseconds>(chrono::steady_clock::now());auto temp = chrono::duration_cast<chrono::milliseconds>(sc.time_since_epoch());return temp.count();}

Timer 类的 GetTick 方法返回当前时间的毫秒数。使用 chrono 库可以精确获取系统的当前时间。

5. 添加定时器
TimerNodeBase AddTimer(time_t msec, TimerNode::Callback func) {time_t expire = GetTick() + msec;if (timeouts.empty() || expire <= timeouts.crbegin()->expire) {auto pairs = timeouts.emplace(GenID(), expire, std::move(func));return static_cast<TimerNodeBase>(*pairs.first);}auto ele = timeouts.emplace_hint(timeouts.crbegin().base(), GenID(), expire, std::move(func));return static_cast<TimerNodeBase>(*ele);
}
  • 该方法用于添加定时任务,接收任务的延迟时间(毫秒)和回调函数。
  • 计算出过期时间 expire,如果时间容器 timeouts 为空或者新的过期时间早于容器中最后一个任务的过期时间,则直接添加到容器。
  • 否则,使用 emplace_hint 在适当的位置插入新任务,以提高性能。
  • 返回添加的定时任务的基本信息。
6. 删除定时器
bool DelTimer(TimerNodeBase &node) {auto iter = timeouts.find(node);if (iter != timeouts.end()) {timeouts.erase(iter);return true;}return false;
}
  • 此方法用于删除已存在的定时任务。使用 find 方法查找并删除,如果成功,返回 true
7. 处理定时器
void HandleTimer(time_t now) {auto iter = timeouts.begin();while (iter != timeouts.end() && iter->expire <= now) {iter->func(*iter);iter = timeouts.erase(iter);}
}
  • 遍历定时任务容器,执行所有已到期的定时任务的回调函数,并将其从容器中移除。
8. 计算休眠时间
time_t TimeToSleep() {auto iter = timeouts.begin();if (iter == timeouts.end()) {return -1;}time_t diss = iter->expire - GetTick();return diss > 0 ? diss : 0;
}
  • 计算下一个定时任务的到期时间,与当前时间的差值,如果没有定时任务,返回 -1,表示可以一直阻塞。
9. ID 生成器
private:static int64_t GenID() {return gid++;}static int64_t gid;set<TimerNode, std::less<>> timeouts;
};int64_t Timer::gid = 0;
  • 生成唯一的 ID,使用静态变量 gid 进行自增。定时任务存储在 set 中,以确保按过期时间自动排序。
10. 主函数
int main() {int epfd = epoll_create(1);unique_ptr<Timer> timer = make_unique<Timer>();
  • 创建一个 epoll 实例,用于事件通知和处理。
  • 使用 unique_ptr 动态分配 Timer 对象。
11. 添加定时任务
    int i = 0;timer->AddTimer(1000, [&](const TimerNode &node) {cout << Timer::GetTick() << " node id:" << node.id << " revoked times:" << ++i << endl;});
  • 添加多个定时任务,每个任务在 1000 毫秒后触发,打印信息并增加计数。
12. 删除定时任务
    auto node = timer->AddTimer(2100, [&](const TimerNode &node) {cout << Timer::GetTick() << " node id:" << node.id << " revoked times:" << ++i << endl;});timer->DelTimer(node);
  • 添加一个定时任务并立即删除它,确保不会执行。
13. 进入事件循环
    cout << "now time:" << Timer::GetTick() << endl;epoll_event ev[64] = {0};while (true) {int n = epoll_wait(epfd, ev, 64, timer->TimeToSleep());time_t now = Timer::GetTick();for (int i = 0; i < n; i++) {/**/}/* 处理定时事件*/timer->HandleTimer(now);}
  • 进入一个无限循环,调用 epoll_wait 等待事件。
  • 第四个参数 timer->TimeToSleep() 计算下一个即将到期的定时任务的休眠时间,从而使线程在此时间段内阻塞,降低 CPU 使用率。
  • 当有事件触发(在这里可以添加处理逻辑),同时调用 HandleTimer 方法处理到期的定时任务。

 实现了一个基本的定时器,利用 epoll 处理多路复用,能有效管理定时任务的添加、删除和执行。通过合理设计容器和优化处理逻辑,确保定时器高效且响应迅速。

总结

定时器方案的设计需要在高效管理大量定时任务和及时触发任务之间找到平衡。通过选择合适的容器结构和检测触发机制,可以显著提升系统的性能和响应速度。同时,结合异步执行和内核级优化,可以实现高效、可靠的定时任务管理。在实际应用中,需根据具体需求选择最适合的方案,并结合优化策略不断提升系统的整体表现。

 

 参考:

0voice · GitHub

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

【Linux】Linux进程的概念

一、冯诺依曼体系结构 我们常见的计算机&#xff0c;比如笔记本&#xff0c;我们不常见的计算机&#xff0c;比如服务器&#xff0c;大部分都遵循冯诺依曼体系结构。 截至目前&#xff0c;我们所认识的计算机&#xff0c;都是有一个一个独立的硬件组成&#xff1a; 输入单元&a…

兼容多个AI应用接口,支持用户自定义切换AI接口

项目背景 2023年ChatGPT横空出世&#xff0c;给IT行业造成了巨大的反响。我第一次发现这个ChatGPT有着如此神奇的功能&#xff08;智能对话&#xff0c;知识问答&#xff0c;代码生成&#xff0c;逻辑推理等&#xff09;&#xff0c;我感到非常吃惊&#xff01;经过一番学习和…

张养浩,文坛政坛的双重巨匠

张养浩&#xff0c;字希孟&#xff0c;号云庄&#xff0c;又称齐东野人&#xff0c;生于元世祖至元七年&#xff08;公元1270年&#xff09;&#xff0c;卒于元英宗至治三年&#xff08;公元1329年&#xff09;&#xff0c;享年59岁。他是中国元代著名的文学家、政治家&#xf…

文章解读与仿真程序复现思路——电网技术EI\CSCD\北大核心《兼顾参与调频辅助服务的工商业储能电站充放电策略》

本专栏栏目提供文章与程序复现思路&#xff0c;具体已有的论文与论文源程序可翻阅本博主免费的专栏栏目《论文与完整程序》 论文与完整源程序_电网论文源程序的博客-CSDN博客https://blog.csdn.net/liang674027206/category_12531414.html 电网论文源程序-CSDN博客电网论文源…

公私域互通下的新商机探索:链动2+1模式、AI智能名片与S2B2C商城小程序的融合应用

摘要&#xff1a;在数字化时代&#xff0c;公私域流量的有效融合已成为企业获取持续增长动力的关键。本文旨在探讨如何通过链动21模式、AI智能名片以及S2B2C商城小程序源码的综合运用&#xff0c;实现公私域流量的高效互通&#xff0c;进而为门店创造巨大商机。通过分析这些工具…

李宏毅机器学习2023-HW11-Domain Adaptation

文章目录 TaskLinkBaselineSimple BaselineMedium BaselineStrong BaselineBoss Baseline Task Domain Adaptation 通过训练真实图片得到分类模型&#xff0c;并将其应用到涂鸦图片上进行分类&#xff0c;来获得更高的精准度。 Link kaggle colab Baseline Simple Baseli…

12V转100V低压升高压DC/DC电源GRB12-100D-100mA-Uz(0-3V)

特点 ● 效率高达75%以上 ● 1*2英寸标准封装 ● 单电压输出 ● 超高性价比 ● 电压控制输出,输出电压随控制电压的变化而线性变压 ● 工作温度: -40℃~75℃ ● 阻燃封装&#xff0c;满足UL94-V0 要求 ● 温度特性好 ● 可直接焊在PCB 上 应用 GRB 系列模块电源是一…

【计算机网络篇】数据链路层 功能|组帧|流量控制与可靠传输机制

&#x1f9f8;安清h&#xff1a;个人主页 &#x1f3a5;个人专栏&#xff1a;【计算机网络】 &#x1f6a6;作者简介&#xff1a;一个有趣爱睡觉的intp&#xff0c;期待和更多人分享自己所学知识的真诚大学生。 系列文章目录 【计算机网络篇】计算机网络概述 【计算机网络篇…

cmake--set_target_properties

作用 设置某个指定的目标(文件&#xff0c;)的某些属性&#xff1b; 可以设置的属性 设置版本属性 cmake_minimum_required(VERSION 3.10)project(test_set_target_properties)add_library(mylib SHARED src/test.cpp) set_target_properties(mylib PROPERTIES VERSION 1.0.…

使用Kali Linux系统生成木马病毒并实现远程控制计算机

木 马 生成木马病毒并实现远程控制计算机 木马病毒是指隐藏在正常程序中的一段具有特殊功能的恶意代码 通常有控制端和被控制端两个可执行程序。 它通过将自身伪装吸引用户下载执行&#xff0c;向施种木马者提供打开被种者电脑的门户&#xff0c;使施种者可以任意毁坏、窃…

红队攻防 | 凭证获取的10个方法,零基础入门到精通,收藏这一篇就够了

作为红队成员&#xff0c;就像许多其他APT一样&#xff0c;我们须找到通往目标网络和资产的方法&#xff0c;因此要付出大量努力&#xff0c;从我们可以获得的任何资源中获取登录凭证或令牌。 这并不意味着我们只寻找登录特定服务的用户名和密码&#xff0c;在许多情况下&…

500元以内头戴式耳机哪款好?盘点500元以内百元宝藏品牌机型推荐

作为耳机发烧友&#xff0c;我深知一副优质的头戴式耳机都能为我们带来沉浸式的听觉体验&#xff0c;但然而&#xff0c;面对市场上琳琅满目的耳机品牌和型号&#xff0c;500元以内头戴式耳机哪款好&#xff1f;对于这个问题我将为大家盘点500元以内百元宝藏品牌机型推荐无论你…

读构建可扩展分布式系统:方法与实践10最终一致性

1. 最终一致性 1.1. 在一些应用领域&#xff0c;通常谈论的是银行和金融行业&#xff0c;最终一致性根本不合适 1.2. 事实上&#xff0c;最终一致性在银行业已经使用了很多年 1.2.1. 支票需要几天时间才能在你的账户上进行核对&#xff0c;而且你可以轻松地开出比账户余额多的…

【Elasticsearch】-实现向量相似检索

1、http请求方式 如果elasticsearch服务设置账号密码&#xff0c;则在请求的header中添加 Basic Auth 认证 请求方式&#xff1a;Post 请求地址&#xff1a;/index_name/_search 请求body&#xff1a;json格式 {"size": 10, //返回条数"min_score": 0.…

一生一芯 预学习阶段 PA1--RTFSC中“make menuconfig”

Enable debug information 问题如上。 在menuconfig将 Enable debug information 选项打开&#xff0c;编译时会产生什么变化&#xff1f; 从选项上看是打开了支持调试的选项&#xff0c;编译时应该是添加了支持编译的参数&#xff0c;具体看代码。 首先&#xff0c;我们先看…

Tomcat 乱码问题彻底解决

1. 终端乱码问题 找到 tomcat 安装目录下的 conf ---> logging.properties .修改ConsoleHandler.endcoding GBK &#xff08;如果在idea中设置了UTF-8字符集&#xff0c;这里就不需要修改&#xff09; 2. CMD命令窗口设置编码 参考&#xff1a;WIN10的cmd查看编码方式&am…

【hot100-java】【组合总和】

R8-回溯篇 印象题&#xff0c;很基本的回溯 class Solution {void backtrack(List<Integer> state,int target,int[] choices,int start,List<List<Integer>> ret){//子集和等于target&#xff0c;记录解if (target0){ret.add(new ArrayList<>(state)…

Java ----常用类

包装类 包装类的分类 1) 针对八种基本数据类型相应的引用类型—包装类2) 有了类的特点&#xff0c;就可以调用类中的方法。3) 如图 包装类和基本数据的转换 jdk5 前的手动装箱和拆箱方式&#xff0c;装箱&#xff1a;基本类型转包装类型&#xff0c;拆箱&#xff1a;包装类型…

探索丹摩智算平台的奇妙之旅:我的CogVideoX实践实验与深刻体验

&#x1f4dd;个人主页&#x1f339;&#xff1a;Eternity._ &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; ❀CogVideoX &#x1f4d2;1. 初识CogVideoX&#x1f4da;2. 部署与准备&#x1f31e;在丹摩智算平台上创建实例&#x1f338;CogVideoX代码仓库…

Vue3 中组件传递 + css 变量的组合

文章目录 需求效果如下图所示代码逻辑代码参考 需求 开发一个箭头组件&#xff0c;根据父组件传递的 props 来修改 css 的颜色 效果如下图所示 代码逻辑 代码 父组件&#xff1a; <Arrow color"red" />子组件&#xff1a; <template><div class&…