标准 time.Ticker 不适合复杂任务调度,因其仅定时发信号,不管理任务生命周期、不处理 panic/阻塞/超时,无法动态增删任务,且多任务共用通道易导致堆积或静默丢失;应改用 sync.Map + time.AfterFunc 实现可注册注销的任务池,并结合 context.WithTimeout 和 errgroup.Group 确保超时控制与 goroutine 干净退出。

如何使用Golang实现并发任务调度器_Golang定时任务与goroutine管理实践

为什么标准 time.Ticker 不适合复杂任务调度

它只负责“按时发信号”,不管理任务生命周期,也不处理 panic、阻塞或超时。一旦某个 goroutine 挂住或 panic,整个 ticker 循环可能卡死或静默丢失任务。

sync.Map + time.AfterFunc 实现可注册/注销的任务池

避免全局 ticker 压力,每个任务用独立的定时触发,靠 sync.Map 管理活跃句柄,注销时调用 Stop() 清理资源。

type Scheduler struct {
	tasks sync.Map // map[string]*time.Timer
}

func (s *Scheduler) Add(name string, delay time.Duration, f func()) {
	timer := time.AfterFunc(delay, func() {
		defer func() {
			if r := recover(); r != nil {
				log.Printf("task %s panicked: %v", name, r)
			}
		}()
		f()
		s.Add(name, delay, f) // 自动重入(周期任务)
	})
	s.tasks.Store(name, timer)
}

func (s *Scheduler) Remove(name string) {
	if v, ok := s.tasks.Load(name); ok {
		if timer, ok := v.(*time.Timer); ok {
			timer.Stop()
		}
		s.tasks.Delete(name)
	}
}

context.WithTimeout 控制单个任务执行时长

防止某个任务长期阻塞,拖垮整个调度器。不能只依赖外部超时,要在任务内部主动检查 context。

func withTimeout(ctx context.Context, timeout time.Duration, f func(context.Context)) {
	ctx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()
	f(ctx)
}

// 使用示例:
sched.Add("fetch-api", 5*time.Second, func() {
	withTimeout(context.Background(), 3*time.Second, func(ctx context.Context) {
		resp, err := http.DefaultClient.GetContext(ctx, "https://api.example.com")
		if err != nil {
			if ctx.Err() == context.DeadlineExceeded {
				log.Println("fetch-api timed out")
			}
			return
		}
		defer resp.Body.Close()
		// ...
	})
})

goroutine 泄漏的两个隐蔽点

一是未清理的 time.Timer,二是任务中启动了子 goroutine 却没做生命周期绑定。

真正难的不是启动一堆 goroutine,而是让它们在该停的时候干净退出。多数调度器崩掉,都是因为某次注销漏掉了一个 timer 或一个 goroutine。

本文转载于:互联网 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。