在Golang的并发编程体系中,channel作为goroutine之间传递数据的管道,天然适合用来实现任务调度逻辑。通过channel传递任务对象,配合多个worker goroutine消费任务,可以轻松搭建出灵活的任务分发与执行架构。

channel实现任务调度的基础原理
任务调度的核心逻辑可以拆分为三个部分:任务生成、任务分发、任务执行。channel在其中承担任务传递的载体角色,任务生成方将任务写入channel,多个worker goroutine从channel中读取任务并执行,整体流程通过channel的阻塞特性实现天然的同步控制。
首先需要定义统一的任务结构,方便后续扩展任务属性:
// 定义任务结构体,包含任务ID和执行逻辑
type Task struct {
ID int
Do func() // 任务执行的具体函数
}
基础版任务调度实现
基础版调度适合任务量不大、不需要复杂控制的场景,核心是使用一个共享的channel传递任务,启动固定数量的worker goroutine消费任务。
1. 初始化调度组件
先创建任务channel和启动对应数量的worker:
package main
import (
"fmt"
"sync"
"time"
)
// 定义任务结构体
type Task struct {
ID int
Do func()
}
func main() {
// 创建有缓冲的任务channel,缓冲大小为10
taskCh := make(chan Task, 10)
// 等待所有任务执行完成
var wg sync.WaitGroup
// 启动3个worker goroutine执行任务
workerNum := 3
for i := 0; i < workerNum; i++ {
go worker(i, taskCh, &wg)
}
// 生成5个任务写入channel
for i := 0; i < 5; i++ {
taskID := i
t := Task{
ID: taskID,
Do: func() {
fmt.Printf("执行任务 %dn", taskID)
time.Sleep(time.Second) // 模拟任务执行耗时
},
}
wg.Add(1)
taskCh <- t
}
// 关闭channel,告知worker没有新任务了
close(taskCh)
// 等待所有任务执行完成
wg.Wait()
fmt.Println("所有任务执行完成")
}
// worker函数,从channel读取任务并执行
func worker(workerID int, taskCh <-chan Task, wg *sync.WaitGroup) {
for task := range taskCh {
fmt.Printf("worker %d 接收到任务 %dn", workerID, task.ID)
task.Do()
wg.Done()
}
fmt.Printf("worker %d 退出n", workerID)
}
2. 代码逻辑说明
- 任务channel使用有缓冲类型,避免任务生成方频繁阻塞,缓冲大小可以根据实际任务生成速度调整
- 使用
sync.WaitGroup跟踪所有任务的执行状态,确保所有任务完成后再退出主程序 - 任务生成完成后关闭channel,worker通过
for range读取channel时会在channel关闭且数据读完后自动退出循环
进阶版:支持动态控制调度流程
实际场景中可能需要支持暂停调度、动态调整worker数量等需求,可以通过额外的控制channel实现。
1. 增加调度控制逻辑
新增一个控制channel用来接收调度指令,比如暂停、恢复、退出等:
package main
import (
"fmt"
"sync"
"time"
)
type Task struct {
ID int
Do func()
}
// 调度控制指令
type ControlCmd int
const (
CmdContinue ControlCmd = iota // 继续调度
CmdPause // 暂停调度
CmdStop // 停止调度
)
func main() {
taskCh := make(chan Task, 10)
ctrlCh := make(chan ControlCmd, 1)
var wg sync.WaitGroup
// 启动调度器,负责分发任务和控制调度流程
go scheduler(taskCh, ctrlCh, &wg)
// 启动worker
workerNum := 3
for i := 0; i < workerNum; i++ {
go worker(i, taskCh, &wg)
}
// 模拟生成任务
go func() {
for i := 0; i < 10; i++ {
taskID := i
t := Task{
ID: taskID,
Do: func() {
fmt.Printf("执行任务 %dn", taskID)
time.Sleep(500 * time.Millisecond)
},
}
wg.Add(1)
// 写入任务前先检查调度状态
select {
case taskCh <- t:
fmt.Printf("任务 %d 已加入队列n", taskID)
case cmd := <-ctrlCh:
if cmd == CmdStop {
fmt.Println("调度已停止,不再添加新任务")
return
} else if cmd == CmdPause {
fmt.Println("调度已暂停,等待恢复")
// 等待恢复指令
<-ctrlCh
// 重新尝试写入任务
taskCh <- t
}
}
time.Sleep(300 * time.Millisecond)
}
close(taskCh)
}()
// 模拟控制调度流程
time.Sleep(2 * time.Second)
fmt.Println("发送暂停指令")
ctrlCh <- CmdPause
time.Sleep(2 * time.Second)
fmt.Println("发送恢复指令")
ctrlCh <- CmdContinue
time.Sleep(2 * time.Second)
fmt.Println("发送停止指令")
ctrlCh <- CmdStop
wg.Wait()
fmt.Println("所有流程结束")
}
func scheduler(taskCh chan<- Task, ctrlCh <-chan ControlCmd, wg *sync.WaitGroup) {
// 调度器可以根据需要动态调整任务分发逻辑
// 这里仅做控制指令转发示例
_ = taskCh
_ = ctrlCh
_ = wg
}
func worker(workerID int, taskCh <-chan Task, wg *sync.WaitGroup) {
for task := range taskCh {
fmt.Printf("worker %d 处理任务 %dn", workerID, task.ID)
task.Do()
wg.Done()
}
fmt.Printf("worker %d 退出n", workerID)
}
常见问题与注意事项
- 避免goroutine泄漏:如果任务channel没有正确关闭,worker会一直阻塞在读取channel的操作上,导致goroutine无法退出,造成泄漏
- 任务执行异常捕获:如果
task.Do内部可能出现panic,需要在worker中增加recover逻辑,避免单个任务异常导致整个worker退出 - channel容量设置:有缓冲channel的容量需要根据任务生成速度和worker处理速度合理设置,过小会导致任务生成方频繁阻塞,过大可能会占用过多内存
- 关闭channel的时机:只能在任务全部生成完成后关闭任务channel,且关闭后不能再往channel中写入数据,否则会触发panic
适用场景总结
使用channel实现任务调度适合轻量级、不需要复杂调度策略的场景,比如后台异步任务处理、批量数据处理、请求限流等。如果项目需要更复杂的调度功能,比如任务优先级、定时任务、失败重试等,可以结合channel和第三方调度库一起使用,能大幅降低开发成本。