前言

之前字节三面被问到了如何实现一个定时任务,以及定时任务的存储,用什么数据结构相关的问题。面试当时整个人都愣住了,面试结束后关于这个问题思考了很久,也和朋友讨论过这个问题,关于数据结构我还是觉得hashmap是个不错的选择,但是其实维护一个有序的双向链表好像也有点麻烦,所以还有待考究。
目前是先完成了一个简单的定时任务,使用到的数据结构是非常简单的动态数组,后面再想想怎么去优化,另外还有一些问题是要在控制协程创建数量的同时保证每个任务都能按时完成,这是后期需要优化的。

思路

说到定时,肯定在等待时间到的这一段时间里是不能占着CPU的,同时我们要知道要等待多少时间。所以我们选择协程(线程,如果是在java语言或者其他什么语言中)睡眠来完成这个任务,当然wait也可以,两个方法选一个就行,思路都是一样的。Java的Timer底层就是使用的wait方法。

实现
package main

import (
	"fmt"
	"log"
	"sort"
	"sync"
	"time"
)

// 暂时用且切片保存任务列表
var taskList = ScheduleTaskSort{
	// 默认先初始化16个
	// 第一个参数是长度,第二个参数是容量
	tasks: make([]ScheduleTask, 0, 16),
}

// 已完成的任务列表
var finishTaskList = make([]ScheduleTask, 0, 16)
// 正在进行的任务列表
var runningTaskList = make([]ScheduleTask, 0, 16)

// 读写锁用于任务列表
var mu = sync.RWMutex{}

// 未完成的任务数量
var taskNum = 0
// 已完成的任务数量
var finishTaskNum = 0
// 正在进行中的任务数量
var runningTaskNum = 0

type ScheduleTask struct {
	// 定时时间
	Time int64
	// 任务
	Task func()
}

type ScheduleTaskSort struct {
	// 根据时间进行一个定时任务的排序
	sort.Interface
	tasks []ScheduleTask
}

func (s ScheduleTaskSort) Len() int {
	return len(taskList.tasks)
}

func (s ScheduleTaskSort) Less(i, j int) bool {
	return taskList.tasks[i].Time <= taskList.tasks[j].Time
}

func (s ScheduleTaskSort) Swap(i, j int) {
	taskList.tasks[i], taskList.tasks[j] = taskList.tasks[j], taskList.tasks[i]
}

// 初始化
// TODO 从文件中读取待完成和已完成的任务
func init() {
	
}

func main() {
	scheduleTaskNew(2021, 4, 8, 9, 46, func() {
		fmt.Println("lalalala成功啦!")
	})
	scheduleTaskNew(2021, 4, 8, 9, 47, func() {
		fmt.Println("lalalala成功啦...!")
	})
	schedule()
}

// 生成任务
// 返回 true  -> 任务生成成功
// 返回 false -> 任务生成失败
func ScheduleTaskNew(year int, month int, day int, hour int, min int, task func()) bool {
	// 需要根据用户给定的时间 比如 2021:04:07 13:00
	// 如果小于当前时间则放弃掉任务并提醒用户
	// 根据输入的时间生成时间戳
	taskTime := time.Date(year, time.Month(month), day, hour, min, 0, 0, time.Local).Unix()
	// 对比当前时间的时间戳
	if taskTime-time.Now().Unix() < 0 {
		return false
	}
	// 生成定时任务
	scheduleTask := ScheduleTask{
		Time: taskTime,
		Task: task,
	}
	// 添加到任务列表
	taskAdd(scheduleTask)
	return true
}

// 向任务链表中增加任务
func taskAdd(task ScheduleTask) {
	// 加锁
	mu.Lock()
	if taskNum < len(taskList.tasks) {
		if task.Time < taskList.tasks[0].Time {
			// 将task放到第一个
			temp := append([]ScheduleTask{}, taskList.tasks[0:]...)
			taskList.tasks[0] = task
			taskList.tasks = append(taskList.tasks, temp...)
		} else {
			taskList.tasks[taskNum] = task
		}
	} else {
		if task.Time < taskList.tasks[0].Time {
			// 将task放到第一个
			temp := append([]ScheduleTask{}, taskList.tasks[0:]...)
			taskList.tasks[0] = task
			taskList.tasks = append(taskList.tasks, temp...)
		} else {
			// 添加到任务列表里
			taskList.tasks = append(taskList.tasks, task)
			// 根据时间进行从小到大排序
			sortList()
		}
	}
	taskNum++
	// 解锁
	mu.Unlock()
}

// 对任务列表进行从大到小排序
func sortList() {
	sort.Sort(taskList)
}

// 任务执行
func taskExec(task ScheduleTask) {

	// 将已执行的任务保存到已执行任务列表
	if finishTaskNum < len(finishTaskList) {
		finishTaskList[finishTaskNum] = task
	} else {
		finishTaskList = append(finishTaskList, task)
	}
	finishTaskNum++

	// 这里是一些日志打印
	log.Println("开始睡眠")
	// time.Unix()返回的是单位为秒的时间戳
	log.Println("时间差:", task.Time - time.Now().Unix())
	// 这里睡眠时间要注意时间单位的转换
	time.Sleep(time.Second * time.Duration(task.Time - time.Now().Unix()))
	log.Println("睡眠结束,开始执行任务")
	// 执行
	task.Task()
}

// 返回当前所有未完成的任务
func ShowTaskList() []ScheduleTask {
	// TODO 应该加上已经起协程等待运行的任务列表
	temp := taskList.tasks
	fmt.Println(taskList.tasks)
	return temp
}

// 返回当前所有已完成的任务
func ShowFinishTaskList() []ScheduleTask {
	temp := finishTaskList
	return temp
}

// 任务调度
func Schedule() {
	log.Println("开始调度....")
	for {
		// TODO 当待执行任务数量等于0时进行阻塞,不然一直循环会导致CPU资源浪费
		// TODO 对新建协程数进行一个控制,同时要考虑到时间差上的问题
		if taskNum > 0 {
			// 从taskList里取任务并执行
			go taskExec(taskList.tasks[0])
			// 加锁
			mu.Lock()
			// TODO 将正在进行中的任务放入到正在进行的任务列表中

			// 删除任务
			taskList.tasks = taskList.tasks[1:]
			// 任务数量 - 1
			taskNum--
			// 解锁
			mu.Unlock()
		}
	}
}

如代码中所写的TODO,等都解决掉这些问题,再放在用golang实现定时任务(二)中,我之后还打算写一个对应的前端页面用于增删改查这些定时任务。

如有错误或者可以优化的地方,欢迎指正!