https://github.com/nextzhou/workpool
fork-join style goroutines flow controler
https://github.com/nextzhou/workpool
errgroup flowcontroller forkjoin go golang goroutines structured-concurrency
Last synced: 2 months ago
JSON representation
fork-join style goroutines flow controler
- Host: GitHub
- URL: https://github.com/nextzhou/workpool
- Owner: nextzhou
- License: mit
- Created: 2021-09-14T10:34:20.000Z (over 4 years ago)
- Default Branch: master
- Last Pushed: 2024-01-07T14:38:47.000Z (about 2 years ago)
- Last Synced: 2024-01-08T15:17:43.302Z (about 2 years ago)
- Topics: errgroup, flowcontroller, forkjoin, go, golang, goroutines, structured-concurrency
- Language: Go
- Homepage:
- Size: 63.5 KB
- Stars: 5
- Watchers: 2
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.cn.md
- License: LICENSE
Awesome Lists containing this project
README
# WorkPool
[](https://golang.org/)
[](https://github.com/nextzhou/workpool/actions/workflows/go.yml)
[](https://github.com/nextzhou/workpool/actions/workflows/golangci-lint.yml)
[](https://goreportcard.com/report/github.com/nextzhou/workpool)
[English Document](README.md)
workpool 实现了一个 [fork-join](https://zh.wikipedia.org/wiki/Fork-join%E6%A8%A1%E5%9E%8B) 模型的结构化并发库,使得并发任务更安全、可控。
```go
// 新建 Workpool,并限制最大并发数为 4
wp := workpool.New(context.TODO(), workpool.Options.ParallelLimit(4))
for _, task := range tasks {
task := task // Shadowing the task variable
wp.Go(func(ctx context.Context) error { // 在这里异步执行子任务
return task(ctx)
})
}
err := wp.Wait() // 在这里等待所有任务完成,并处理错误与 panic
```
## 核心特性
- [x] 轻量级的 fork-join 并发模型,惰性扩展工作协程。
- [x] 收集子任务的错误,并在 `Workpool.Wait()` 函数中汇总。
- [x] 子任务 panic 将在父协程中抛出,从而避免导致整个进程崩溃。
- [x] 通过`Context`控制子任务生命周期,使得所有工作协程能保证在 `Workpool.Wait()` 都被即时释放。
- [x] 脱离 `Workpool` 的单个 `Task` 也可以安全地异步执行。
- [x] 分阶段任务,可交互式地从异步任务中获取阶段性结果
- [ ] 支持基于 `channel` 生产-消费者 的任务,生产者任务全部完成后自动通知消费者任务(依赖泛型)
## 设计
`New()`、`Go()`、`Wait()` 三段式分别对应 `config`、`fork`、`join`
### Options
`Option` 可在 `New()` 传入,例如 `wp := New(ctx, Options.TaskTimeout(time.Second), Options.Chain(Wraps.PanicAsErr))`
| Option | 功能 |
|:---------------------------------------|:------------------------------------------------------------------------|
| Options.TaskTimeout(time.Duration) | 为每个任务设置独立的超时 |
| Options.ParallelLimit(uint) | 子任务最大并发限制 |
| Options.ExitTogether() | 当有任意子任务完成时通知其他子任务退出,一般在启动多个常驻服务时使用 |
| Options.WrapsChain(...wpcore.TaskWrap) | 为每个`Task`添加传入的`wpcore.TaskWrap`,作用顺序从左至右 |
| Options.Recover(wpcore.Recover) | 自定义当子任务panic时如何处理 |
| Options.SkipPendingTask(bool) | 默认情况下,就算`ctx`结束了,后续添加的 `Task` 也不会被跳过。添加该选项后则可以直接跳过 `ctx` 结束后添加的新 `Task` |
### Wraps
`TaskWrap` 将 `Task` 包装成新的 `Task`,例如记录 metrics 等等, 可以按照需求自行扩展。
一般与 `Options.WrapsChain()` 配合使用,可自动应用到所有 `Task` 上。
```go
wp := New(ctx, Options.WrapsChain(Wraps.PanicAsErr)) // 配合 Options.WrapsChain() 使用
wp.Go(Wraps.PanicAsErr(task)) // 单独对某个 Task 使用
```
| TaskWrapper | 功能 |
|:-------------------|:--------------------------------------------------------|
| Wraps.PanicAsError | 子任务 panic 会转换成错误 |
| Wraps.Phased | 将分阶段任务转成普通任务, 详见 [分阶段任务](#分阶段任务) |
| Wraps.RunStopTask | 将某些停止执行单独出来的任务转换为ctx控制的任务, 详见 [单独Stop函数任务](#单独stop函数任务) |
## 单任务
有时只需要异步地执行单个任务,过后再检查其执行结果。 这时如果再使用 `Workpool` 就显得过于繁琐了。
不过我们还可以调用 `Task.Go(context.Context)` 启动异步任务,而无需新建 `Workpool`。
该函数会返回一个`wpcore.TaskWait`,它是`func() error` 的别名,执行返回的 `TaskWait` 时会等待任务结束并返回结果。
```go
task := workpool.Task(func(context.Context) error {
// Order a coffee.
})
waitCoffee := task.Go(context.TODO())
// Save the world.
// Blah blah blah
if err := waitCoffee(); err == nil {
// Enjoy your coffee.
}
```
与在 `Workpool` 中执行 `Task` 一致,`Task` 中的所有错误或 `panic` 都会收集到 `Wait()` 中抛出。同时你也可以使用 `Wraps.PanicAsError` 包装需要异步执行的单任务。
## 分阶段任务
分阶段任务提供一种与异步任务交互的手段,通过一个例子我们就很容易理解:
> 我们有一个异步执行的定时更新数据任务,但在启动时第一次更新必须成功。
在没有分阶段任务时常规的解决方法时将第一次更新单独执行,剩下的部分作为一个`Task`异步执行。
```go
// Constructing `wp`, `ctx`...
err := initTask(ctx)
if err != nil {
return err
}
wp.Go(func(ctx context.Context) error {
// task balabala
})
```
但这样的问题是初始化部分就无法也异步处理了(如果有多个这样的任务时是很有必要的),
而且单个任务的逻辑被拆散,不方便维护。
如果有了分阶段任务,这个问题就很好解决了:
```go
// construct wp、ctx ...
task, supervisor := Wraps.Phased(func(ctx context.Context, helper wpcore.PhasedTaskHelper) error {
err := taskInit(ctx)
if err != nil {
return err
}
// Task initialization is complete. Let's mark this milestone.
helper.MarkAMilestone(taskInitOk)
// Process the remaining parts of the task.
})
wp.Go(task)
initResult, status := supervisor.WaitMilestoneOrCancel(ctx)
```
在分阶段任务中,我们可以通过调用 0 或多次 `helper.MarkAMilestone(interface{})`
来记录阶段性成果。
这有点类似于其他语言中 Generator 中的 yield 操作,
但区别在于分阶段任务在 `MarkAMileston` 之后并不会挂起,而是会继续执行。
在任务外,我们可以通过 `Wraps.Parsed()` 返回的 `PhasedTaskSupervisor` 来与任务交互,
达到确认阶段性成果、或者设置阶段性成果的 Deadline 超过则取消等操作:
|函数| 功能 |
|:---|:------------------|
|WaitMilestone| 等待一个里程碑 |
|WaitMilestoneOrCancel| 等待一个里程碑,若超时了则取消任务 |
另外,通过 `WaitMilestone` 系列函数中,除了返回里程碑还会返回一个 `PhasedTaskStatus`,
通过该值可以判断函数返回时的状态:
|状态|说明| 备注 |
|:---|:---|:--------------------------|
|IsOK()| 成功取到里程碑| |
|IsContextDone()|ctx done 并且未能取到里程碑| 可能与 IsTaskNotRunning() 共存 |
|IsTaskDone()|任务结束了,但并没有产生里程碑||
|IsTaskNotRunning()| ctx done 时还任务还为开始运行| 一定会与 IsContextDone() 共存 |
## 单独Stop函数任务
有些现有的长时间执行的服务并不是由`ctx`来控制停止,
而是提供一个单独的 `Stop`/`Close` 之类的函数来控制关闭。
例如 `http.Server` 通过 `Serve()` 函数启动服务、通过 `net.Listener.Close()` 停止服务。
`Wraps.RunStopTask()` 提供一个简单的包装将这类型的任务转换成 `workpool.Task` 类型。
还是以 `http.Server` 为例,通过以下代码即可转换成 `workpool.Task` 任务:
```go
task := Wraps.RunStopTask(func() error { // Running function.
err := srv.Serve(l)
if errors.Is(err, http.ErrServerClosed) { // Ignore the ServerClosed error.
return nil
}
return err
}, func() error { // Stopping function.
return l.Close()
})
```