Kafka中时间轮分析与Java实现

news2024/11/24 11:22:30

仿kafka实现java版时间轮_java实现时间轮算法_Hekliu的博客-CSDN博客

https://www.cnblogs.com/softlin/p/7426083.html

https://blog.csdn.net/happyjacob/article/details/128518700

一、背景

在Kafka中应用了大量的延迟操作但在Kafka中 并没用使用JDK自带的Timer或是DelayQueue用于延迟操作,而是使用自己开发的DelayedOperationPurgatory组件用于管理延迟操作,Kafka这类分布式框架有大量延迟操作并且对性能要求及其高,而java.util.Timer与java.util.concurrent.DelayQueue的插入和删除时间复杂度都为对数阶O(log n)并不能满足Kafka性能要求,所以Kafka实现了基于时间轮的定时任务组件,该时间轮定时任务实现的插入与删除(开始定时器与暂停定时器)的时间复杂度都为常数阶O(1)
  时间轮的应用并不少见,在Netty、akka、Quartz、Zookeeper等高性能组件中都存在时间轮定时器的踪影;

二、时间轮数据结构

2.1 时间轮名词解释:

     时间格:环形结构中用于存放延迟任务的区块;
  指针(CurrentTime):指向当前操作的时间格,代表当前时间
  格数(ticksPerWheel):为时间轮中时间格的个数
  间隔(tickDuration):每个时间格之间的间隔
  总间隔(interval):当前时间轮总间隔,也就是等于ticksPerWheel*tickDuration

  TimingWheel并非简单的环形时间轮,而是多层级时间轮,每个时间轮由多个时间格组成,每个时间格为一个时间间隔,底层的时间格跨度较小,然后随着延迟任务延迟时间的长短逐层变大;如上图,底下的时间轮每个时间格为1ms,整个时间轮为10ms,而上面一层的时间轮中时间格为10ms,整个时间轮为100ms;

  时间轮添加上级时间轮的规则为:当前currentTime为上级时间轮的startMs,当前interval为上级时间轮的tickDuration,每层ticksPerWheel相同;简单点说就是上层时间轮跨度为当前的M倍,时间格为当前的N倍;

在kafka中第一个槽默认一格表示1ms,第一个时间轮是20个槽,所以

  • 第一一个时间轮代表20ms。
  • 第二个时间轮的每一格式第一个时间轮的总时间,也就是20ms,所以第二个时间轮可表示的时间范围是400ms,
  • 依次类推,第三个时间轮可表示的时间范围是8s,
  • 第四个时间轮是160s等等。

由于时间在向前推进,故一段时间后,第二个时间轮上的任务会向转移到第一个时间轮上,这样递进的方式,最终任务都会执行。
kafka中的每个槽表示一个TimerTaskList,每个任务加到这个TimerTaskList上,如下图中时间轮中每个槽都代表一个TimerTaskList。 

三、Kafka中时间轮的实现

3.1 名词解释

  Kafka中时间轮时间类为TimingWheel,该类结构为存储定时任务的环形队列,内部使用数组实现,数组是用于存放TimerTaskList对象,TimerTaskList环形双向链表,链表项TimerTaskEntry封装了定时任务TimerTaskTimerTaskListTimerTaskEntry中均有超时时间字段,TimerTaskdelayMs字段用于记录任务延迟时间;该三个类为Kafka时间轮实现的核心;

  • TimingWheel:表示一个时间轮,通常会有多层时间轮也就存在多个TimingWheel对象;
  • TimerTaskList:为数组对象用于存放延迟任务,一个TimerTaskList就代表一个时间格,一个时间格中能保存的任务到期时间只可在[t~t+10ms]区间(t为时间格到期时间,10ms时间格间格),每个时间格有个过期时间,时间格过期后时间格中的任务将向前移动存入前面时间格中;
  • TimerTask:表示延迟任务;
  • SystemTimer:kafka实现的定时器,内部封装了TimningWheel用于执行、管理定时任务;
  • tickMs:滴答一次的时长,类似于手表的例子中向前推进一格的时间。对于秒针而言,tickMs 就是 1 秒。同理,分针是 1 分,时针是 1 小时。在 Kafka 中,第 1 层时间轮的 tickMs 被固定为 1 毫秒,也就是说,向前推进一格 Bucket 的时长是 1 毫秒。

  • wheelSize:每一层时间轮上的 Bucket 数量。第 1 层的 Bucket 数量是 20。

  • startMs:时间轮对象被创建时的起始时间戳。

  • taskCounter:这一层时间轮上的总定时任务数。

  • queue:将所有 Bucket 按照过期时间排序的延迟队列。随着时间不断向前推进,Kafka 需要依靠这个队列获取那些已过期的 Bucket,并清除它们。

  • interval:这层时间轮总时长,等于滴答时长乘以 wheelSize。以第 1 层为例,interval 就是 20 毫秒。由于下一层时间轮的滴答时长就是上一层的总时长,因此,第 2 层的滴答时长就是 20 毫秒,总时长是 400 毫秒,以此类推。

  • buckets:时间轮下的所有 Bucket 对象,也就是所有 TimerTaskList 对象。

  • currentTime:当前时间戳,只是源码对它进行了一些微调整,将它设置成小于当前时间的最大滴答时长的整数倍。举个例子,假设滴答时长是 20 毫秒,当前时间戳是 123 毫秒,那么,currentTime 会被调整为 120 毫秒。

  • overflowWheel:Kafka 是按需创建上层时间轮的。这也就是说,当有新的定时任务到达时,会尝试将其放入第 1 层时间轮。如果第 1 层的 interval 无法容纳定时任务的超时时间,就现场创建并配置好第 2 层时间轮,并再次尝试放入,如果依然无法容纳,那么,就再创建和配置第 3 层时间轮,以此类推,直到找到适合容纳该定时任务的第 N 层时间轮。

3.2 工作过程

下面通过一个示例来介绍kafka时间轮的工作过程:

  时间轮初始化:初始时间轮中的格数、间隔、指针的初始化时间,创建时间格所对应的buckets数组,计算总间隔interval;
  添加延迟任务:判断该任务是否已被取消、是否已经过期如已过期则把任务放入线程池中执行、根据时间轮总间隔与当前时间判断任务是否可存入当前层级时间轮否则添加上层时间轮并再次尝试往时间轮中添加该任务;

  时间轮降级:有一个定时任务再300ms后将执行,现层级时间轮每层有10个时间格,顶层时间轮的时间格间隔为1ms,整个时间轮为10ms,无法存下该任务。这时创建第二层时间轮,时间格间隔为10ms,整个时间轮为100ms,还是无法存该任务。接着创建第三层时间轮,时间格间隔为100ms,整个时间轮为1000ms,此时任务存入第三层时间轮的第三个时间格中;过了段时间,TimerTaskList到期(时间格)可该任务还有90ms,还无法执行。此时将再次把定时任务添加到时间轮中,顶层时间轮还是无法满足存入条件,往第二层时间轮添加,这时定时任务存入第二层时间轮第九个时间格当中;任务在时间轮中如此反复,直到任务过期时将放入线程池中执行;

3.3 关键实现方法

 public boolean add(TaskEntry e) {
    synchronized (this) {
        long expiration = e.getExpirationMs(); 
        if(expiration<(currentTime+tickDuration)){
            //当前任务过期时间
            LOGGER.info("当前任务已过期");
            return false;
        }else if(expiration<(currentTime+interval)) {
            //查找时间格的位置,过期时间/时间格%时间轮大小
            long virtualId = expiration / tickDuration;
             TaskEntryList taskEntryList = buckets.get((int) (virtualId % ticksPerWheel));
            taskEntryList.add(e); 
            //设置EntryList过期时间
            if(taskEntryList.setTime(virtualId * tickDuration)) { 
                listDelayQueue.offer(taskEntryList);

            }
            return true;
        }else{
            if(overflowWheel==null){ 
 // 添加上级timingWheel
                addOverflowWheel();
            }
            return overflowWheel.add(e);

         }
      }
 }  

 /**
 *时间表针移动
 * @param timeMS
  */
 public void advanceClock(long timeMS){
    if(timeMS>=(currentTime+tickDuration)){
        currentTime=timeMS-(timeMS%tickDuration);
    }
    if (overflowWheel != null) overflowWheel.advanceClock(currentTime);
}

/**
 * 添加定时任务
 * @param taskEntry
 */
public void add(TaskEntry taskEntry) {
    if (!timingWheel.add(taskEntry)) {
        System.out.println(String.format("任务已过期,开始执行 %s",taskEntry.getTimerTask()));
        taskExecutor.execute(taskEntry.getTimerTask());
    }
}

四、java版时间轮的实现

4.1 任务TimerTask源码分析

  • TimingWheel: 时间轮时间类,存储定时任务的环形队列,内部使用数组实现,数组是用于存放TimerTaskList对象,
  • TimerTaskList:环形双向链表
  • TimerTaskEntry:环形双向链表的链表项封装了定时任务TimerTask
  • TimerTask: 表示一个要执行的任务,实现了Runnable接口,TimerTaskdelayMs字段用于记录任务延迟时间

TimerTaskListTimerTaskEntry中均有超时时间字段,该三个类为Kafka时间轮实现的核心。

public abstract class TimerTask implements Runnable {
    public long delayMs; //表示当前任务延迟多久后执行(单位ms),比如说延迟3s,则此值为3000
 
    public TimerTask(long delayMs) {
        this.delayMs =  delayMs;
    }

    // 指向TimerTaskEntry对象,一个TimerTaskEntry包含一个TimerTask,TimerTaskEntry是可复用的
    private TimerTaskList.TimerTaskEntry timerTaskEntry = null;
  
    // 取消当前任务,就是从TimerTaskEntry移出TimerTask,并且把当前的timerTaskEntry置空
    public synchronized void cancel() {
        if(timerTaskEntry != null) {
            timerTaskEntry.remove();
        }
        timerTaskEntry = null;
    }
 
    //设置当前任务绑定的TimerTaskEntry
    public synchronized void setTimerTaskEntry(TimerTaskList.TimerTaskEntry entry) {
        if(timerTaskEntry != null && timerTaskEntry != entry) {
            timerTaskEntry.remove();
        }
        timerTaskEntry = entry;
    }
 
    public TimerTaskList.TimerTaskEntry getTimerTaskEntry() {
        return timerTaskEntry;
    }
}

4.2 任务包装类TimerTaskEntry

TimerTaskEntryTimerTask的包装,实现了Compareable接口,用来比较两个任务的过期时间,以决定任务list插入的顺序。

public static class TimerTaskEntry implements Comparable<TimerTaskEntry>{
	//包含一个任务
	public TimerTask timerTask;
	// 任务的过期时间,此处的过期时间设置的过期间隔+系统当前时间(毫秒)
	public Long expirationMs;
	
	// 当前任务属于哪一个列表
	private TimerTaskList list;
	// 当前任务的上一个任务,用双向列表连接
	private TimerTaskEntry prev;
	private TimerTaskEntry next;


	public TimerTaskEntry(TimerTask timerTask,Long expirationMs) {
		this.timerTask = timerTask;
		this.expirationMs = expirationMs;
		// 传递进来任务TimerTask,并设置TimerTask的包装类
		if(timerTask != null) {
			timerTask.setTimerTaskEntry(this);
		}
	}
  
	// 任务的取消,就是判断任务TimerTask的Entry是否是当前任务
	public boolean cancel() {
		return timerTask.getTimerTaskEntry() != this;
	}
  
   // 任务的移出
	public void remove() {
		TimerTaskList currentList = list;
		while(currentList != null) {
			currentList.remove(this);
			currentList = list;
		}
	}
	// 比较两个任务在列表中的位置,及那个先执行
	@Override
	public int compareTo(TimerTaskEntry that) {
		return Long.compare(expirationMs,that.expirationMs);
	}
}

4.3 每个槽中的任务列表

在时间轮中每个槽代表一个列表,即TimerTaskList,每个TimerTaskList中包含多个TimerTaskEntry,并且用双向列表链接。TimerTaskList实现了Delayed接口,用于返回剩余的时间,把上层时间轮的任务移动位置。

public class TimerTaskList implements Delayed {
    //当前列表中包含的任务数
    private AtomicInteger taskCounter;
    // 列表的头结点
    private TimerTaskEntry root;
    // 过期时间
    private AtomicLong expiration = new AtomicLong(-1L);
 
 
    public TimerTaskList(AtomicInteger taskCounter) {
        this.taskCounter = taskCounter;
        this.root =  new TimerTaskEntry(null,-1L);
        root.next = root;
        root.prev = root;
    }
 
    // 给当前槽设置过期时间
    public boolean setExpiration(Long expirationMs) {
        return expiration.getAndSet(expirationMs) != expirationMs;
    }
 
    public Long getExpiration() {
        return expiration.get();
    }
 
    // 用于遍历当前列表中的任务
    public synchronized  void foreach(Consumer<TimerTask> f) {
        TimerTaskEntry entry = root.next;
        while(entry != root) {
            TimerTaskEntry nextEntry = entry.next;
            if(!entry.cancel()) {
                f.accept(entry.timerTask);
            }
            entry = nextEntry;
        }
    }
  
   // 添加任务到列表中
    public void add(TimerTaskEntry timerTaskEntry) {
        boolean done = false;
        while(!done) {
            //  在添加之前尝试移除该定时任务,保证该任务没有在其他链表中
            timerTaskEntry.remove();
 
            synchronized (this) {
                synchronized (timerTaskEntry) {
                    if(timerTaskEntry.list == null) {
                        TimerTaskEntry tail = root.prev;
                        timerTaskEntry.next = root;
                        timerTaskEntry.prev = tail;
                        timerTaskEntry.list = this;
                        tail.next = timerTaskEntry;
                        root.prev = timerTaskEntry;
                        taskCounter.incrementAndGet();
                        done = true;
                    }
                }
            }
        }
    }
 
    //移出任务
    private synchronized void remove(TimerTaskEntry timerTaskEntry) {
        synchronized (timerTaskEntry) {
            if(timerTaskEntry.list == this) {
                timerTaskEntry.next.prev = timerTaskEntry.prev;
                timerTaskEntry.prev.next = timerTaskEntry.next;
                timerTaskEntry.next = null;
                timerTaskEntry.prev = null;
                timerTaskEntry.list = null;
                taskCounter.decrementAndGet();
            }
        }
    }
 
    public synchronized void flush(Consumer<TimerTaskEntry> f) {
        TimerTaskEntry head = root.next;
        while(head != root) {
            remove(head);
            f.accept(head);
            head = root.next;
        }
         expiration.set(-1L);
    }
    //获得当前任务剩余时间
    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(Math.max(getExpiration() - System.currentTimeMillis(),0),TimeUnit.MICROSECONDS);
    }
    
    @Override
    public int compareTo(Delayed d) {
        TimerTaskList other = (TimerTaskList) d;
        return Long.compare(getExpiration(),other.getExpiration());
    }
}

4.4 时间轮结构

时间轮TimeWheel代表一层时间轮,即第一层时间轮表示20ms,主要功能是添加任务和驱动时间轮向前。

public class TimingWheel {
    private Long tickMs;  //每一个槽表示的时间范围
    private Integer wheelSize; // 时间轮大小,即每一层时间轮的大小
    private Long startMs; // 系统的启动时间
    private AtomicInteger taskCounter;  // 当前层任务数
    private DelayQueue<TimerTaskList> queue; //延迟队列,用于从队列取每个任务列表
 
    private Long interval; //每一层时间轮代表的时间
    private List<TimerTaskList> buckets;  // 每一层的每一个槽中的时间任务列表
    private Long currentTime;  // 修正后的系统启动时间
  
    private TimingWheel overflowWheel = null;  // 上一层时间轮
 
    public TimingWheel(Long tickMs, Integer wheelSize, Long startMs, AtomicInteger taskCounter, DelayQueue<TimerTaskList> queue) {
        this.tickMs = tickMs;
        this.wheelSize = wheelSize;
        this.startMs = startMs;
        this.taskCounter = taskCounter;
        this.queue = queue;
        interval = tickMs * wheelSize;
        currentTime = startMs - (startMs % tickMs); //当前时间,往前推
 
        buckets = new ArrayList<>(wheelSize);
        for(int i = 0;i < wheelSize;i++) {
            buckets.add(new TimerTaskList(taskCounter));  //创建每一个槽中的列表
        }
    }
 
    // 创建上层时间轮
    public synchronized void addOverflowWheel() {
        if(overflowWheel == null) {
            overflowWheel = new TimingWheel(
                    interval,  // 此处interval即表示上一层时间轮表示的范围
                    wheelSize,
                    currentTime,
                    taskCounter,
                    queue
            );
        }
    }
  
  // 添加任务
    public boolean add(TimerTaskList.TimerTaskEntry timerTaskEntry) {
        Long expiration = timerTaskEntry.expirationMs;
       
        Long thisTime = currentTime + tickMs;
        // 任务是否已经取消,取消则返回
        if(timerTaskEntry.cancel()) {
            return false;
        // 当前任务是否已经过期,如果过期则返回false,要立即执行
        }else if(expiration < currentTime + tickMs) {
            return false;
        // 判断当前任务能否在添加到当前时间轮
        }else if(expiration < currentTime + interval) {
           
            Long virtualId = expiration / tickMs;  
            // 计算当前任务要分配在哪个槽中
            int whereBucket = (int) (virtualId % wheelSize);
            TimerTaskList bucket = buckets.get((int)(virtualId % wheelSize));
 
            bucket.add(timerTaskEntry);
 
            long bucketExpiration = virtualId * tickMs;
            //更新槽的过期时间,添加入延迟队列
            if(bucket.setExpiration(virtualId * tickMs)) {
                queue.offer(bucket);
            }
            return true;
        }else {
          //添加任务到高层时间轮
            if(overflowWheel == null) addOverflowWheel();
            return overflowWheel.add(timerTaskEntry);
        }
    }
 
    // 向前驱动时间
    public void advanceClock(Long timeMs) {
        if(timeMs >= currentTime + tickMs) {
            currentTime = timeMs - (timeMs % tickMs);
 
            if(overflowWheel != null) {
                overflowWheel.advanceClock(currentTime);
            }
        }
    }
}

4.5 时间轮接口

  • kafka中提供了Timer接口,用于对外提供调用,分别是
    • Timer#add 添加任务;
    • Timer#advanceClock 驱动时间;
    • Timer#size 时间轮中总任务数;
    • Timer#shutdown 停止时间轮
public interface Timer {
    void add(TimerTask timerTask);
    boolean advanceClock(Long timeoutMs) throws Exception;
    int size();
    void shutdown();
}
  • Timer的实现类是SystemTimer
public class SystemTimer implements Timer {
    private String executorName;
    private Long tickMs = 1L;
    private Integer wheelSize = 20;
    private Long startMs = System.currentTimeMillis();
    //用来执行TimerTask任务
    private ExecutorService taskExecutor =
            Executors.newFixedThreadPool(1,(runnable) -> {
                Thread thread = new Thread(runnable);
                thread.setName("executor-" + executorName);
                thread.setDaemon(false);
                return thread;
            });
    //延迟队列
    private DelayQueue<TimerTaskList> delayQueue = new DelayQueue<>();
    private AtomicInteger taskCounter = new AtomicInteger(0);
    private TimingWheel timingWheel;
 
    private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
    private ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
 
    // 用来执行时间轮的重新排列,及上一个槽中的任务列表被执行后,后面的槽中的任务列表移动
    private Consumer<TimerTaskEntry> reinsert = (timerTaskEntry) -> addTimerTaskEntry(timerTaskEntry);
 
    public SystemTimer(String executorName, Long tickMs, Integer wheelSize, Long startMs) {
        this.executorName = executorName;
        this.tickMs = tickMs;
        this.wheelSize = wheelSize;
        this.startMs = startMs;
        this.timingWheel = new TimingWheel(
                tickMs,
                wheelSize,
                startMs,
                taskCounter,
                delayQueue
        );
    }
 
    // 可能会多个线程操作,所以需要加锁
    @Override
    public void add(TimerTask timerTask) {
        readLock.lock();
        try{
            addTimerTaskEntry(new TimerTaskEntry(timerTask,timerTask.delayMs + System.currentTimeMillis()));
        }finally {
            readLock.unlock();
        }
    }
 
    private void addTimerTaskEntry(TimerTaskEntry timerTaskEntry) {      // 往时间轮添加任务
        if(!timingWheel.add(timerTaskEntry)) {
            // 返回false并且任务未取消,则提交当前任务立即执行。
            if(!timerTaskEntry.cancel()) {
                taskExecutor.submit(timerTaskEntry.timerTask);
            }
        }
    }
 
    // 向前驱动时间轮
    @Override
    public boolean advanceClock(Long timeoutMs) throws Exception{
        // 使用阻塞队列获取任务
        TimerTaskList bucket = delayQueue.poll(timeoutMs, TimeUnit.MILLISECONDS);
        if(bucket != null) {
            writeLock.lock();
            try{
                while(bucket != null) {
                    timingWheel.advanceClock(bucket.getExpiration());
                    // 驱动时间后,需要移动TimerTaskList到上一个槽或者从上一层移动到本层
                    bucket.flush(reinsert);
                    bucket = delayQueue.poll();
                }
            }finally {
                writeLock.unlock();
            }
            return true;
        }else {
            return false;
        }
    }
 
    @Override
    public int size() {
        return taskCounter.get();
    }
 
    @Override
    public void shutdown() {
        taskExecutor.shutdown();
    }
}

4.6 时间轮接口测试

public class SystemTimerTest {
    //驱动时间轮向前的线程
    private static ExecutorService executorService = Executors.newFixedThreadPool(1);
    public static  SystemTimer timer = new SystemTimer("test",1000L,5,System.currentTimeMillis());
 
 
    public static void runTask() throws Exception {
        for(int i = 0;i < 10000;i+= 1000) {
            // 添加任务,每个任务间隔1s
            timer.add(new TimerTask(i) {
                @Override
                public void run() {
                    System.out.println("运行testTask的时间: " + System.currentTimeMillis());
                }
            });
        }
    }
 
    public static void main(String[] args) throws Exception {
        runTask();
 
        executorService.submit(() -> {
            while(true) {
                try {
                    // 驱动时间轮线程间隔0.2s驱动
                    timer.advanceClock(200L);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
 
 
        Thread.sleep(1000000);
        timer.shutdown();
        executorService.shutdown();
    }
}

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

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

相关文章

m3u8转mp4下载,有URL,IV

1、背景 在线m3u8现在是主流加密方式的视频。 2、下载m3u8视频难点 首先需要连接m3u8文件格式,这个自行百度,其次加密方式确定和key以及iv。如果没有加密直接找一个在线的m3u8转mp4就可以,但是问题就是很多带加密,而且key不是m3m8中key URL返回的数据,市面上软件无法直…

基于matlab评估机场监控雷达上 5G 新无线电 (NR) 信号的干扰

一、前言 随着5G NR系统的频率范围超出LTE中使用的频段&#xff0c;频谱管理变得更加复杂。对扩大5G覆盖范围的需求是由更高的数据速率和更低的延迟的好处推动的。新5G基站的实施反过来又推动了了解这些信号如何影响在相同频段上运行的已安装系统的需求。其中一个系统是空中交通…

类对象

一、类初识 类&#xff1a;表示一种事物所具有的共同特征和行为 对象&#xff1a;一个类的实例 如下图&#xff0c;通过狗这个类进行详解 这是一个Dog类 对象&#xff1a;斗牛犬、小猎犬、牧羊犬 类中的属性&#xff1a;breed(品种)、size(大小)、color(颜色)、age(年龄)、 …

OpenCv基础之绘图及几何变换实例

文章目录 OpenCv基础之绘图及几何变换实例创建背景图线段绘制矩形绘制圆绘制椭圆绘制绘制多边形添加文字 几何变换图像平移图像缩放图像旋转仿射变换透视变化 OpenCv基础之绘图及几何变换实例 绘图在图像处理中&#xff0c;主要是在处理完图像后&#xff0c;将图像中的目标进行…

Python算法设计 - 哈夫曼编码

目录 一、哈夫曼树二、哈夫曼编码三、Python算法实现四、作者Info 一、哈夫曼树 上图是根据“this is an example of a huffman tree”中得到的字母频率来建构的哈夫曼树 二、哈夫曼编码 多年来&#xff0c;哈夫曼编码在统计数据压缩方面是非常先进的&#xff0c;应当指出&am…

C# 类库打包推送到nuget

步骤1&#xff1a;注册nuget 账号&#xff0c;可以使用outlook邮箱进行注册 步骤2&#xff1a;建立 apikey 名字自己起&#xff0c;Glob Pattern 填入“*” 步骤3&#xff1a;把程序打包&#xff0c;打包很简单右键vs2022 打包就好 但是注意*.csproj 文件修改,修改目的是为了…

IGS 产品长文件命名方式简介

文章目录 Part.I IntroductionPart.II 文件命名方式Chap.I 官方说明Chap.II 实例 Reference Part.I Introduction 2022 年 11 月 30 日&#xff08;DOY 331, GPSWD 22380&#xff09;及以后&#xff0c;IGS 的参考框架从 IGS-14 切换为 用 IGS-20&#xff0c;最新的卫星和地…

vue3中<script setup> 和 setup函数的区别

<script setup> 是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。当同时使用 SFC 与组合式 API 时该语法是默认推荐。相比于普通的 <script> 语法&#xff0c;它具有更多优势&#xff1a; 更少的样板内容&#xff0c;更简洁的代码。能够使用纯 TypeScript…

拿下模板进阶

模板进阶 1. 非类型模板参数 模板参数分类类型形参与非类型形参。 类型形参即&#xff1a;出现在模板参数列表中&#xff0c;跟在class或者typename之类的参数类型名称。 非类型形参&#xff0c;就是用一个常量作为类(函数)模板的一个参数&#xff0c;在类(函数)模板中可将…

代码随想录训练营day53|1143、最长公共子序列;1035、不相交的线;53、最大子序和动态规划

1143、最长公共子序列 给定两个字符串 text1 和 text2&#xff0c;返回这两个字符串的最长公共子序列的长度。 一个字符串的 子序列 是指这样一个新的字符串&#xff1a;它是由原字符串在不改变字符的相对顺序的情况下删除某些字符&#xff08;也可以不删除任何字符&#xff…

如何在一个中文大模型上,加入招投标字段标注的数据,搭建一个招投标字段解析的Transformer模型?

ChatGPT方案1 在一个中文大模型上加入招投标字段标注的数据&#xff0c;并搭建招投标字段解析的Transformer模型可以通过以下步骤实现&#xff1a; 收集并标注招投标相关的数据。可以使用现有的数据集&#xff0c;也可以通过爬虫技术获取相关数据&#xff0c;然后进行人工标注。…

Linux应用编程(进程)

一、进程与程序 注册进程终止处理函数 atexit() #include <stdlib.h> int atexit(void (*function)(void));使用该函数需要包含头文件<stdlib.h>。 函数参数和返回值含义如下&#xff1a; function&#xff1a;函数指针&#xff0c;指向注册的函数&#xff0c;此…

使用S3协议通过dfs实现Spring的SPI机制和spring.factories

目录 参考一、SPI机制1、什么是SPI2、使用场景&#xff1f;3、使用介绍4、代码演示新建工程edevp-dfs-api的spi接口新建阿里云oss实现类新建minio实现类新建测试工程edevp-demo测试 5、总结优点&#xff1a;解耦缺点&#xff1a; 二、Spring Boot的扩展机制之Spring Factories1…

MathType7最新版本下载安装与使用,注册表文件分享,添加为Word公式插件

wx供重浩&#xff1a;创享日记 对话框发送&#xff1a;mathtype 免费获取MathType7安装包注册表文件 MathType是强大的数学公式编辑器&#xff0c;与常见的文字处理软件和演示程序配合使用&#xff0c;能够在各种文档中加入复杂的数学公式和符号&#xff0c;可用在编辑数学试卷…

【回忆 总结】我的大学四年

大学四年关键词速览 如果穿越回大一&#xff0c;你想对大一的你提什么最重要的建议&#xff1f;同样是上网课&#xff0c;我为何能比大多数同学学的更好&#xff1f;回到学校&#xff0c;我的大二似乎一帆风顺&#xff1f;在不断的迷茫和徘徊中&#xff0c;大三的我做出的决定&…

Vue开发过程中那些易混淆的知识点

vue & vue cli Vue CLI Vue 一堆的js插件Vue CLI是基于 Node.js 开发出来的工具&#xff0c;它是一个官方发布 vue.js 项目脚手架&#xff0c;可以快速搭建 Vue 开发环境以及对应的 webpack 配置&#xff0c;单独编译&#xff0c;单独部署。可以再集成各种第三方插件&am…

偏好强化学习概述

文章目录 为什么需要了解偏好强化学习什么是偏好强化学习基于偏好的马尔科夫决策过程&#xff08;Markov decision processes with preferences&#xff0c;MDPP&#xff09; 反馈类型分类学习算法分类近似策略分布(Approximating the Policy Distribution)比较和排序策略(Comp…

STATS 782 - R Basic Concepts

文章目录 前言一、R basic1. R Operator2. 变量赋值3. c() - combine 函数4. 对向量的操作5. Special Numerical Values 二、 Built-in Functions1. min, max and range2. sum and prod3. Cumulative Summaries4. paste5. list&#xff08;&#xff09;6. seq&#xff08;&…

【Unity VR开发】结合VRTK4.0:添加遮蔽追踪器

语录&#xff1a; 恋爱应该是双方扶持对方共同完成自己的目标&#xff0c;而不是虚幻的思想、肤浅的物质、和纸醉金迷的生活。 前言&#xff1a; 遮蔽追踪器&#xff08;Trackers.ObscuranceTracker&#xff09;是基于游戏对象存在或不可见之间切换对象的状态&#xff0c;从而遮…

SpringBoot 通过AOP + Redis 防止表单重复提交

Spring Boot是一个用于构建Web应用程序的框架&#xff0c;通过AOP可以实现防止表单重复提交。在这篇博客中&#xff0c;我将介绍如何使用AOP来防止表单重复提交。 配置Redis 1. 添加Redis依赖 <dependency><groupId>org.springframework.boot</groupId>&…