TimeWheel时间轮算法原理及实现(附源码)
时间轮算法原理及实现
- 前言
- 1.时间轮核心
- 2.简单定时器
- 3.任务队列
- 4.优化任务队列
- 5.简单时间轮
- 6.多层时间轮
前言
在各种业务场景中,我们总是会需要一些定时进行一些操作,这些操作可能是需要在指定的某个时间点操作,也可能是每过一个固定的时间间隔后进行操作,这就要求我们需要有一个定时调度的逻辑,同时,这种定时操作,既有可能在某一刻数量比较密集,也有可能时间间隔比较密集,这就要考虑定时调度器对性能的影响.
如果在业务逻辑中,存在数量较大的定时任务,且每个定时任务都创建一个只属于自己的的调度管理器负责自身的生命周期及周期任务执行, 这就极大的浪费cpu的资源,降低自身性能.时间轮算法是一种调度模型,可以有效地利线程资源来处理批量周期任务,时间轮调度模型将数量巨大的定时任务绑定在单个调度器上,并统一使用这个调度器来管理,触发以及执行任务.这种模型使得大量延时任务,周期任务以及通知任务的管理变得高效.
1.时间轮核心
时间轮算法的核心是,轮询线程不再是负责遍历所有任务,而只在负责处于其当前时间上的任务
.就像钟表一样,时间轮有一个指针会一直旋转,每转到一个新的时间,就去执行挂在这一时刻下的所有任务,当然,这些任务通常是交给其他线程去做,指针要做的只是继续往下转,不会关心任务的执行进度.
下面,我们就从一个最简单的定时任务来一步步优化,看看时间轮到底是怎么设计出来的
2.简单定时器
这种方式最简单,如果想定期执行一个操作,只需要起一个定时器,设置时间间隔,设置回调函数,让它跑就完了.在定时任务非常少的情况下,这种方式没什么问题.如果定时任务的数目很大,并且都有不同的周期,那就产生了非常多的定时器, 这对系统的内存
和cpu
都产生了很大的压力,程序还没开始跑呢,定时器已经满天飞了…
3.任务队列
为了不产生过多的定时器,我们只使用一个定时器,将所有的定时任务放到一个队列
中,每个定时任务都保存一份自己的定时信息,定时器每隔一个周期轮询一遍队列中的所有任务,如果任务的超时时间已到,则执行该任务,如果超时时间还没到,则将该任务的定时信息减掉一个定时器的时间间隔,等到完全减为0时,该任务就可以被执行了, 这个定时器一直这么执行并轮询下去.假设当前定时任务总数有100个,那定时器每个周期会遍历
一个100个元素的队列,听上去还可以,那要有1000个的时候,10000个时候,这定时器就太可怜了,像一头老牛😄
4.优化任务队列
为了解决任务队列中任务太多,一个定时器压力太大的问题,我们继续对其进行优化,既然所有的定时任务都放在一个队列下不太行,那就对定时任务进行分类,将定时周期相同的任务放在同一个定时器下,这样每个定时器的压力就会大大减小,每个定时器只负责自己周期内的任务,不负责其他周期的任务.但是如果每个任务的周期都相同,还是要产生很多定时器,似乎还是无法从根本上解决问题…
5.简单时间轮
这时候时间轮的优势就体现出来了,我们设置一个环状的时间队列,队列上的每一个元素,我们称为一个槽(slot)
,这个槽所对应的时间在整个时间轮里是唯一的, 根据前面的分析也能知道,槽里面是放任务的,而且肯定不会放一个任务,而是放一个任务队列
.
对这个环状队列,我们维护一个指针,指针指向的槽,就是已经到达超时时间
的槽,槽里的任务就要被执行.任务在被插入时间轮的时候,就根据当前时间以及自己的时间周期,确定好了自己会处于时间轮中的哪个槽.等到时间轮指针指到这个槽,任务就被触发
.
这样,时间轮只需要定时
的轮询轮上的槽,如果有任务就交给任务处理线程去做,没有就继续轮询,即使总任务有一万个,调度器还是只会轮询这十个槽,而不会去轮询一遍十万个任务.
这就是单层时间轮
,这个时间轮的所能处理的最大周期是有限的,比如,一个具有10个slot的时间轮,wheel size = 10,每两个槽之间的间隔为1s,这个间隔称为tick,即最小的时间间隔,那么这个时间轮的跨度就是10*1 = 10s,也就是所支持能设置的最大周期为10s,如果一个任务每隔11秒执行一次.
同时,10s这个周期太短了,现在各种系统中不乏周期为成百上千秒的定时任务,且以1s为分割,颗粒
太大了,秒级以下的定时任务无法被触发.
如果仅仅是个时间跨度为10s切以秒为tick的时间轮,是基本满足不了大部分场景的,为了满足需求,最简单快速的方法就是加大时间轮跨度来提升周期,降低tick来提高精度,如果时间跨度提升为60s, tick改成10ms,就需要6000个槽来安插任务,这样就可以设置周期更长的任务,可以根据更精细的时间单位(10ms)来执行定时任务
需求满足了,但同时又带来了一些问题.
- 轮询线程遍历效率低下问题:当timescale数量增加,task数量较少时,轮询线程遍历效率下降,比如只有50槽上有task,但是却需要遍历6000个timescale。这违背了我们时间轮算法的初衷:
解决遍历轮询线程遍历效率低下的问题
。 - 内存空间浪费问题:时间尺度密集,任务数量少,大部分时间尺度占用的内存空间没有意义。
如果将时间轮跨度设置为1小时,那么整个时间轮需要60x60x1000/100 = 36000个单位的时间刻度,此时时间轮算法的遍历线程会遇到更大的运行效率低下。
因此简单(单层)时间轮的性能上限很低,一旦精度和时间跨度要求上来,就无法达到期望的目标了.
6.多层时间轮
在上面的场景下,多层时间轮
就诞生了,就像我们生活中见过的水表一样,有非常多的小表盘
多层时间轮的概念也非常清晰,将时间轮分为多个,每两个轮之间是进位
的关系,例如最普遍的秒,分,时.
即:
-
秒级时间轮上,设有60个槽, 每两个槽之间的时间为1s.
-
分钟级时间轮上,设有60s个槽,每两个槽之间的时间为1min
-
小时级时间轮上,设有24个槽,每两个槽之间的时间为1h.
这样,秒级每走60个槽,时间过去一分钟,秒级时间轮归零,分级时间轮走一个槽; 分级时间轮每走过60个槽,时间过去一小时,分级时间轮归零,小时级时间轮走一个槽.
通过三级时间轮,只需要遍历60+60+60 =180个槽,就可以成为一个精度为1s, 周期为60x60x60 = 216000s的定时调度器.
多级时间轮的思想在很多开发中间件中都被应用,例如Netty
、Akka
、Quartz
、ZooKeeper
、Kafka
等等.
作为学习Linux上C++开发的必备书籍,《Linux高性能服务器编程》以及《Linux多线程服务端编程:使用muduo C++网络库》两本书中,都介绍到了时间轮定时器,其中第二本书中,作者陈硕
详细介绍了如果将时间轮应用在经典项目:muduo C++网络库
中,非常值得参考学习!
参照《Linux高性能服务器编程》的第11章:11.4高性能定时器
,
以及《Linux多线程服务端编程:使用muduo C++网络库》第7章:7.10 用timing wheel 踢掉空闲连接
,
简单实现了下一个三级定时器,下面是源码,并且在关键的地方进行了注释.
TimWheel.h
#include <memory>
#include <list>
#include <vector>
#include <mutex>typedef struct TimePos{int pos_ms;int pos_sec;int pos_min;
}TimePos_t;typedef struct Event {int id;void(*cb)(void);void* arg;TimePos_t timePos;int interval;
}Event_t;class TimeWheel {typedef std::shared_ptr<TimeWheel> TimeWheelPtr;typedef void (*EventCallback_t)(void );typedef std::vector<std::list<Event_t>> EventSlotList_t;
public:TimeWheel();void initTimeWheel(int steps, int maxMin);void createTimingEvent(int interval, EventCallback_t callback);public:static void* loopForInterval(void* arg);private:int getCurrentMs(TimePos_t timePos);int createEventId();int processEvent(std::list<Event_t> &eventList);void getTriggerTimeFromInterval(int interval, TimePos_t &timePos);void insertEventToSlot(int interval, Event_t& event);EventSlotList_t m_eventSlotList;TimePos_t m_timePos;pthread_t m_loopThread;int m_firstLevelCount;int m_secondLevelCount;int m_thirdLevelCount; int m_steps;int m_increaseId; // not usedstd::mutex m_mutex;
};
TimeWheel.cpp
#include "TimeWheel.h"#include <iostream>
#include <memory.h>
#include <chrono>
#include <thread>TimeWheel::TimeWheel() : m_steps(0), m_firstLevelCount(0), m_secondLevelCount(60), m_thirdLevelCount(0),m_increaseId (0){memset(&m_timePos, 0, sizeof(m_timePos));}void* TimeWheel::loopForInterval(void* arg)
{if(arg == NULL) {printf("valid parameter\n");return NULL;}TimeWheel* timeWheel = reinterpret_cast<TimeWheel*>(arg);while(1) {std::this_thread::sleep_for(std::chrono::milliseconds(timeWheel->m_steps));// printf("wake up\n");TimePos pos = {0};TimePos m_lastTimePos = timeWheel->m_timePos;//update slot of current TimeWheeltimeWheel->getTriggerTimeFromInterval(timeWheel->m_steps, pos);timeWheel->m_timePos = pos;{std::unique_lock<std::mutex> lock(timeWheel->m_mutex);// if minute changed, process in integral point (minute)if (pos.pos_min != m_lastTimePos.pos_min){// printf("minutes changed\n");std::list<Event_t>* eventList = &timeWheel->m_eventSlotList[timeWheel->m_timePos.pos_min + timeWheel->m_firstLevelCount + timeWheel->m_secondLevelCount];timeWheel->processEvent(*eventList);eventList->clear();}else if (pos.pos_sec != m_lastTimePos.pos_sec){//in same minute, but second changed, now is in this integral second// printf("second changed\n");std::list<Event_t>* eventList = &timeWheel->m_eventSlotList[timeWheel->m_timePos.pos_sec + timeWheel->m_firstLevelCount];timeWheel->processEvent(*eventList);eventList->clear();}else if (pos.pos_ms != m_lastTimePos.pos_ms){//now in this ms// printf("ms changed\n");std::list<Event_t>* eventList = &timeWheel->m_eventSlotList[timeWheel->m_timePos.pos_ms];timeWheel->processEvent(*eventList);eventList->clear();}// printf("loop over\n");}}return nullptr;
}//init TimeWheel's step and maxmin, which detemine the max period of this wheel
void TimeWheel::initTimeWheel(int steps, int maxMin)
{if (1000 % steps != 0){printf("invalid steps\n");return;}m_steps = steps;m_firstLevelCount = 1000/steps;m_thirdLevelCount = maxMin;m_eventSlotList.resize(m_firstLevelCount + m_secondLevelCount + m_thirdLevelCount);int ret = pthread_create(&m_loopThread, NULL, loopForInterval, this);if(ret != 0) {printf("create thread error:%s\n", strerror(errno));return;}// pthread_join(m_loopThread, NULL);
}void TimeWheel::createTimingEvent(int interval, EventCallback_t callback){if(interval < m_steps || interval % m_steps != 0 || interval >= m_steps*m_firstLevelCount*m_secondLevelCount*m_thirdLevelCount){printf("invalid interval\n");return;}printf("start create event\n");Event_t event = {0};event.interval = interval;event.cb = callback;//set time startevent.timePos.pos_min = m_timePos.pos_min;event.timePos.pos_sec = m_timePos.pos_sec;event.timePos.pos_ms = m_timePos.pos_ms;event.id = createEventId();// insert it to a slot of TimeWheelstd::unique_lock<std::mutex> lock(m_mutex);insertEventToSlot(interval, event);printf("create over\n");
}int TimeWheel::createEventId() {return m_increaseId++;
}void TimeWheel::getTriggerTimeFromInterval(int interval, TimePos_t &timePos) {//get current time: msint curTime = getCurrentMs(m_timePos);// printf("interval = %d,current ms = %d\n", interval, curTime);//caculate which slot this interval should belong to int futureTime = curTime + interval;// printf("future ms = %d\n", futureTime);timePos.pos_min = (futureTime/1000/60)%m_thirdLevelCount;timePos.pos_sec = (futureTime%(1000*60))/1000;timePos.pos_ms = (futureTime%1000)/m_steps;// printf("next minPos=%d, secPos=%d, msPos=%d\n", timePos.pos_min, timePos.pos_sec, timePos.pos_ms);return;
}int TimeWheel::getCurrentMs(TimePos_t timePos) {return m_steps * timePos.pos_ms + timePos.pos_sec*1000 + timePos.pos_min*60*1000;
}int TimeWheel::processEvent(std::list<Event_t> &eventList){// printf("eventList.size=%d\n", eventList.size());//process the event for current slotfor(auto event = eventList.begin(); event != eventList.end(); event ++) {//caculate the current msint currentMs = getCurrentMs(m_timePos);//caculate last time(ms) this event was processedint lastProcessedMs = getCurrentMs(event->timePos);//caculate the distance between now and last time(ms)int distanceMs = (currentMs - lastProcessedMs + (m_secondLevelCount+1)*60*1000)%((m_secondLevelCount+1)*60*1000);//if interval == distanceMs, need process this eventif (event->interval == distanceMs){//process eventevent->cb();//get now pos as this event's start pointevent->timePos = m_timePos;//add this event to slotinsertEventToSlot(event->interval, *event);}else{//this condition will be trigger when process the integral point printf("event->interval != distanceMs\n");// although this event in this positon, but it not arriving timing, it will continue move to next slot caculate by distance ms.insertEventToSlot(distanceMs, *event);}}return 0;
}void TimeWheel::insertEventToSlot(int interval, Event_t& event)
{printf("insertEventToSlot\n");TimePos_t timePos = {0};//caculate the which slot this event should be set togetTriggerTimeFromInterval(interval, timePos);{// printf("timePos.pos_min=%d, m_timePos.pos_min=%d\n", timePos.pos_min, m_timePos.pos_min);// printf("timePos.pos_sec=%d, m_timePos.pos_sec=%d\n", timePos.pos_sec, m_timePos.pos_sec);// printf("timePos.pos_ms=%d, m_timePos.pos_ms=%d\n", timePos.pos_ms, m_timePos.pos_ms);// if minutes not equal to current minute, first insert it to it's minute slotif (timePos.pos_min != m_timePos.pos_min){printf("insert to %d minute\n", m_firstLevelCount + m_secondLevelCount + timePos.pos_min);m_eventSlotList[m_firstLevelCount + m_secondLevelCount + timePos.pos_min].push_back(event);}// if minutes is equal, but second changed, insert slot to this integral point secondelse if (timePos.pos_sec != m_timePos.pos_sec){printf("insert to %d sec\n",m_firstLevelCount + timePos.pos_sec);m_eventSlotList[m_firstLevelCount + timePos.pos_sec].push_back(event);}//if minute and second is equal, mean this event will not be trigger in integral point, set it to ms slotelse if (timePos.pos_ms != m_timePos.pos_ms){printf("insert to %d ms\n", timePos.pos_ms);m_eventSlotList[timePos.pos_ms].push_back(event);}}return;
}
main.cpp
#include <iostream>
#include "TimeWheel.h"
using namespace std;void funccc(void) {cout << "exec function" << endl;
}int main()
{TimeWheel wheel;wheel.initTimeWheel(100, 10);wheel.createTimingEvent(200, funccc);while (1){}
}
对于上述实现的三级时间轮,下篇文章将会详细拆解其各个步骤,然后大家就可以自己撸一个时间轮了!
对于学习Linux开发以及C/C++开发,除了上文提到的书之外,《C++ Primer》
以及《Efficetive C++》
等数也是必不可少.这些书籍网上资源也有很多,但是一本一本收集起来还是挺费劲的,如果需要这些书的话,我将其整理到了公众号 上,公众号搜索 程序员DeRozan,回复1207
即可直接拿到整理好的学习资料.
相关文章:

TimeWheel时间轮算法原理及实现(附源码)
时间轮算法原理及实现前言1.时间轮核心2.简单定时器3.任务队列4.优化任务队列5.简单时间轮6.多层时间轮前言 在各种业务场景中,我们总是会需要一些定时进行一些操作,这些操作可能是需要在指定的某个时间点操作,也可能是每过一个固定的时间间隔后进行操作,这就要求我们需要有一个…...

【蓝牙mesh】Upper协议层介绍
【蓝牙mesh】Upper协议层介绍 Upper层简介 Upper协议层用于处理网络层以上的功能,包括设备的应用层数据、安全、群组等信息,是实现蓝牙mesh应用功能的关键协议之一。Upper层接收来自Access层的数据或者是Upper层自己生成的Control数据,并且将…...

NEXUS 6P刷机安装Edxposed
刷机 abd等工具下载: https://developer.android.com/studio/releases/platform-tools?hlzh-cn 下载后配置环境变量 镜像下载: https://developers.google.com/android/images?hlzh-cn#angler Magisk下载 GitHub - topjohnwu/Magisk: The Magic M…...

web、ES、vue等知识总结
1:Vue2和vue3的区别: 一个、两个2:Vue3.x为什么要用Proxy来代替Object.defineProperty?3:Proxy4:一些知识点的原理全面5:vue3中加入eslint和prettier6:详解vue中的diff算法7:响应式布局的常用解决方案对比…...

数据库第一章(王珊课后习题)
文章目录1.试述数据、数据库、数据库管理系统、数据库系统的概念2.使用数据库系统有什么好处?3.试述文件系统与数据库系统的区别和联系。4.试述数据库系统的特点5.数据库管理系统的主要功能有哪些?6.什么是概念模型?试叙述概念模型的作用7.解释实体、实…...

设计模式(十一)----结构型模式之装饰者模式
1、概述 我们先来看一个快餐店的例子。 快餐店有炒面、炒饭这些快餐,可以额外附加鸡蛋、火腿、培根这些配菜,当然加配菜需要额外加钱,每个配菜的价钱通常不太一样,那么计算总价就会显得比较麻烦。 使用继承的方式存在的问题&…...

lighthouse的介绍和基本使用方法
Lighthouse简介 Lighthouse是一个开源的自动化性能测试工具,我们可以使用该功能检测我们的页面存在那些性能方面的问题,并会生成一个详细的性能报告来帮助我们来优化页面 使用方式 LH一共有四种使用方式 Chrome开发者工具Chrome扩展Node 命令行Node …...

分布式算法 - Raft算法
Paxos是出了名的难懂,而Raft正是为了探索一种更易于理解的一致性算法而产生的。它的首要设计目的就是易于理解,所以在选主的冲突处理等方式上它都选择了非常简单明了的解决方案。推荐阅读提示强烈推荐通过如下资料学习raft。 raft.github.io这里面有一个…...

Python|每日一练|链表|双指针|数组|递归|图算法|单选记录:删除链表的倒数第 N 个结点|下一个排列|迷宫问题
1、删除链表的倒数第 N 个结点(链表,双指针) 给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。 进阶:你能尝试使用一趟扫描实现吗? 示例 1: 输入:head …...

天线理论知识2——宽带天线介绍
系列文章目录 文章目录 系列文章目录前言一、行波天线1. 长导线天线2. V形天线二、螺旋天线三、八木-宇田天线前言 宽带天线指的是具有哦宽频带特性的天线,常见的宽带天线有行波天线,螺旋天线以及八木天线。 一、行波天线 长度为 l l l的中心馈电的线天线上的电流呈驻波分…...

【计组笔记05】计算机组成与原理之虚拟存储器、指令系统、中央处理器CPU
这篇文章,主要介绍计算机组成与原理之虚拟存储器、指令系统、中央处理器CPU。 目录 一、虚拟存储器 1.1、页式虚拟存储器 1.2、段式虚拟存储器 1...

多功能土壤速测仪功能介绍
大家好,今天给大家介绍一款多功能土壤速测仪,它由多功能速测仪主机、土壤墒情传感器、USB数据线、电源适配器、便携式手提箱等部分组成。速测主机配备有工业级液晶屏,大屏幕中文显示,使得测量参数,时间,设备…...

《设计模式》命令模式
《设计模式》命令模式 命令模式(Command Pattern)是一种行为型设计模式,它将请求和处理分开,使得请求发送者和接收者解耦,从而降低系统的耦合度。在命令模式中,请求被封装为一个独立的对象,并且…...

开源物联网平台有哪些?
目前市面上有许多开源物联网平台可供选择。以下是其中一些较为流行和知名的平台: Eclipse IoT:Eclipse IoT 是一个开源的物联网平台,旨在提供可扩展、灵活和高度集成的工具和框架,用于构建、部署和管理 IoT 解决方案。它包含多个…...

Tesla Autopilot,处理器和硬件
作者 | 初光 出品 | 车端 备注 | 转载请阅读文中版权声明 知圈 | 进“汽车电子与AutoSAR开发”群,请加微“cloud2sunshine” 总目录链接>> AutoSAR入门和实战系列总目录 Tesla MOdelS/X 中有 60 多个处理器。其他型号的处理器较少,但数量仍然不少…...

jianzhiOffer第二版难重点记录
04. 二维数组中的查找https://leetcode.cn/problems/er-wei-shu-zu-zhong-de-cha-zhao-lcof/ 思路:可以每层用以恶搞二分查找,优化思路:从左下角出发直接用二分。 07. 重建二叉树https://leetcode.cn/problems/zhong-jian-er-cha…...

C语言 | 问题20230225
C语言 | 问题20230225 文章目录C语言 | 问题202302251.问题1无限循环2.问题2C 中的运算符优先级实例1:1.问题1 Which slice of the following code is NOT endless loop? 以下代码的哪一部分不是无限循环? A for (;(cgetchar())!\n; ) printf("*c&…...

【机器学习笔记】Python基础笔记
目录基础语法加载数据:pd.read_csv查看数据大小:shape浏览数据行字段:columns浏览少量数据:head()浏览数据概要:describe()基础功能语法缺省值去除缺失值:dropna按行删除:存在空值,即…...

js-DOM03-DOM对CSS的操作
DOM对CSS的操作 - 读取和修改内联样式 - 使用style属性来操作元素的内联样式 - 读取内联样式: 语法:元素.style.样式名 - 例子: 元素.style.width 元素.style.…...

tun驱动之tun_init
tun驱动的初始化方法是tun_init。 static int __init tun_init(void) {int ret 0;pr_info("%s, %s\n", DRV_DESCRIPTION, DRV_VERSION);ret rtnl_link_register(&tun_link_ops);if (ret) {pr_err("Cant register link_ops\n");goto err_linkops;}re…...

模拟退火算法优化bp
%% 基于模拟退火遗传算法优化BP神经网络的钢带厚度预测 clear clc close all format short %% 加载训练数据 Xtrxlsread(train_data.xlsx); DDsize(Xtr,2); input_trainXtr(:,1:DD-1);% output_trainXtr(:,DD);% %% 加载测试数据 Xtexlsread(test_data.xlsx); input_testXte(…...

【NFC音乐相册】简易制作
欢迎来到 Claffic 的博客 💞💞💞 前言: NFC音乐相册在前段时间火了一把,想必大家都听过了,最近我刷到了这个东西,闲来无事就弄了几个,这篇博客就记录下制作工序。 注:我所…...

每日一题——L1-085 试试手气(15)
L1-085 试试手气 我们知道一个骰子有 6 个面,分别刻了 1 到 6 个点。下面给你 6 个骰子的初始状态,即它们朝上一面的点数,让你一把抓起摇出另一套结果。假设你摇骰子的手段特别精妙,每次摇出的结果都满足以下两个条件:…...

FreeRTOS信号量
前面介绍过,队列(queue)可以用于传输数据:在任务之间,任务和中断之间。消息队列用于传输多个数据,但是有时候我们只需要传递一个状态,这个状态值需要用一个数值表示,比如:…...

Leetcode.2385 感染二叉树需要的总时间
题目链接 Leetcode.2385 感染二叉树需要的总时间 Rating : 1711 题目描述 给你一棵二叉树的根节点 root,二叉树中节点的值 互不相同 。另给你一个整数 start。在第 0分钟,感染 将会从值为 start的节点开始爆发。 每分钟,如果节点…...

[蓝桥杯 2022 国 B] 卡牌(贪心/二分)
题目传送门 该题第一思路是想去模拟题目中所描述的过程 这里我选择从大到小遍历可能凑出的牌套数,计算凑出它需要补的牌数以及判断是否会超出能补的牌数 #include<iostream> #include<climits> #include<vector> #include<algorithm> #def…...

1301:大盗阿福
经典的dp打家劫舍问题状态设计dp[i][0]:在前i个店铺中选,且不选第i家的最大和dp[i][1]:在前i个店铺中选,且选第i家的最大和状态转移dp[i][0] max(dp[i-1][1], dp[i-1][0];第i家店不选,那么我们可以选第i-1个店 也可以…...

Netty——序列化的作用及自定义协议
序列化的作用及自定义协议序列化的重要性大小对比效率对比自定义协议序列化数据结构自定义编码器自定义解码器安全性验证NettyClientNettyServerNettyClientTestHandlerNettyServerTestHandler结果上一章已经说了怎么解决沾包和拆包的问题,但是这样离一个成熟的通信…...

一起Talk Android吧(第五百零五回:如何调整组件在约束布局中的大小)
文章目录 背景介绍调整方法各位看官们大家好,上一回中咱们说的例子是"如何调整组件在约束布局中的位置",这一回中咱们说的例子是" 如何调整组件在约束布局中的大小"。闲话休提,言归正转, 让我们一起Talk Android吧! 背景介绍 在使用约束(constraintl…...

【数据库】数据库的完整性
第五章 数据库完整性 数据库完整性 数据库的完整性是指数据的正确性和相容性 数据的正确性是指数据是符合现实世界语义,反映当前实际状况的数据的相容性是指数据库的同一对象在不同的关系中的数据是符合逻辑的 关系模型中有三类完整性约束:实体完整性…...