【基于springboot分析Quartz(v2.3.2)的启动流程】

基于springboot分析Quartz(v2.3.2)的启动流程

最近公司的定时任务使用了Quartz框架,在开发中经常出现定任务不执行了的问题,但是我又找不到原因所在,可把我愁坏了。于是我决定看看Quartz框架是怎么调度任务的。(ps:适合用过Quart框架的同学阅读,如果从来没有用过Quartz框架的同学,可以看看我之前的文章【Quartz入门】)

如何定位到关键代码

1.通过控制台打印的关键日志入手

在程序启动时候,可以看到控制台会输出很多quartz相关的日志,从这些日志我们可以定位到quartz框架的初始化关键代码,下面是我本地启动时候打印的日志

2024-03-29T22:14:00.779+08:00  INFO 10044 --- [           main] org.quartz.core.QuartzScheduler          : Scheduler meta-data: Quartz Scheduler (v2.3.2) 'quartzScheduler' with instanceId 'NON_CLUSTERED'
  Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally.
  NOT STARTED.
  Currently in standby mode.
  Number of jobs executed: 0
  Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads.
  Using job-store 'org.springframework.scheduling.quartz.LocalDataSourceJobStore' - which supports persistence. and is not clustered.

2024-03-29T22:14:00.779+08:00  INFO 10044 --- [           main] org.quartz.impl.StdSchedulerFactory      : Quartz scheduler 'quartzScheduler' initialized from an externally provided properties instance.
2024-03-29T22:14:00.779+08:00  INFO 10044 --- [           main] org.quartz.impl.StdSchedulerFactory      : Quartz scheduler version: 2.3.2
2024-03-29T22:14:00.779+08:00  INFO 10044 --- [           main] org.quartz.core.QuartzScheduler          : JobFactory set to: org.springframework.scheduling.quartz.SpringBeanJobFactory@70a898b0
2024-03-29T22:14:01.496+08:00  INFO 10044 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8087 (http) with context path ''
2024-03-29T22:14:01.497+08:00  INFO 10044 --- [           main] o.s.s.quartz.SchedulerFactoryBean        : Starting Quartz Scheduler now

我这儿就通过最后一行的打印(o.s.s.quartz.SchedulerFactoryBean : Starting Quartz Scheduler now)定位到具体的代码中如下,并在此debug

image.png

  1. 可以看到scheduler.start()这行代码肯定是我们一个重要的突破口,从字面意思可以得知,Quartz框架在这个地方就启动了。
  2. 从左下角的堆栈信息可以看到quart启动流程是在context.refresh()阶段调用。

从日志定位到了关键方法,接下来我们就深入到start方法,深入到start方法,下面就看看start的核心逻辑到底在干嘛把

2.在job任务中debug分析上下文

image.png
可以看到第一个栈是SimpleThreadPool的WorkerThread内部类的一个线程,顺腾摸瓜最后定位到关键代码入口
QuartzSchedulerThread.run

分析代码

1.SchedulerFactoryBean.start

通过打印的日志定位到,代码入口SchedulerFactoryBean.start

public void start() throws SchedulerException {
      //首先,检查调度器的状态,如果已经在关闭中(shuttingDown)或已经关闭(closed),则抛出 SchedulerException 异常,表示调度器无法在关闭后重新启动
    if (shuttingDown|| closed) {
        throw new SchedulerException(
                "The Scheduler cannot be restarted after shutdown() has been called.");
    }

    // QTZ-212 : calling new schedulerStarting() method on the listeners
    // right after entering start()
    //调用 notifySchedulerListenersStarting() 方法通知调度器监听器,表示调度器即将启动
    notifySchedulerListenersStarting();
    //如果 initialStart 为 null,说明调度器是第一次启动:
    //设置 initialStart 为当前日期和时间。
    //调用作业存储器的 schedulerStarted() 方法,通知作业存储器调度器已经启动。
    //调用 startPlugins() 方法,启动插件。
    if (initialStart == null) {
        initialStart = new Date();
        this.resources.getJobStore().schedulerStarted();            
        startPlugins();
    } else {
    //如果 initialStart 不为 null,说明调度器已经启动过:
    //调用作业存储器的 schedulerResumed() 方法,通知作业存储器调度器已经恢复运行。
        resources.getJobStore().schedulerResumed();
    }
     //将调度器线程的暂停状态设置为 false,以确保调度器不处于暂停状态。
    schedThread.togglePause(false);

    getLog().info(
            "Scheduler " + resources.getUniqueIdentifier() + " started.");
    //通知调度器监听器调度器已经完全启动。
    notifySchedulerListenersStarted();
}

看到这儿,嘿嘿关键代码又来咯,核心代码this.resources.getJobStore().schedulerStarted();那我们接着分析吧

public void schedulerStarted() throws SchedulerException {
//首先,检查是否为集群模式(调用 isClustered() 方法)。
//如果是集群模式,创建并初始化集群管理线程(ClusterManager)。

//如果指定了 initializersLoader,将其设置为集群管理线程的上下文类加载器。
//调用集群管理线程的 initialize() 方法进行初始化。
    if (isClustered()) {
        clusterManagementThread = new ClusterManager();
        if(initializersLoader != null)
            clusterManagementThread.setContextClassLoader(initializersLoader);
        clusterManagementThread.initialize();
    } else {
        try {
            recoverJobs();
        } catch (SchedulerException se) {
            throw new SchedulerConfigException(
                    "Failure occured during job recovery.", se);
        }
    }
    //初始化触发器
    misfireHandler = new MisfireHandler();
    if(initializersLoader != null)
        misfireHandler.setContextClassLoader(initializersLoader);
    misfireHandler.initialize();
    schedulerRunning = true;
    
    getLog().debug("JobStore background threads started (as scheduler was started).");
}
  1. clusterManagementThread.initialize 判断当前节点是否是集群中目前执行任务节点,是则发送任务调度通知signalSchedulingChangeImmediately

public void run() {
    while (!shutdown) {

        if (!shutdown) {
            long timeToSleep = getClusterCheckinInterval();
            long transpiredTime = (System.currentTimeMillis() - lastCheckin);
            timeToSleep = timeToSleep - transpiredTime;
            if (timeToSleep <= 0) {
                timeToSleep = 100L;
            }

            if(numFails > 0) {
                timeToSleep = Math.max(getDbRetryInterval(), timeToSleep);
            }
            
            try {
                Thread.sleep(timeToSleep);
            } catch (Exception ignore) {
            }
        }

        if (!shutdown && this.manage()) {
            signalSchedulingChangeImmediately(0L);
        }

    }//while !shutdown
}
  1. misfireHandler.initialize主要就是启动一个线程,去查询错过执行的任务,立即发出调度变更的信号signalSchedulingChangeImmediately,并传递最早的新时间(earliestNewTime)。
@Override
public void run() {
    
    while (!shutdown) {

        long sTime = System.currentTimeMillis();

        RecoverMisfiredJobsResult recoverMisfiredJobsResult = manage();

        if (recoverMisfiredJobsResult.getProcessedMisfiredTriggerCount() > 0) {
            signalSchedulingChangeImmediately(recoverMisfiredJobsResult.getEarliestNewTime());
        }

        if (!shutdown) {
            long timeToSleep = 50l;  // At least a short pause to help balance threads
            if (!recoverMisfiredJobsResult.hasMoreMisfiredTriggers()) {
                timeToSleep = getMisfireThreshold() - (System.currentTimeMillis() - sTime);
                if (timeToSleep <= 0) {
                    timeToSleep = 50l;
                }

                if(numFails > 0) {
                    timeToSleep = Math.max(getDbRetryInterval(), timeToSleep);
                }
            }
            
            try {
                Thread.sleep(timeToSleep);
            } catch (Exception ignore) {
            }
        }//while !shutdown
    }
}

signalSchedulingChangeImmediately具体实现:QuartzSchedulerThread.signalSchedulingChange
到这儿,start方法执行已经到底了,维护了QuartzSchedulerThread类变量

public void signalSchedulingChange(long candidateNewNextFireTime) {
    synchronized(sigLock) {
        signaled = true;
        signaledNextFireTime = candidateNewNextFireTime;
        sigLock.notifyAll();
    }
}
总结一下scheduler.start()方法底层核心逻辑
  1. 器群模式实现启动集群线程,检查目前节点状态,如果目前节点可执行任务则标记立即执行任务调度(JobStoreSupport.signalSchedulingChangeImmediately
  2. 启动查询错过的任务线程MisFireHandler,去监听是否有错过的执行任务,有则发送任务调度通知(JobStoreSupport.signalSchedulingChangeImmediately)

上面两个线程都没真正的去调度我们的任务,主要就是维护集群,发送是否要执行任务调度的信号,执行signalSchedulingChangeImmediately方法,此方法修改的就是QuartzSchedulerThread类变量,以及唤醒sigLock锁,说明有其他线程在获取sigLock,做一些事儿,估计就是真正的在做任务调度的事儿了。
接下来就可以分析QuartzSchedulerThread谁在使用sigLock,但是我没有继续分析哈哈,我是转头去job任务debug一下,看一下上下文方法栈找到调度任务的线程

2.QuartzSchedulerThread.run

通过在job任务中debug,定位到核心的run方法,接下来就是分析它在干嘛了

(SchedulerFactoryBean.afterPropertiesSet()中会进行QuartzScheduler的初始化,初始化过程有个重要的成员变量QuartzSchedulerThread这个线程的run方法就是核心所在)

@Override
public void run() {
    int acquiresFailed = 0;

    while (!halted.get()) {
        try {
            // check if we're supposed to pause...
            synchronized (sigLock) {
                while (paused && !halted.get()) {
                    try {
                        // wait until togglePause(false) is called...
                        sigLock.wait(1000L);
                    } catch (InterruptedException ignore) {
                    }

                    // reset failure counter when paused, so that we don't
                    // wait again after unpausing
                    acquiresFailed = 0;
                }

                if (halted.get()) {
                    break;
                }
            }

            // wait a bit, if reading from job store is consistently
            // failing (e.g. DB is down or restarting)..
            if (acquiresFailed > 1) {
                try {
                    long delay = computeDelayForRepeatedErrors(qsRsrcs.getJobStore(), acquiresFailed);
                    Thread.sleep(delay);
                } catch (Exception ignore) {
                }
            }

            int availThreadCount = qsRsrcs.getThreadPool().blockForAvailableThreads();
            if(availThreadCount > 0) { // will always be true, due to semantics of blockForAvailableThreads...

                List<OperableTrigger> triggers;

                long now = System.currentTimeMillis();

                clearSignaledSchedulingChange();
                try {
                    triggers = qsRsrcs.getJobStore().acquireNextTriggers(
                            now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow());
                    acquiresFailed = 0;
                    if (log.isDebugEnabled())
                        log.debug("batch acquisition of " + (triggers == null ? 0 : triggers.size()) + " triggers");
                } catch (JobPersistenceException jpe) {
                    if (acquiresFailed == 0) {
                        qs.notifySchedulerListenersError(
                            "An error occurred while scanning for the next triggers to fire.",
                            jpe);
                    }
                    if (acquiresFailed < Integer.MAX_VALUE)
                        acquiresFailed++;
                    continue;
                } catch (RuntimeException e) {
                    if (acquiresFailed == 0) {
                        getLog().error("quartzSchedulerThreadLoop: RuntimeException "
                                +e.getMessage(), e);
                    }
                    if (acquiresFailed < Integer.MAX_VALUE)
                        acquiresFailed++;
                    continue;
                }

                if (triggers != null && !triggers.isEmpty()) {

                    now = System.currentTimeMillis();
                    long triggerTime = triggers.get(0).getNextFireTime().getTime();
                    long timeUntilTrigger = triggerTime - now;
                    while(timeUntilTrigger > 2) {
                        synchronized (sigLock) {
                            if (halted.get()) {
                                break;
                            }
                            if (!isCandidateNewTimeEarlierWithinReason(triggerTime, false)) {
                                try {
                                    // we could have blocked a long while
                                    // on 'synchronize', so we must recompute
                                    now = System.currentTimeMillis();
                                    timeUntilTrigger = triggerTime - now;
                                    if(timeUntilTrigger >= 1)
                                        sigLock.wait(timeUntilTrigger);
                                } catch (InterruptedException ignore) {
                                }
                            }
                        }
                        if(releaseIfScheduleChangedSignificantly(triggers, triggerTime)) {
                            break;
                        }
                        now = System.currentTimeMillis();
                        timeUntilTrigger = triggerTime - now;
                    }

                    // this happens if releaseIfScheduleChangedSignificantly decided to release triggers
                    if(triggers.isEmpty())
                        continue;

                    // set triggers to 'executing'
                    List<TriggerFiredResult> bndles = new ArrayList<TriggerFiredResult>();

                    boolean goAhead = true;
                    synchronized(sigLock) {
                        goAhead = !halted.get();
                    }
                    if(goAhead) {
                        try {
                            List<TriggerFiredResult> res = qsRsrcs.getJobStore().triggersFired(triggers);
                            if(res != null)
                                bndles = res;
                        } catch (SchedulerException se) {
                            qs.notifySchedulerListenersError(
                                    "An error occurred while firing triggers '"
                                            + triggers + "'", se);
                            //QTZ-179 : a problem occurred interacting with the triggers from the db
                            //we release them and loop again
                            for (int i = 0; i < triggers.size(); i++) {
                                qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));
                            }
                            continue;
                        }

                    }

                    for (int i = 0; i < bndles.size(); i++) {
                        TriggerFiredResult result =  bndles.get(i);
                        TriggerFiredBundle bndle =  result.getTriggerFiredBundle();
                        Exception exception = result.getException();

                        if (exception instanceof RuntimeException) {
                            getLog().error("RuntimeException while firing trigger " + triggers.get(i), exception);
                            qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));
                            continue;
                        }

                        // it's possible to get 'null' if the triggers was paused,
                        // blocked, or other similar occurrences that prevent it being
                        // fired at this time...  or if the scheduler was shutdown (halted)
                        if (bndle == null) {
                            qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));
                            continue;
                        }

                        JobRunShell shell = null;
                        try {
                            shell = qsRsrcs.getJobRunShellFactory().createJobRunShell(bndle);
                            shell.initialize(qs);
                        } catch (SchedulerException se) {
                            qsRsrcs.getJobStore().triggeredJobComplete(triggers.get(i), bndle.getJobDetail(), CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_ERROR);
                            continue;
                        }

                        if (qsRsrcs.getThreadPool().runInThread(shell) == false) {
                            // this case should never happen, as it is indicative of the
                            // scheduler being shutdown or a bug in the thread pool or
                            // a thread pool being used concurrently - which the docs
                            // say not to do...
                            getLog().error("ThreadPool.runInThread() return false!");
                            qsRsrcs.getJobStore().triggeredJobComplete(triggers.get(i), bndle.getJobDetail(), CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_ERROR);
                        }

                    }

                    continue; // while (!halted)
                }
            } else { // if(availThreadCount > 0)
                // should never happen, if threadPool.blockForAvailableThreads() follows contract
                continue; // while (!halted)
            }

            long now = System.currentTimeMillis();
            long waitTime = now + getRandomizedIdleWaitTime();
            long timeUntilContinue = waitTime - now;
            synchronized(sigLock) {
                try {
                  if(!halted.get()) {
                    // QTZ-336 A job might have been completed in the mean time and we might have
                    // missed the scheduled changed signal by not waiting for the notify() yet
                    // Check that before waiting for too long in case this very job needs to be
                    // scheduled very soon
                    if (!isScheduleChanged()) {
                      sigLock.wait(timeUntilContinue);
                    }
                  }
                } catch (InterruptedException ignore) {
                }
            }

        } catch(RuntimeException re) {
            getLog().error("Runtime error occurred in main trigger firing loop.", re);
        }
    } // while (!halted)

    // drop references to scheduler stuff to aid garbage collection...
    qs = null;
    qsRsrcs = null;
}

上面是 Quartz 中 QuartzSchedulerThread 类的 run() 方法的具体代码。该方法是线程运行的主要逻辑,负责获取触发器并执行作业。

以下是 run() 方法的大致流程:

  1. 定义一个变量 acquiresFailed,用于记录连续获取触发器失败的次数。
  2. 进入一个循环,只要 halted 标志为 false,就会一直执行。
  3. 检查是否需要暂停调度器。
    • 如果需要暂停,进入等待状态,直到调用 togglePause(false) 方法来恢复调度器。
    • 如果 halted 标志为 true,跳出循环。
  4. 如果获取触发器的连续失败次数大于 1,等待一段时间。
    • 等待时间由 computeDelayForRepeatedErrors() 方法计算。
  5. 获取可用的线程数。
  6. 如果有可用线程,则获取下一批触发器并执行作业。
    • 获取触发器时,指定了最大批处理大小和时间窗口。
    • 如果获取触发器过程中发生异常,根据失败次数进行错误处理。
  7. 如果获取到触发器且触发器列表不为空,等待触发器的执行时间到来。
    • 如果期间发生调度器关闭、时间变化等情况,跳出循环。
    • 如果触发器执行时间到达或发生了显著的调度变化,跳出循环。
  8. 如果触发器列表为空,跳过本次循环。
  9. 设置触发器为 “executing” 状态。
  10. 创建 JobRunShell 对象,并初始化。
  • 如果发生异常,标记作业触发指令为 “SET_ALL_JOB_TRIGGERS_ERROR”。
  1. 在线程池中运行 JobRunShell
  • 如果返回值为 false,表示调度器已关闭或存在线程池的问题,进行相应的错误处理。
  1. 继续下一次循环,获取并执行下一批触发器。
  2. 如果没有可用线程,继续下一次循环。
  3. 计算随机的空闲等待时间,并等待一段时间。
  • 如果调度计划发生变化,提前结束等待。
  1. 在循环中捕获并处理 RuntimeException 异常。
  2. halted 标志为 true,跳出循环。
  3. 清除对调度器资源的引用,以便垃圾回收。

总结

通过启动日志、以及在任务中debug,反向推理出Quartz在springboot中的启动流程,以及Quartz框架调度任务的核心逻辑。授人以鱼不如授人以渔,希望本篇文章不仅仅能帮助大家理解Quartz,还能帮助大家学会去阅读框架源码。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于单片机模糊算法温度控制系统设计

**单片机设计介绍&#xff0c; 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机模糊算法温度控制系统设计是一个综合性的项目&#xff0c;结合了单片机技术、传感器技术、模糊控制算法等多个方面。以下是对该设计的概要…

162.乐理基础-和声大调、旋律大调

内容参考于&#xff1a; 三分钟音乐社 上一个内容&#xff1a;161.音程、和弦板块总结、重点、建议 首先需要回忆一下18.调式、自然大调式&#xff08;C大调、D大调。。。&#xff09;与19.音阶是什么、有什么用&#xff0c;在18.调式、自然大调式&#xff08;C大调、D大调。…

openPLC_Editor C语言编程 在mp157 arm板上调用io等使用记录

1.编程界面比较简单&#xff0c;具备PLC开发编程的四种编程方式。梯形图语言LD &#xff0c;指令表语言IL&#xff0c;结构化文本语言ST&#xff0c;功能模块图语言FBD。 2.官方使用手册。学习资料实在是太少&#xff0c;目前都是自己比较费劲的研究。 3.2 Creating Your First…

UE5 SQLite笔记

开发环境&#xff1a; 系统&#xff1a;Windows 10 64 bit 引擎&#xff1a;Unreal Engine 5.1.1 IDE&#xff1a;JetBrains Rider 2023.2.1 语言&#xff1a;C 工具&#xff1a;DB Browser for SQLite SQLite数据类型&#xff1a; //INTEGER TEXT BLOB REAL NUMERIC/*integer…

家庭网络防御系统搭建-配置流量镜像到NDR系统

由于需要将家庭网络中的全部流量送到NDR分析系统进行分析&#xff0c;因此需要一个具备流量镜像功能的交换机或者路由器。在前面文章所提及的家庭网络架构中&#xff0c;需要一台交换机即可拷贝东西向流量以及南北向流量。当然如果家庭中的路由器或者其他设备具备交换机镜像功能…

EXCEL通过VBA字典快速分类求和

EXCEL通过VBA字典快速分类求和 汇总截图 Option ExplicitOption Explicit Sub answer3() Dim wb As Workbook Dim sht As Worksheet Set wb ThisWorkbook Set sht wb.Worksheets(2) Dim ss1 As Integer Dim ss2 As Integer Dim i As Integer Dim j As Integer j 1Dim aa()…

Moonbeam 开发工具集合:打造 Web3 开发游乐场

原文&#xff1a;https://moonbeam.network/blog/moonbeam-developer-tooling-ecosystem/ 作者&#xff1a;Moonbeam 团队 编译&#xff1a;OneBlock Moonbeam 一直以来都在支持以太坊和 Dotsama 生态系统中的构建者和开发者。它特别为 Solidity 开发者提供了熟悉的工具&…

vue2处理跨域问题

vue中访问springboot中的RestController中的服务 &#xff08;vue.config.js不生效-CSDN博客&#xff09; 1、创建项目 使用vue init webpack my_frontend 创建vue项目 在HelloWorld.vue文件中添加内容&#xff1a; HelloWorld.vue 文件内容&#xff1a; <template>&…

react Audio 倒计时5秒,每秒播放一次音频

文章目录 1. react 倒计时 每秒播放一次音频简单demo代码2. 问题及处理方式2.1 Audio 引入出现的报错2.2 解决方法 1. react 倒计时 每秒播放一次音频简单demo代码 import React, { useState,useRef } from react; import redBagMp3 from /branch/assets/mp3/redBag.mp3 const…

Swift:“逻辑运算子“与“比较运算符“

1. 逻辑非 ! 逻辑非运算符 ! 是用于对布尔值取反的。当操作数为 true 时&#xff0c;! 将返回 false&#xff0c;而当操作数为 false 时&#xff0c;! 将返回 true。 let isTrue true let isFalse !isTrue // isFalse 现在是 false 2. 逻辑与 && 逻辑与运算符 &a…

spring-boot之接口文档Swagger配置使用

Swagger 前后端分离 Vue SpringBoot 后端时代:前端只用管理静态页面; html> 后端。模板引擎JSP >后端是主力 前后端分离式时代: ●后端:后端控制层&#xff0c;服务层,数据访问层[后端团队] ●前端:前端控制层&#xff0c;视图层[前端团队] 。伪造后端数据&#xff0c;…

Oracle Cloud公布 | 每小时 126 亿次 SQL 数据库查询

广而告之&#xff1a;2024 年数据技术嘉年华大会将于 4 月12-13 日在北京召开&#xff0c;春暖花开之际&#xff0c;数据库行业蓬勃发展之时&#xff0c;广邀天下豪杰&#xff0c;相聚北京&#xff0c;共论数据库技术发展的创新与未来。 注册&#xff1a;https://www.modb.pro/…

链表合集(easy难度)

合并两个有序链表 双指针法 由于list1和list2都是递增的&#xff0c;可以想到用双指针法。假如当前list1这个指针指向的节点被收入完成&#xff0c;那就list1&#xff1b;如果是list2被收入&#xff0c;那就list2。 具体是list1和节点被收入还是list2的节点被收入&#xff…

Java NIO详解

一、概念 NIO, 即new io&#xff0c;也叫非阻塞io 二、NIO三个核心组件&#xff1a; Buffer数据缓冲区Channel通道Selector选择器 1、Buffer缓冲区 缓冲区本质上是一个可以存放数据的内存块&#xff08;类似数组&#xff09;&#xff0c;可以在这里进行数据写入和读取。此…

webpack项目打包console git分支、打包时间等信息 exec

相关链接 MDN toLocaleString child_process Node.js strftime 格式 代码 buildinfo.js const { execSync, exec } require("child_process"); // exec: 在 Windows 执行 bat 和 cmd 脚本// execSync 同步 // exec 异步// exec 使用方法 // exec(git show -s,…

notepad++里安装32位和64位的16进制编辑器Hex-Editor

这个16进制编辑器确实是个好东西&#xff0c;平时工作种会经常用到&#xff0c; 这是hex-editor的官网。这个里边只能下载32位的(64位的看最下边)&#xff0c;选一个合适的版本&#xff0c;我当时选的是最新的版本 https://sourceforge.net/projects/npp-plugins/files/Hex%20E…

[机器学习]练习KNN算法-曼哈顿距离

曼哈顿距离(Manhattan distance) 曼哈顿距离是指在几何空间中两点之间的距离&#xff0c;其计算方法是通过将两点在各个坐标轴上的差值的绝对值相加得到。在二维空间中&#xff0c;曼哈顿距离可以表示为两点在横纵坐标上的差值的绝对值之和&#xff1b;在三维空间中&#xff0…

物联网实战--入门篇之(三)嵌入式STM32

目录 一、Keil简介 二、工程结构 三、文件目录 四、STM32简介 五、编码风格 六、总结 一、Keil简介 Keil是一款常用的单片机开发工具&#xff0c;主要包含了编译、仿真、调试和开发界面(IDE)&#xff0c;后被ARM公司收购&#xff0c;与其MDK-ARM合并为MDK-ARM Keil软件包…

如何用 C++ 部署深度学习模型?

深度学习模型在诸多领域如图像识别、自然语言处理、语音识别等展现出强大的应用潜力。然而&#xff0c;模型训练与实际部署是两个不同的环节&#xff0c;许多开发者在使用Python进行模型训练后&#xff0c;出于性能、集成便利性或特定平台要求等因素&#xff0c;会选择使用C进行…

[机器学习]练习-KNN算法

1&#xff0e;&#x1d458;近邻法是基本且简单的分类与回归方法。&#x1d458;近邻法的基本做法是&#xff1a;对给定的训练实例点和输入实例点&#xff0c;首先确定输入实例点的&#x1d458;个最近邻训练实例点&#xff0c;然后利用这&#x1d458;个训练实例点的类的多数来…