基于 golang 从零到一实现时间轮算法 (二)
Go实现单机版时间轮
上一章介绍了时间轮的相关概念,接下来我们会使用 golang 标准库的定时器工具 time ticker 结合环状数组的设计思路,实现一个单机版的单级时间轮。
首先我们先运行一下下面的源码,看一下如何使用。
https://github.com/xiaoxuxiansheng/timewheel
package mainimport ("container/list""fmt""sync""time"
)type taskElement struct {task func()pos intcycle intkey string
}type TimeWheel struct {sync.Onceinterval time.Durationticker *time.Tickerstopc chan struct{}addTaskCh chan *taskElementremoveTaskCh chan stringslots []*list.ListcurSlot intkeyToETask map[string]*list.Element
}func NewTimeWheel(slotNum int, interval time.Duration) *TimeWheel {if slotNum <= 0 {slotNum = 10}if interval <= 0 {interval = time.Second}t := TimeWheel{interval: interval,ticker: time.NewTicker(interval),stopc: make(chan struct{}),keyToETask: make(map[string]*list.Element),slots: make([]*list.List, 0, slotNum),addTaskCh: make(chan *taskElement),removeTaskCh: make(chan string),}for i := 0; i < slotNum; i++ {t.slots = append(t.slots, list.New())}go t.run()return &t
}func (t *TimeWheel) Stop() {t.Do(func() {t.ticker.Stop()close(t.stopc)})
}func (t *TimeWheel) AddTask(key string, task func(), executeAt time.Time) {pos, cycle := t.getPosAndCircle(executeAt)t.addTaskCh <- &taskElement{pos: pos,cycle: cycle,task: task,key: key,}
}func (t *TimeWheel) RemoveTask(key string) {t.removeTaskCh <- key
}func (t *TimeWheel) run() {defer func() {if err := recover(); err != nil {// ...}}()for {select {case <-t.stopc:returncase <-t.ticker.C:t.tick()case task := <-t.addTaskCh:t.addTask(task)case removeKey := <-t.removeTaskCh:t.removeTask(removeKey)}}
}func (t *TimeWheel) tick() {list := t.slots[t.curSlot]defer t.circularIncr()t.execute(list)
}func (t *TimeWheel) execute(l *list.List) {// 遍历每个 listfor e := l.Front(); e != nil; {taskElement, _ := e.Value.(*taskElement)if taskElement.cycle > 0 {taskElement.cycle--e = e.Next()continue}// 执行任务go func() {defer func() {if err := recover(); err != nil {// ...}}()taskElement.task()}()// 执行任务后,从时间轮中删除next := e.Next()l.Remove(e)delete(t.keyToETask, taskElement.key)e = next}
}func (t *TimeWheel) getPosAndCircle(executeAt time.Time) (int, int) {delay := int(time.Until(executeAt))cycle := delay / (len(t.slots) * int(t.interval))pos := (t.curSlot + delay/int(t.interval)) % len(t.slots)return pos, cycle
}func (t *TimeWheel) addTask(task *taskElement) {list := t.slots[task.pos]if _, ok := t.keyToETask[task.key]; ok {t.removeTask(task.key)}eTask := list.PushBack(task)t.keyToETask[task.key] = eTask
}func (t *TimeWheel) removeTask(key string) {eTask, ok := t.keyToETask[key]if !ok {return}delete(t.keyToETask, key)task, _ := eTask.Value.(*taskElement)_ = t.slots[task.pos].Remove(eTask)
}func (t *TimeWheel) circularIncr() {t.curSlot = (t.curSlot + 1) % len(t.slots)
}func main() {timeWheel := NewTimeWheel(10, 500*time.Millisecond)defer timeWheel.Stop()fmt.Println(time.Now())timeWheel.AddTask("test1", func() {fmt.Printf("test1, %v\n", time.Now())}, time.Now().Add(time.Second))timeWheel.AddTask("test2", func() {fmt.Printf("test2, %v\n", time.Now())}, time.Now().Add(5*time.Second))timeWheel.AddTask("test2", func() {fmt.Printf("test2, %v\n", time.Now())}, time.Now().Add(3*time.Second))<-time.After(10 * time.Second)
}
运行结果如下:
2023-11-03 13:02:57.042834 +0800 CST m=+0.000173292
test1, 2023-11-03 13:02:58.043555 +0800 CST m=+1.000891376
test2, 2023-11-03 13:03:00.043567 +0800 CST m=+3.000897126
结果说明,首先添加test1任务定时1秒钟,test2任务定时5秒钟,但后续修改了test2定时为3秒钟,所以输出test1和test2的时间差为2秒钟。
数据结构
在对时间轮的类定义中,核心字段如下图所示:
type TimeWheel struct {sync.Onceinterval time.Durationticker *time.Tickerstopc chan struct{}addTaskCh chan *taskElementremoveTaskCh chan stringslots []*list.ListcurSlot intkeyToETask map[string]*list.Element
}
在几个核心字段中:
- slots——类似于时钟的表盘
- curSlot——类似于时钟的指针
- ticker 是使用 golang标准库的定时器工具,类似于驱动指针运转的齿轮
在创建时间轮实例时,会通过一个异步的常驻 goroutine 执行定时任务的检索、添加、删除等操作,并通过几个 channel 进行 goroutine 的执行逻辑和生命周期的控制:
- stopc:用于停止 goroutine
- addTaskCh:用于接收创建定时器指令
- removeTaskCh:用于接收删除定时任务的指令
此处有几个技术细节需要提及:
首先:所谓环状数组指的是逻辑意义上的. 在实际的实现过程中,会通过一个定长数组结合循环遍历的方式,来实现这个逻辑意义上的“环状”性质.(有点类似于上一章提到的cycle)
其次:数组每一轮能表达的时间范围是固定的. 每当在添加添加一个定时任务时,需要根据其延迟的相对时长推算出其所处的 slot 位置,其中可能跨遍历轮次的情况,这时候需要额外通过定时任务中的 cycle 字段来记录这一信息,避免定时任务被提前执行.
最后:时间轮中一个 slot 可能需要挂载多笔定时任务,因此针对每个 slot,需要采用 golang 标准库 container/list 中实现的双向链表进行定时任务数据的存储.
定时任务
我们现在先看一笔任务的结构体介绍:
// 封装了一笔定时任务的明细信息
type taskElement struct {// 内聚了定时任务执行逻辑的闭包函数task func()// 定时任务挂载在环状数组中的索引位置pos int// 定时任务的延迟轮次. 指的是 curSlot 指针还要扫描过环状数组多少轮,才满足执行该任务的条件cycle int// 定时任务的唯一标识键key string
}
-
task func(): 这是一个函数类型的字段,它引用了一个闭包。闭包是一种匿名函数,能够捕获到其外部作用域中的变量。在这里,task字段代表着定时任务的执行逻辑本身。当定时器触发时,这个闭包会被执行。这样设计可以让taskElement持有执行任务所需要进行的任何操作,使任务逻辑高度内聚和独立。
-
pos int: 该字段表示任务在环形数组(通常用于实现时间轮定时器)中的位置索引。环形数组是时间轮算法中的一种数据结构,用来表示时间的流逝。pos就是这个任务在这个环中的具体位置,当时间轮的指针指向这个位置时,就意味着这个taskElement代表的定时任务可能需要被执行。
-
cycle int: 在时间轮算法中,cycle用于表示任务延迟的轮次数。时间轮有一个当前指针curSlot,每当curSlot遍历一次完整的环形数组,所有任务的cycle值都会减1。一个任务的cycle值指示了curSlot需要再经过多少完整的遍历,该任务才会被执行。当cycle为0时,表示定时任务在当前轮次达到了执行条件。
-
key string: 这个字段是每个定时任务的唯一标识。key的存在允许任务在全局范围内被唯一标识和引用。这意味着你可以使用这个key来查询或者操作特定的定时任务,比如更新任务的延迟时间、取消任务或者是在任务被执行之前获取任务的状态。
综上所述,taskElement结构体将一个定时任务的执行逻辑、在时间轮中的位置、剩余的延迟轮次以及唯一标识符组合在一起,为定时任务的调度提供了必要的信息。
构造器
在创建时间轮的构造器函数中,需要传入两个入参:
- slotNum:由使用方指定 slot 的个数,默认为 10
- interval:由使用方指定每个 slot 对应的时间范围,默认为 1 秒
初始化时间轮实例的过程中,会完成定时器 ticker 以及各个 channel 的初始化,并针对数组 中的各个 slot 进行初始化,每个 slot 位置都需要填充一个 list.
每个时间轮实例都会异步调用 run 方法,启动一个常驻 goroutine 用于接收和处理定时任务.
// 创建单机版时间轮 slotNum——时间轮环状数组长度 interval——扫描时间间隔
func NewTimeWheel(slotNum int, interval time.Duration) *TimeWheel {// 环状数组长度默认为 10if slotNum <= 0 {slotNum = 10}// 扫描时间间隔默认为 1 秒if interval <= 0 {interval = time.Second}// 初始化时间轮实例t := TimeWheel{interval: interval,ticker: time.NewTicker(interval),stopc: make(chan struct{}),keyToETask: make(map[string]*list.Element),slots: make([]*list.List, 0, slotNum),addTaskCh: make(chan *taskElement),removeTaskCh: make(chan string),}for i := 0; i < slotNum; i++ {t.slots = append(t.slots, list.New())}// 异步启动时间轮常驻 goroutinego t.run()return &t
}
构造函数比较简单,由于异步run启动时间轮常驻 goroutine,所以我们现在看看run方法。
启动
时间轮运行的核心逻辑位于 timeWheel.run 方法中,该方法会通过 for 循环结合 select 多路复用的方式运行,属于 golang 中非常常见的异步编程风格.
goroutine 运行过程中需要从以下四类 channel 中接收不同的信号,并进行逻辑的分发处理:
- stopc:停止时间轮,使得当前 goroutine 退出
- ticker:接收到 ticker 的信号说明时间由往前推进了一个 interval,则需要批量检索并执行当前 slot 中的定时任务. 并推进指针 curSlot 往前偏移
- addTaskCh:接收创建定时任务的指令
- removeTaskCh:接收删除定时任务的指令
此处值得一提的是,后续不论是创建、删除还是检索定时任务,都是通过这个常驻 goroutine 完成的,因此在访问一些临界资源的时候,不需要加锁,因为不存在并发访问的情况
// 运行时间轮
func (t *TimeWheel) run() {defer func() {if err := recover(); err != nil {// ...}}()// 通过 for + select 的代码结构运行一个常驻 goroutine 是常规操作for {select {// 停止时间轮case <-t.stopc:return// 接收到定时信号case <-t.ticker.C:// 批量执行定时任务t.tick()// 接收创建定时任务的信号case task := <-t.addTaskCh:t.addTask(task)// 接收到删除定时任务的信号case removeKey := <-t.removeTaskCh:t.removeTask(removeKey)}}
}
停止
时间轮提供了一个 Stop 方法,用于手动停止时间轮,回收对应的 goroutine 和 ticker 资源.
停止时间轮的操作是通过关闭 stopc channel 完成的,由于 channel 不允许被反复关闭,因此这里通过 sync.Once 保证该逻辑只被调用一次.
// 停止时间轮
func (t *TimeWheel) Stop() {// 通过单例工具,保证 channel 只能被关闭一次,避免 panict.Do(func() {// 定制定时器 tickert.ticker.Stop()// 关闭定时器运行的 stopcclose(t.stopc)})
}
创建任务
创建一笔定时任务的核心步骤如下:
- 使用方往 addTaskCh 中投递定时任务,由常驻 goroutine 接收定时任务
- 根据执行时间,推算出定时任务所处的 slot 位置以及需要延迟的轮次 cycle
- 将定时任务包装成一个 list node,追加到对应 slot 位置的 list 尾部
- 以定时任务唯一键为 key,list node 为 value,在 keyToETask map 中建立映射关系,方便后续删除任务时使用
我们首先看一下源码,然后再看相应的图解。
AddTask
// 添加定时任务到时间轮中
func (t *TimeWheel) AddTask(key string, task func(), executeAt time.Time) {// 根据执行时间推算得到定时任务从属的 slot 位置,以及需要延迟的轮次pos, cycle := t.getPosAndCircle(executeAt)// 将定时任务通过 channel 进行投递t.addTaskCh <- &taskElement{pos: pos,cycle: cycle,task: task,key: key,}
}
pos, cycle := t.getPosAndCircle(executeAt): 这行代码调用了TimeWheel的另一个方法getPosAndCircle,传入期望执行的时间executeAt。这个方法计算出任务应该放置在时间轮的哪个槽位上(pos),以及在任务第一次执行前,时间轮需要转过多少完整的圈数(cycle)。
t.addTaskCh <- &taskElement{: 这是Go语言的通道(channel)操作。它创建了一个taskElement结构体实例,并通过TimeWheel中的addTaskCh通道发送出去。这种方式通常用于跨goroutine的安全通信,意味着AddTask方法将定时任务提交到另一个可能在不同goroutine中运行的执行上下文。
- pos: pos,: 设置taskElement的pos字段,表示这个任务在时间轮的哪一个位置。
- cycle: cycle,: 设置taskElement的cycle字段,表示任务在能被执行前时间轮需要转动多少圈。
- task: task,: 将外部传入的任务闭包task赋给taskElement。
- key: key,: 将任务的唯一标识符key赋给taskElement。
getPosAndCircle
// 根据执行时间推算得到定时任务从属的 slot 位置,以及需要延迟的轮次
func (t *TimeWheel) getPosAndCircle(executeAt time.Time) (int, int) {delay := int(time.Until(executeAt))// 定时任务的延迟轮次cycle := delay / (len(t.slots) * int(t.interval))// 定时任务从属的环状数组 indexpos := (t.curSlot + delay/int(t.interval)) % len(t.slots)return pos, cycle
}
为了举例说明这个函数如何工作,我们需要设定一些参数:
- 假设时间轮TimeWheel的slots有60个槽位,代表一分钟内的每一秒(len(t.slots) = 60)。
- 时间轮的每个槽位对应1秒钟(t.interval = 1秒)。
- 假设当前时间轮的指针curSlot在第0槽位上(t.curSlot = 0),这通常表示整点时刻。
- 设定一个将来的时间点executeAt,假设这个时间点是从现在开始的第62秒后。这意味着我们希望在1分钟2秒后执行任务(delay = 62秒)。
// 从现在开始到执行时间的延迟时间(秒)
delay := int(time.Until(executeAt)) // delay = 62// 计算定时任务需要经过的完整时间轮循环数
cycle := delay / (len(t.slots) * int(t.interval))
// cycle = 62 / (60 * 1) = 1.033,向下取整为 1// 计算定时任务应该位于的槽位(数组index)
pos := (t.curSlot + delay/int(t.interval)) % len(t.slots)
// pos = (0 + 62/1) % 60 = 62 % 60 = 2
所以,函数getPosAndCircle将会返回(2, 1):
假设时间轮有5个槽位,每个槽位间隔为1秒,并且当前槽位(curSlot)为0。我们需要计算延迟0到11秒的任务对应的槽位(pos)和轮次(cycle)。
- 延迟0秒:槽位0,轮次0
- 延迟1秒:槽位1,轮次0
- 延迟2秒:槽位2,轮次0
- 延迟3秒:槽位3,轮次0
- 延迟4秒:槽位4,轮次0
- 延迟5秒:槽位0,轮次1
- 延迟6秒:槽位1,轮次1
- 延迟7秒:槽位2,轮次1
- 延迟8秒:槽位3,轮次1
- 延迟9秒:槽位4,轮次1
- 延迟10秒:槽位0,轮次2
- 延迟11秒:槽位1,轮次2
现在看一下执行过程。
addTask
// 常驻 goroutine 接收到创建定时任务后的处理逻辑
func (t *TimeWheel) addTask(task *taskElement) {// 获取到定时任务从属的环状数组 index 以及对应的 listlist := t.slots[task.pos]// 倘若定时任务 key 之前已存在,则需要先删除定时任务if _, ok := t.keyToETask[task.key]; ok {t.removeTask(task.key)}// 将定时任务追加到 list 尾部eTask := list.PushBack(task)// 建立定时任务 key 到将定时任务所处的节点t.keyToETask[task.key] = eTask
}
倘若定时任务 key 之前已存在,则需要先删除定时任务,然后重新添加到末尾。这张图很详细的说明执行的过程了。
删除任务
删除一笔定时任务的核心步骤如下:
- 使用方往 removeTaskCh 中投递删除任务的 key,由常驻 goroutine 接收处理
- 从 keyToETask map 中,找到该任务对应的 list node
- 从 keyToETask map 中移除该组 kv 对
- 从对应 slot 的 list 中移除该 list node
// 删除定时任务,投递信号
func (t *TimeWheel) RemoveTask(key string) {t.removeTaskCh <- key
}
// 时间轮常驻 goroutine 接收到删除任务信号后,执行的删除任务逻辑
func (t *TimeWheel) removeTask(key string) {eTask, ok := t.keyToETask[key]if !ok {return}// 将定时任务节点从映射 map 中移除delete(t.keyToETask, key)// 获取到定时任务节点后,将其从 list 中移除task, _ := eTask.Value.(*taskElement)_ = t.slots[task.pos].Remove(eTask)
}
执行定时任务
最后来捋一下最核心的链路——检索并批量执行定时任务的流程.
首先,每当接收到 ticker 信号时,会根据当前的 curSlot 指针,获取到对应 slot 位置挂载的定时任务 list,调用 execute 方法执行其中的定时任务,最后通过 circularIncr 方法推进 curSlot 指针向前移动。
// 常驻 goroutine 每次接收到定时信号后用于执行定时任务的逻辑
func (t *TimeWheel) tick() {// 根据 curSlot 获取到当前所处的环状数组索引位置,取出对应的 listlist := t.slots[t.curSlot]// 在方法返回前,推进 curSlot 指针的位置,进行环状遍历defer t.circularIncr()// 批量处理满足执行条件的定时任务t.execute(list)
}
在 execute 方法中,会对 list 中的定时任务进行遍历:
- 对于 cycle > 0 的定时任务,说明当前还未达到执行条件,需要将其 cycle 值减 1,留待后续轮次再处理
- 对于 cycle = 0 的定时任务,开启一个 goroutine ,执行其中的闭包函数 task,并将其从 list 和 map 中移除
// 执行定时任务,每次处理一个 list
func (t *TimeWheel) execute(l *list.List) {// 遍历 listfor e := l.Front(); e != nil; {// 获取到每个节点对应的定时任务信息taskElement, _ := e.Value.(*taskElement)// 倘若任务还存在延迟轮次,则只对 cycle 计数器进行扣减,本轮不作任务的执行if taskElement.cycle > 0 {taskElement.cycle--e = e.Next()continue}// 当前节点对应定时任务已达成执行条件,开启一个 goroutine 负责执行任务go func() {defer func() {if err := recover(); err != nil {// ...}}()taskElement.task()}()// 任务已执行,需要把对应的任务节点从 list 中删除next := e.Next()l.Remove(e)// 把任务 key 从映射 map 中删除delete(t.keyToETask, taskElement.key)e = next}
}// 每次 tick 后需要推进 curSlot 指针的位置,slots 在逻辑意义上是环状数组,所以在到达尾部时需要从新回到头部
func (t *TimeWheel) circularIncr() {t.curSlot = (t.curSlot + 1) % len(t.slots)
}
总结
看了小徐先生的推文跟B站视频收获很多,也期待后续跟着大佬继续学习。
参考
https://zhuanlan.zhihu.com/p/658079556
https://blog.csdn.net/YouMing_Li/article/details/134089794
相关文章:

基于 golang 从零到一实现时间轮算法 (二)
Go实现单机版时间轮 上一章介绍了时间轮的相关概念,接下来我们会使用 golang 标准库的定时器工具 time ticker 结合环状数组的设计思路,实现一个单机版的单级时间轮。 首先我们先运行一下下面的源码,看一下如何使用。 https://github.com/x…...

【系统架构设计】架构核心知识: 5 系统安全性与保密性设计
目录 一 信息安全基础 1 信息安全的基本要素 2 信息安全的范围 3 网络安全...

无人零售奶柜:革新牛奶购买体验
无人零售奶柜:革新牛奶购买体验 无人零售奶柜的投放地点覆盖了社区、写字楼等靠近居民的场所,大大提升了消费者购买牛奶的体验。这一创新不仅令消费者能够享受到与电商平台相媲美的直供价格优势,还让他们能够购买更多、更丰富的知名品牌牛奶。…...

【Mybatis小白从0到90%精讲】15: Mybatis配置打印SQL日志
文章目录 前言配置日志实现前言 日志(Log)是每个程序都不可或缺的一部分,它可以帮助开发人员诊断和调试问题。Mybatis,作为一款备受赞誉的ORM框架,自然也提供了强大的日志功能。 它不仅提供了内置的标准实现,还支持集成各种主流的日志框架,让我们可以轻松地查看最终执行…...

vue3-video-play视频播放组件
安装: npm i vue3-video-play --save使用说明: https://codelife.cc/vue3-video-play/guide/install.html...

vue项目中页面遇到404报错
vue页面访问正常,但是一刷新就会404的问题解决办法: 1.解决方法: 将vue的路由模式 mode: history 修改为 mode: hash模式 //router.js文件 const router new Router({//mode: history, mode: hash,routes: [{ path: /, redirect: /login …...

快手直播弹幕websocket protobuf序列化与反序列化
系列文章目录 websocket训练地址:https://www.qiulianmao.com,正在搭建中 基础-websocket逆向基础-http拦截基础-websocket拦截基础-base64编码与解码基础-python实现protobuf序列化与反序列化基础-前端js实现protobuf序列化与反序列化基础-protobufjs实现protobuf序列化与反…...

viple入门(三)
(1)条件循环活动 条件循环活动中,必须给定条件,条件成立,则执行条件循环的后续程序。 条件不成立,则不执行后续程序。 从报错信息来看,程序提示:条件循环要和结束循环活动一起使用。…...

Vue渲染函数渲染html
版本 vue2.6 使用 domProps属性 domProps: {innerHTML: xxx},官方文档...

Odoo|“视图”和“模型”之间的数据传输
01前言 今天带领大家学习Odoo系统中“视图”与“模型”之间的数据传输。看题目我们可以知道,这篇文章是面向的是Odoo的初学者。Odoo作为当前最普遍的二开ERP系统,其开源,模块化,灵活开发的属性使得它在ERP相关领域十分受青睐。 …...

Electron进程通信的另一种方式
上一篇讲述了主进程和渲染进程之间的通信,其中是通过调用 ipcMain 和 ipcRenderer 来完成的。比如渲染进程给主进程发送一个消息,然后主进程再返回一个消息给渲染进程: 主进程的逻辑: ipcMain.on(selectDate,(e,date)>{conso…...

二次型的相关理解
...

Spring框架中用于注入构造函数参数的标签constructor-arg
一、constructor-arg的介绍 constructor-arg是Spring框架中用于注入构造函数参数的标签,它可以用于实现依赖注入的方式之一。在实际开发中,我们通常会在Spring配置文件中声明bean的时候使用constructor-arg标签注入构造函数参数。 constructor-arg标签有…...

spdlog简单介绍和使用
spdlog 是一个C的快速、可扩展的日志库,具有高性能和友好的接口。它支持多种日志输出目标,如控制台、文件、远程服务器等,并具有强大的日志格式化和异步日志记录功能。 以下是对spdlog的简单介绍和使用示例: 安装 spdlog 你可以…...

分类模型的Top 1和Top 5
分类模型的Top 1和Top 5 flyfish 模型分类的结果指标如下图 acc top1 和 acc top5这两列 关于Top 1和Top 5分两种 top 1 accuracy top 5 accuracy 和 top 1 error rate top 5 error rate 这里将需要评估的分类器称为模型 假如一共要测试N张图像,一共有1000个类…...

LinkdeList集合
1.LinkdeList集合的特点: 1.LinkedList是集合的一个实现类 2.LinkedList内部封装了一个双向链表 3.LinkedList集合的增删快,查询慢 4.线程不安全 2.LinkedList集合的方法 1.添加 1.boolean add(Object element) 将元素附加到链表末尾 2.boolean add(int…...

KaiOS APN配置文件apn.json调试验证方法(无需项目全编)
1、KaiOS 的应用就类似web应用,结合文件夹路径webapp字面意思理解。 2、KaiOS APN配置文件源代码在apn.json, (1)apn.json可以自定义路径,通过配置脚本实现拷贝APN在编译时动态选择路径在机器中生效。 (…...

【qemu逃逸】HWS2017-FastCP
前言 虚拟机用户名:root 虚拟机密码:无密码 本题有符号,所以对于设备定位啥的就不多说了,直接逆向设备吧。 设备逆向 在 realize 函数中设置一个时钟任务,并且可以看到只注册了 mmio,大小为 0x100000。…...

调节扬声器和麦克风的音量
const audioSrc require("./../../src/assets/music.mp3")// 调节扬声器音量switchYSQColumn(){//使用Audio对象创建一个新的音频元素const audioElement new Audio();//将音频元素的src属性设置为音频文件的urlaudioElement.src audioSrc;audioElement.play()//创…...

XShelll-修改快捷键-xftp-修改编辑器
文章目录 1.XShelll-修改快捷键2.Xftp-修改文本编辑器3.总结 1.XShelll-修改快捷键 工具>选项 鼠标键盘,右键编辑,新建快捷键。 复制粘贴改成shiftc,shiftv。更习惯一些。 2.Xftp-修改文本编辑器 xftp修改服务器文件默认的编辑器,是记…...

编译原理(1)----LL(1)文法(首符号集,后跟符号集,选择符号集)
一.首符号集(First()) 技巧:找最左边可能出现的终结符 例: 1.First(E) E->T,最左边为T,又因为T->F,最左边为F,F->(E)|i,则最左边为{(,i } 2.First(T):只需要看符号串最左…...

远程仓库地址改变后更换url
请按照以下步骤进行操作: 打开终端或命令提示符,并导航到你的本地仓库目录。运行以下命令,查看当前的远程仓库配置:git remote -v 这将显示当前的远程仓库地址。 如果远程仓库地址显示为192.168.1.178,请使用以下命…...

VR全景如何助力乡村振兴,乡村发展在哪些方面用到VR全景技术
引言: 乡村振兴是当今中国发展的重要战略,也是推动农村经济社会全面发展的关键举措。在这一过程中,虚拟现实(VR)全景技术正逐渐崭露头角,为乡村振兴提供了机遇。 一.VR全景技术的概念和应用 1…...

21.6 Python 构建ARP中间人数据包
ARP中间人攻击(ARP spoofing)是一种利用本地网络的ARP协议漏洞进行欺骗的攻击方式,攻击者会向目标主机发送虚假ARP响应包,使得目标主机的ARP缓存中的IP地址和MAC地址映射关系被篡改,从而使得目标主机将网络流量发送到攻…...

DVWA靶场SQL注入
本次注入的是DVWA靶场的SQL injection 1.判断是字符型注入还是数字型注入,构造SQL语句 1 and 12 由此可以判断出为字符型注入 2.考虑闭合方式,先随便丢一个单引号试试看看报错提示 You have an error in your SQL syntax; check the manual that cor…...

AD教程 (九)导线及NetLabel的添加
AD教程 (九)导线及NetLabel的添加 添加导线 绘制导线 点击放置,选择线,或者直接CtrlW快速绘制注意要与绘图工具中的线区别开来,导线是具有电气属性的,绘图工具中的线没有电气属性,只是辅助线绘制导线过程…...

Rust函数进阶
文章目录 函数函数中的函数lambda表达式函数作为参数 Rust系列:初步⚙所有权⚙结构体和枚举类 函数 先来回顾一下Rust中函数的创建过程,在Rust中,函数用fn声明,如有传入参数或返回值,都需要声明数据类型,…...

springboot前后端时间类型传输
springboot前后端时间类型传输 前言1.java使用时间类型java.util.Date2.java使用localDateTime 前言 springboot前后端分离项目总是需要进行时间数据类型的接受和转换,针对打代码过程中不同的类型转化做个总结 1.java使用时间类型java.util.Date springboot的项目中使用了new …...

Deepin安装英特尔AX200 Wifi驱动
谁支持我的 Linux* 驱动程序? Linux 驱动程序是上游 Linux* 内核的一部分。它们通过常规渠道、分发,或 Linux* 内核档案 提供。我们仅支持作为内核版本中一部分的驱动程序的使用。请参阅下表以了解 iwlwifi 驱动程序支持的设备的列表。 在 英特尔 Wi-Fi…...

第十九节——vue内置组件
Vue提供了一些内置的组件,这些组件可以在Vue应用中直接使用,无需额外安装或配置。以下是一些常见的Vue内置组件 一、<transition> 和 <transition-group> 1、概念 <transition> 组件用于在元素插入或移除时应用过渡效果,…...