代码拉取完成,页面将自动刷新
// Copyright (c) 2023 刻BITTER
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#pragma once
#include <type_traits>
#ifdef __PERF_COUNTER__
#include "perf_counter.h"
#endif
#ifdef ARDUINO
#include "Arduino.h"
#endif
#define GET_TASK_MGR_TIME_TYPE(scheduler) typename decltype(scheduler)::TimeType
#define GET_TASK_MGR_INDEX_TYPE(scheduler) typename decltype(scheduler)::IndexType
namespace scheduler_basic {
#ifndef __cpp_lib_is_invocable
// Borrowed from: https://stackoverflow.com/a/47699262/22007109
template <class F, class... Args>
struct is_invocable {
template <class U>
static auto test(U *p) -> decltype((*p)(std::declval<Args>()...), void(), std::true_type());
template <class U>
static auto test(...) -> decltype(std::false_type());
static constexpr bool value = decltype(test<F>(0))::value;
};
template <class F, class... ArgTypes>
constexpr bool is_invocable_v = is_invocable<F, ArgTypes...>::value;
#endif
#ifdef __PERF_COUNTER__
// 用来提供时间戳的函数必须放在一个类或结构体里,才能作为模板参数传入DelayCallback 中
// 时间戳函数的原型为:
// inline static auto get_time()
//
// 因为只用于转发其他库的时间函数,所以可定义为inline,返回值也定义为auto,跟着被调用的底层函数走。
// 函数必须是静态的,不能引用类里的非静态成员,因为DelayTask 中不会多余创建一个对象再调用get_time()。
// 如果get_time() 放在类里,要设为public
struct PerfCounterMsSource {
inline static auto get_time() {
return get_system_ms();
}
using TimeType = decltype(get_system_ms());
};
struct PerfCounterUsSource {
inline static auto get_time() {
return get_system_us();
}
using TimeType = decltype(get_system_us());
};
struct PerfCounterTicksSource {
inline static auto get_time() {
return get_system_ticks();
}
using TimeType = decltype(get_system_ticks());
};
#endif
#ifdef ARDUINO
struct ArduinoMsSource {
inline static auto get_time() {
return millis();
}
using TimeType = decltype(millis());
};
struct ArduinoUsSource {
inline static auto get_time() {
return micros();
}
using TimeType = decltype(micros());
};
#endif
/**
* @brief DelayCallback 支持在运行时输出日志,比如提供任务运行时间、tick 运行时间,CPU 时间占用比例之类的信息。
*
* 与时间源一样,输出日志所用的函数也通过一个结构体作为模板参数传入。结构体内需包含四个函数:
*
* static auto enabled() : 返回0 或false 时关闭日志输出功能.返回值可以依赖于变量,所以能实现动态开关功能;
* static void log(const char *) : 输出字符串;
* static void log(uint32_t) : 输出整数,其中整数的类型可以自由设定,需要输出的最大的整数是时间戳,所以整数类型最好和时间戳类型一致;
* static void newline() : 输出换行或其他分隔符
*
* 默认Logger 是_DummyLogger,其中enabled 函数返回false 常量,关闭了日志输出,所以三个输出函数的实现都是空的
*
*/
struct _DummyLogger {
inline static constexpr auto enabled() {
return false;
}
inline static void log(const char *text) {}
inline static void log(uint32_t value) {}
inline static void newline() {}
};
/**
* @brief 用于释放CallbackPtr 指向的资源
*
* DelayCallback 在运行中删除任务时,将会调用destroy 函数删除任务指针。这是为了给动态创建的临时任务提供方便,
* 因为临时任务的创建者可能早就退出了,如果DelayCallback 删除任务时不做处理,就会产生野指针。
* 动态任务可以是new 创建的,也可以是在内存池中分配的。
*
* 由于任务列表中,一个CallbackPtr 可以对应多个任务,所以删除指针时需要实现引用计数功能。
* DelayCallback 会在添加任务时调用new_task 函数,可在其中将CallbackPtr 指向的对象中的引用计数加一。
*
* new_task 返回值用于控制要不要添加任务,返回false 则丢弃。这样可以控制一个任务对象只能被添加一次。
*
* @tparam CallbackPtr
*/
template <typename CallbackPtr>
struct _DummyDestroyer {
static constexpr auto enabled() {
return false;
}
static constexpr bool new_task(CallbackPtr fptr) { return false; }
static constexpr void destroy(CallbackPtr fptr) {}
};
/**
* @brief 存储任务信息的结构体
*
* 由于这只是个简单的调度器,任务结构体也就尽量简单,只有回调函数的指针和计时器这两个必要的成员。
* 任务计时采用倒计时原理,每次在tick 函数中更新所有任务的倒计时数值,原因是避免计时器溢出导致BUG。
*
* 调度器只支持在一段时间后运行指定的任务,本身不支持循环执行,这也是为了实现简单。想实现延迟指定时间后运行的功能,一般可以有三种思路:
*
* 1. 任务结构体内存储一个时间戳T1,代表预定要执行的时间。由于计时器溢出的问题,这种设计容易BUG。
*
* 2. 任务结构体内记录添加任务时的时间戳T0 和延迟时间ΔT,用当前时间T 减去T0 得到时间间隔,再和ΔT 比较。
* 这是Arduino 社区推荐的delay 函数的实现方式,不会因为一次计时器溢出导致BUG,最大延时时间为一个溢出周期。
* 但结构体里要多放一个TimeType 类型的变量,空间效率差一点。
*
* 3. 结构体里只放一个倒计时器,也就是这里用的方法,每次tick 函数检查任务状态时更新所有任务的倒计时器,
* 倒计时器减去的值等于两次tick 执行的间隔,当任务的倒计时剩余时间小于等于要减去的间隔时,说明任务延时结束。
*/
template <typename TimeType, typename CallbackPtr>
struct TaskBox {
TimeType down_counter;
CallbackPtr fptr;
};
// 可以使用对象包装回调函数,但调度器内部链表中存储的还是TaskBox,只是其中的回调函数指针换成了指向对象的指针
template <typename TimeType>
class TaskBase {
public:
virtual TimeType run() = 0;
};
template <typename TimeType>
using FunctionPtr = TimeType (*)();
// 检查CallbackPtr 是否和FunctionPtr 类型一致
template <typename CallbackPtr, typename TimeType>
TimeType _call_callback(CallbackPtr fptr,
typename std::enable_if_t<
std::is_same<FunctionPtr<TimeType>, std::decay_t<CallbackPtr>>::value> * = nullptr) {
return fptr();
}
// template <typename CallbackPtr, typename TimeType>
// TimeType _call_callback(CallbackPtr fptr,
// typename std::enable_if_t<
// is_invocable_v<std::remove_pointer_t<CallbackPtr>, void>> * = nullptr) {
// return fptr();
// }
// 检查CallbackPtr 指向的对象中是否含有TimeType run() 函数
template <typename CallbackPtr, typename TimeType>
TimeType _call_callback(CallbackPtr fptr,
typename std::enable_if_t<
std::is_same<TimeType, decltype(std::declval<std::remove_pointer_t<CallbackPtr>>().run())>::value> * = nullptr) {
return fptr->run();
}
template <bool fit_in_8, bool fit_in_16, bool fit_in_32>
struct _GetFittedTaskFlagType {
};
template <>
struct _GetFittedTaskFlagType<true, true, true> {
using Type = uint8_t;
};
template <>
struct _GetFittedTaskFlagType<false, true, true> {
using Type = uint16_t;
};
template <>
struct _GetFittedTaskFlagType<false, false, true> {
using Type = uint32_t;
};
template <>
struct _GetFittedTaskFlagType<false, false, false> {
using Type = uint64_t;
};
template <uint8_t Count>
struct _GetTaskFlagType {
using Type = typename _GetFittedTaskFlagType<Count <= 8, Count <= 16, Count <= 32>::Type;
};
// #define _IOXX_DELAY_CALLBACK_DISABLE_MAX_DURATION 1
/**
* @brief 简单的函数回调式定时任务管理器,默认只支持单次延时执行的任务,循环执行可通过函数返回值实现,或者手动重复添加定时任务。
*
* 回调函数返回值非0 则被看作该任务的下一次执行延时数值,直接用该值更新任务的倒计时。
*
* **支持在定时任务执行过程中添加新任务**,但不能在执行中主动删除任务,任务函数只能通过返回0 将自己删除。
* 新添加的任务在当次tick 中不会更新倒计时。
*
* 当DelayCallback 正在操作列表时,禁止从外部修改列表,比如,**禁止从中断函数中增加或删除任务**,在OS 环境下,
* 禁止其他异步任务直接操作任务列表。因为中断函数或异步任务会在随机位置打断当前操作,无法高效的随时随地检测列表的变化。
*
* DelayCallback 中不提供标志位用于检测是否正在操作列表,如果有必要从中断函数或异步任务中检测DelayCallback 的状态,
* 可在运行DelayCallback 的主程序中额外设置标志位,每次增加、删除任务或调用tick 函数时将标志置位。
*
* 如果最大任务数量不超过16,DelayCallback 内部将使用uint16_t 存储任务标志,在8 位AVR 单片机上,比如mega328p,
* 相比用uint32_t 可以省下差不多200 字节的flash。
*
* 最大10 个任务时,在AVR 单片机上,DelayCallback 本身占用的Flash 约1.1K 字节。
*
* 可以定义宏_IOXX_DELAY_CALLBACK_DISABLE_MAX_DURATION 关闭超时时间功能,大概能省下几十字节的Flash,
* 或许可以减少一点tick 函数的额外时间开销。
*
*
* @tparam TimeSource 获取时间戳的源,不关心时间的单位,可以是微秒、毫秒或时钟周期数。参考ArduinoMsSource 的实现,
* 时间源是一个结构体或类,其中有一个静态函数 static TimeType get_time(),返回时间戳。
* get_time 函数的返回值类型决定了TimeType。
*
* @tparam MaxTaskCount 最大可容纳的任务数量,是无耐心任务和普通任务的总和,超过该值后,再添加任务不会有效果。
* 同一个函数指针或回调对象可以多次重复添加进任务列表,且被视为不同的任务
*
* @tparam CallbackPtr 回调函数指针的类型,或回调对象指针的类型,如:TaskBase<TimeType>*。
* 回调函数指针的类型必须和FunctionPtr 一致,即,返回TimeType (*)():无参数,返回TimeType。
* 回调对象的指针可以指向任意类的对象,只要其中含有类型为 TimeType () 的成员函数run。
*
* @tparam Logger 与时间源类似,用于传入输出日志的工具函数,参考_DummyLogger
*
* @tparam CallbackDestroyer 与时间源类似,用于传入管理回调对象生命周期的工具函数,参考_DummyDestroyer
*/
template <typename TimeSource, size_t MaxTaskCount,
typename CallbackPtr = FunctionPtr<decltype(TimeSource::get_time())>,
typename CallbackDestroyer = _DummyDestroyer<CallbackPtr>,
typename Logger = _DummyLogger>
class DelayCallback2 {
public:
using TimeType = decltype(TimeSource::get_time());
using IndexType = uint8_t;
private:
using TaskType = TaskBox<TimeType, CallbackPtr>;
#ifdef _IOXX_USE_FIXED_32BIT_FLAG_TYPE
using TaskFlagType = uint32_t;
#else
using TaskFlagType = typename _GetTaskFlagType<MaxTaskCount>::Type;
#endif
TaskType _task_list[MaxTaskCount] = {0};
// 上一次tick 被调用的时间,在初始化后,_last_tick_time 是第一个任务被添加进列表的时间
TimeType _last_tick_time = 0;
IndexType _max_task_count_in_one_tick = 0;
IndexType _current_task_count = 0;
#ifndef _IOXX_DELAY_CALLBACK_DISABLE_MAX_DURATION
uint16_t _max_duration_of_one_tick = 0;
#endif
// 用变量中的一个bit 表示数组中对应位置的任务是不是impatient 任务,
// 所以数组最大不能超过变量的位数
TaskFlagType _impatient_flag_bit = 0;
constexpr static size_t _MAX_MAX_COUNT = sizeof(_impatient_flag_bit) * 8;
static_assert(MaxTaskCount <= _MAX_MAX_COUNT);
// 标记任务是否刚被添加,用于支持定时任务中添加新任务
TaskFlagType _new_task_flag_bit = 0;
auto test_impatient_bit(uint8_t index) {
return _impatient_flag_bit & (1 << index);
}
void set_impatient_bit(uint8_t index) {
_impatient_flag_bit |= (1 << index);
}
void clr_impatient_bit(uint8_t index) {
_impatient_flag_bit &= ~(1 << index);
}
auto test_new_task_bit(uint8_t index) {
return _new_task_flag_bit & (1 << index);
}
void set_new_task_bit(uint8_t index) {
_new_task_flag_bit |= (1 << index);
}
void clr_new_task_bit(uint8_t index) {
_new_task_flag_bit &= ~(1 << index);
}
void clr_all_new_task_bit() {
_new_task_flag_bit = 0;
}
public:
#ifndef _IOXX_DELAY_CALLBACK_DISABLE_MAX_DURATION
DelayCallback2(uint8_t max_task_count_in_tick = 0, uint16_t max_duration_of_tick = 0) :
_max_task_count_in_one_tick(max_task_count_in_tick), _max_duration_of_one_tick(max_duration_of_tick) {}
#else
DelayCallback2(uint8_t max_task_count_in_tick = 0) :
_max_task_count_in_one_tick(max_task_count_in_tick) {}
#endif
/**
* @brief 更新任务倒计时,执行延时完成的任务。
*
* 调用函数瞬间的时刻被缓存下来,用于更新所有任务的倒计时,
* 所以任务执行的时序都是同步的,tick 函数执行过程的时间变化不会被任务感知到。
* 缺点是,如果tick 执行时间太长,从第一个任务执行到最后一个任务的过程中,系统实际时间已经发生较大变化,
* 任务的执行的实时性将会恶化。
*
* 比如,在最后一个任务更新时间的瞬间,如果按实际时间计算,该任务的延时已经完成,但由于tick 函数的实现,
* 该任务只能等到下一次tick 才能运行。
*
*/
void tick() {
// TODO: 添加日志输出和信息统计
uint8_t task_counter = 0;
auto start_time = TimeSource::get_time();
// tick interval 是两次tick 开始的时间差,即上次tick 的运行时间加上退出tick 后主程序的运行时间
auto tick_interval = start_time - _last_tick_time;
_last_tick_time = start_time;
if (Logger::enabled()) {
Logger::log("[DelayCallback2]: tick start at: ");
Logger::log(start_time);
Logger::newline();
}
clr_all_new_task_bit();
for (uint8_t i = 0; i < MaxTaskCount; ++i) {
// 只能固定的遍历整个列表,不能根据当前任务总数提前终止遍历。因为允许遍历途中添加任务,所以任务总数有可能增加,
// 但添加的任务可能在当前遍历位置的前面,结果一直到列表遍历结束,检测到的任务数和任务总数都对不上。
// 可以用双条件,即,任务总数和列表总长是或的关系,只要有一个达到,就结束遍历。但这样做性价比可能很低,
// 任务列表最长只有32,大部分时候可能十来个就够了,如果设计程序时的估算比较准,列表大部分时间是比较满的,
// 那么遍历整个表的额外开销就更小了。
if (_task_list[i].down_counter == 0 || test_new_task_bit(i)) { // 跳过本次tick 过程中新添加的任务
continue;
}
if (_task_list[i].down_counter <= tick_interval) { // 执行并处理返回值
// 如果正在遍历impatient 任务,则不管超时时间和任务数量限制,必须执行任务
if (test_impatient_bit(i)
|| ((_max_task_count_in_one_tick == 0 || task_counter < _max_task_count_in_one_tick)
#ifndef _IOXX_DELAY_CALLBACK_DISABLE_MAX_DURATION
&& (_max_duration_of_one_tick == 0 || ((TimeSource::get_time() - start_time) < _max_duration_of_one_tick))
#endif
)) {
++task_counter;
_task_list[i].down_counter = _call_callback<CallbackPtr, TimeType>(_task_list[i].fptr);
if (_task_list[i].down_counter == 0) {
_remove_task_from_list(i);
}
}
else {
_task_list[i].down_counter = 1; // 把本次没轮到的任务的倒计时设为足够小的数
}
}
else {
_task_list[i].down_counter -= tick_interval;
}
// 倒计时为0 的任务被视为已删除,不需要操作内存删除任务
}
if (Logger::enabled()) {
Logger::log("[DelayCallback2]: tick stopped, time consumption: ");
Logger::log(TimeSource::get_time() - start_time);
Logger::newline();
Logger::log("[DelayCallback2]: executed task count:");
Logger::log(task_counter);
Logger::newline();
Logger::log("[DelayCallback2]: tick interval: ");
Logger::log(tick_interval);
Logger::newline();
}
}
/**
* @brief 将上一次tick 的时间设置为当前时间
*
*/
void reset_last_tick_time() {
_last_tick_time = TimeSource::get_time();
}
bool not_full() const {
return _current_task_count < MaxTaskCount;
}
/**
* @brief 在tick 函数调用后一次性执行的最大任务数
*
* 一次tick 中执行太多耗时的任务会占用过多CPU 时间,还会导致下一次tick 被推迟,从而降低任务执行的实时性。
* 值为0 则不限制,值大于1 表示最多只执行这个数值的任务,其他任务只更新倒计时,不执行。
* 列表中的impatient 任务也会被计数,但tick 函数只有在impatient 任务全部执行后才退出。
* 即,当最大任务数小于等于impatient 任务数时,tick 函数在执行完所有impatient 任务后退出。
* 若最大任务数大于impatient 任务数,则会继续执行普通任务,直到等于最大任务数。
*
* @param max_count
*/
void set_max_task_counter_in_tick(uint8_t max_count) {
_max_task_count_in_one_tick = max_count;
}
#ifndef _IOXX_DELAY_CALLBACK_DISABLE_MAX_DURATION
/**
* @brief 一次tick 能执行任务的最长时间
*
* 若值不为0,则每次任务退出时在tick 函数中检查时间,若超时,后续的任务只更新倒计时,不执行。
* 与`_max_task_count_in_one_tick` 相同,若存在impatient 任务,则tick 函数在所有impatient 任务执行后才退出。
* 若_max_task_count_in_one_tick 和_max_duration_of_one_tick 都不为0,则两者同时生效,
* 只要超过其中一个的限制,tick 函数就不再执行任务。
*
* @param max_duration
*/
void set_max_duration_of_tick(uint16_t max_duration) {
_max_duration_of_one_tick = max_duration;
}
#endif
private:
/**
* @brief 创建新任务
*
* @param fptr
* @param delay_time 延迟时间,若值等于0,则立即执行一次,使用返回值创建任务
* 若返回值还是0,任务不会被加入列表
*
* @return IndexType 若任务添加失败,返回MaxTaskCount
*/
IndexType _add_task_to_list(CallbackPtr fptr, TimeType delay_time) {
do {
if (delay_time == 0) {
delay_time = _call_callback<CallbackPtr, TimeType>(fptr);
if (delay_time == 0) {
break;
}
}
if (not_full()) {
if (CallbackDestroyer::enabled()) {
if (!CallbackDestroyer::new_task(fptr)) {
break;
}
}
if (_current_task_count == 0) {
this->reset_last_tick_time();
}
uint8_t index;
for (index = 0; index < MaxTaskCount; ++index) {
if (_task_list[index].down_counter == 0)
break;
}
++_current_task_count;
_task_list[index].down_counter = delay_time;
_task_list[index].fptr = fptr;
set_new_task_bit(index);
return index;
}
} while (0);
return MaxTaskCount;
}
public:
/**
* @brief 创建一个普通任务,延时delay_time 后执行
*
* @param fptr
* @param delay_time 若值为0,则立即执行一次,然后再根据返回值加入任务列表
* 若返回值还是0,任务不会被加入列表
*
* @return IndexType 任务的索引。若任务添加失败,返回MaxTaskCount,
*/
IndexType add_normal_task(CallbackPtr fptr, TimeType delay_time) {
auto i = _add_task_to_list(fptr, delay_time);
clr_impatient_bit(i);
return i;
}
/**
* @brief 创建一个没耐心的任务,延时delay_time 后执行。
*
* 没耐心(impatient) 的任务是指:
*
* 1. 执行耗时较短;
* 2. 不允许因为tick 超时而被跳过不执行;
*
* @param fptr
* @param delay_time 若值为0,则立即执行一次,然后再根据返回值加入任务列表
* 若返回值还是0,任务不会被加入列表
*
* @return 任务的索引。若任务添加失败,返回MaxTaskCount,
*/
IndexType add_impatient_task(CallbackPtr fptr, TimeType delay_time) {
auto i = _add_task_to_list(fptr, delay_time);
set_impatient_bit(i);
return i;
}
private:
void _remove_task_from_list(IndexType index) {
if (index < MaxTaskCount) {
--_current_task_count;
_task_list[index].down_counter = 0;
if (CallbackDestroyer::enabled()) {
CallbackDestroyer::destroy(_task_list[index].fptr);
}
}
}
public:
/**
* @brief 将指定的任务删除,可以阻止任务下次执行
*
* @param index 任务的索引,即添加任务时获得的返回值
*/
void remove_normal_task(IndexType index) {
_remove_task_from_list(index);
}
/**
* @brief
*
* @param index
*/
void remove_impatient_task(IndexType index) {
_remove_task_from_list(index);
}
/**
* @brief 重置一个正在列表中的任务的倒计时
*
* 注意,这个函数无法判断任务有没有被删除
*
* @param index
* @param delay_time 若为0 则无动作
*/
void reset_task(IndexType index, TimeType delay_time) {
if (delay_time == 0)
return;
_task_list[index].down_counter = delay_time;
}
};
/**
* @brief 用来方便实现简单的轮询延时操作
*
* @tparam TimeSource
*/
template <typename TimeSource>
class TimeCycle {
public:
using TimeType = typename TimeSource::TimeType;
private:
TimeType _last_time;
public:
TimeCycle() {
_last_time = TimeSource::get_time();
}
void reset() {
_last_time = TimeSource::get_time();
}
/**
* @brief 周期性延时指定的时间
*
* 每次延时结束时返回true,同时自动更新参考时间,从而开启下一个延时周期。
*
* @param duration 时间
* @return true 一个延时周期结束
* @return false 当前延时周期未结束
*/
bool cycle(TimeType duration) {
if (this->delay(duration)) {
_last_time = TimeSource::get_time();
return true;
}
return false;
}
/**
* @brief 非阻塞延时指定的时间
*
* 延时结束不自动更新参考时间,一次延时结束后,返回值保持为true。
*
* @param duration 时间
* @return true 已经过了指定的时间
* @return false 延时尚未结束
*/
bool delay(TimeType duration) {
return (TimeSource::get_time() - _last_time) > duration;
}
};
/*
=========== Useless for users ===========
=========== 设计备份 =============
DelayCallback 的设计变更:
本来计划是让列表中的任务在加入时都按照延迟时间有序排列,从小到大,从而让快要到时间的任务在扫描时排在前面,
每次tick 中,要执行的任务都在列表头部的位置,或许可以让任务的时序表现更好,时间上比较紧的任务也能靠前执行。
但是首先,在性能方面,这样做会显著增大逻辑复杂度。每次插入任务都要耗时遍历任务列表,寻找插入位置。运行中,如果任务返回值不为0,
不能原地更新该任务的倒计时数值,还要先把任务取出,放进一个临时列表,之后要么一边执行任务,一边寻找临时列表中任务的插入位置,
要么列表执行完成后再一个一个把临时列表中的任务有序插入任务列表里,不用说,这又是很大的开销。
所以问题就是,如果任务函数只要很短的执行时间,那么一次tick 主要的时间消耗都是tick 函数本身贡献的。
另一方面,既然已经区分出了impatient 任务,对时序有需求的任务本来就可以优先执行。遍历没到时间的任务时只是做一次减法判断时间,
不会耽误后面的任务太长时间。况且,因为tick 本身是在主循环里手动调用的,时序上本来就做不好,所以有序列表的性价比不高。
综上,DelayCallback 改为LIFO 排列,最后添加的任务在表头,先执行;取消临时列表,任务返回值不为0 时,直接更新其倒计时数值。
这样做还有个优点,比如函数里临时添加一个一次性任务,延迟时间比较短,减少添加任务之类的附加事务的耗时,就可以更快得到执行tick 的机会。
不过如此一来,不再需要按顺序排列,新任务都在头部插入,似乎用不着链表的快速插入特性了。如果改用一个线性表,每个任务用一个标志,
或者用倒计时为0 表示任务删除,哪怕在任务结构体里加上一个标志,结果也就是和链表节点的体积一样了,内粗占用上不会比链表差。
因为数组不用像链表那样加个内存池管理,应该能减少不少代码体积。缺点在于,每次tick 都要遍历整个数组,包括没用到的部分,
因为任务是在头部添加的,但删除位置却是是随机的,所以列表中会存在空洞,要跳过这些位置继续向后扫描。当然可以放一个变量记录
总任务数,遍历到这个数字就不用继续了,但可能意义不大,而且数组遍历是很快的。
至于无耐心任务,因为不想分成两个数组存储,大概还是得在任务里加个标志。但是,无耐心任务和普通任务混在一起是有后果的,就是
tick 不能优先执行无耐心任务,“无耐心” 这个设置剩下的唯一用途就是不会被超时限制。
也有一个技巧可以避免无耐心任务被耗时的普通任务推迟,就是把超时时间设置为较小的值,一旦时间超过阈值,其他的普通任务这次都
不会再被执行,无耐心任务则畅通无阻。但这样又可能导致普通任务永远不会执行。
被跳过的任务会变成就绪状态,下一次`tick` 应该能执行它们,所以可能只是延迟了一个`tick` 函数的周期,
但要是恰好前面又有别的普通或无耐心任务导致超时,那前一次没被执行的任务就又得这么顺延下去,运气不好,就永远卡住了。
可以让就绪任务和无耐心任务一样,不管超时,一定会执行,但这么一来似乎超时控制就没用了。
或者加个计数器,只要连续超过N次`tick` 中都有任务被跳过了,下一次就临时关闭超时控制,把卡住的任务都清理掉,
这样可以保证任务最多只会被卡住N 个周期。
*/
} // namespace scheduler_basic
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。