介绍
-
都是进行goroutine 生命周期管理的
-
和errgroup 很相似
-
和oklog 相似
使用
demo v1
官方没有比较好用的例子,所以这里用一个网上的例子来说明,当然这里是v1版本
package main
import (
"gopkg.in/tomb.v1"
"log"
"sync"
"time"
)
type foo struct {
tomb tomb.Tomb
wg sync.WaitGroup
}
func (f *foo) task(id int) {
for i := 0; i < 10; i++ {
select {
case <-time.After(1e9):
log.Printf("task %d tick\n", id)
case <-f.tomb.Dying():
log.Printf("task %d stopping\n", id)
f.wg.Done()
return
}
}
}
func (f *foo) Run() {
f.wg.Add(10)
for i := 0; i < 10; i++ {
go f.task(i)
}
go func() {
f.wg.Wait()
f.tomb.Done()
}()
}
func (f *foo) Stop() error {
f.tomb.Kill(nil)
return f.tomb.Wait()
}
func main() {
var f foo
f.Run()
time.Sleep(3.5e9)
log.Printf("calling stop\n")
f.Stop()
log.Printf("all done\n")
}
-
在run里面会循环开启goroutine 执行任务,并监听tb 的dying 管道,并每隔时间打印id,
在main 里面,睡眠时间一过,会调用stop 去停止所有任务,先执行kill,再wait 所有任务完成。
demo v2
v2 版本比v1 更加友好,跟errgroup 很类似了
package main
import (
"errors"
"fmt"
"gopkg.in/tomb.v2"
"log"
"time"
)
type foo struct {
tomb tomb.Tomb
}
func (f *foo) task(id int) {
for i := 0; i < 10; i++ {
select {
case <-time.After(1e9):
log.Printf("task %d tick\n", id)
case <-f.tomb.Dying():
log.Printf("task %d stopping\n", id)
return
}
}
}
func (f *foo) Run() {
for i := 0; i < 10; i++ {
var tmp=i
f.tomb.Go(func() error {
if tmp==9{
time.Sleep(2*time.Second)
fmt.Println("出错了")
return errors.New("错了")
}
f.task(tmp)
return nil
})
}
}
func main() {
var f foo
f.Run()
f.tomb.Wait()
}
执行结果
2021/08/25 23:27:24 task 6 tick
2021/08/25 23:27:24 task 2 tick
2021/08/25 23:27:24 task 4 tick
2021/08/25 23:27:24 task 7 tick
2021/08/25 23:27:24 task 5 tick
2021/08/25 23:27:24 task 1 tick
2021/08/25 23:27:24 task 3 tick
2021/08/25 23:27:24 task 0 tick
2021/08/25 23:27:24 task 8 tick
出错了
2021/08/25 23:27:25 task 6 stopping
2021/08/25 23:27:25 task 5 stopping
2021/08/25 23:27:25 task 8 stopping
2021/08/25 23:27:25 task 4 stopping
2021/08/25 23:27:25 task 3 stopping
2021/08/25 23:27:25 task 1 stopping
2021/08/25 23:27:25 task 0 stopping
2021/08/25 23:27:25 task 7 stopping
2021/08/25 23:27:25 task 2 stopping
-
很简洁,通过Go 来执行任务,当出错了停止所有任务
源码解析
创建
var tb Tomb
tomb 结构
type Tomb struct {
m sync.Mutex
dying chan struct{}
dead chan struct{}
reason error
}
固定的error
var (
ErrStillAlive = errors.New("tomb: still alive")
ErrDying = errors.New("tomb: dying")
)
init
func (t *Tomb) init() {
t.m.Lock()
if t.dead == nil {
t.dead = make(chan struct{})
t.dying = make(chan struct{})
t.reason = ErrStillAlive
}
t.m.Unlock()
}
-
很多函数每次都会调用这个init,作用是初始化dead,dying channel,将reason 赋值为ErrStillAlive
dead
// Dead returns the channel that can be used to wait
// until t.Done has been called.
func (t *Tomb) Dead() <-chan struct{} {
t.init()
return t.dead
}
-
返回dead channel
dying
// Dying returns the channel that can be used to wait
// until t.Kill or t.Done has been called.
func (t *Tomb) Dying() <-chan struct{} {
t.init()
return t.dying
}
-
返回dying channel
wait
// Wait blocks until the goroutine is in a dead state and returns the
// reason for its death.
func (t *Tomb) Wait() error {
t.init()
<-t.dead
t.m.Lock()
reason := t.reason
t.m.Unlock()
return reason
}
-
阻塞等待goroutine 关闭了t.dead 这个通道,然后返回reason. 什么时候dead 会关闭呢?
-
从源码来看,只有操作done 的时候,会关闭这个dead,所以肯定最后还是要调用done 方法,不然,wait 会一直阻塞
done(v1 版本才有)
// Done flags the goroutine as dead, and should be called a single time
// right before the goroutine function or method returns.
// If the goroutine was not already in a dying state before Done is
// called, it will be flagged as dying and dead at once with no
// error.
func (t *Tomb) Done() {
t.Kill(nil)
close(t.dead)
}
-
关闭dead channel
kill killf
// Kill flags the goroutine as dying for the given reason.
// Kill may be called multiple times, but only the first
// non-nil error is recorded as the reason for termination.
//
// If reason is ErrDying, the previous reason isn't replaced
// even if it is nil. It's a runtime error to call Kill with
// ErrDying if t is not in a dying state.
func (t *Tomb) Kill(reason error) {
t.init()
t.m.Lock()
defer t.m.Unlock()
if reason == ErrDying {
if t.reason == ErrStillAlive {
panic("tomb: Kill with ErrDying while still alive")
}
return
}
if t.reason == nil || t.reason == ErrStillAlive {
t.reason = reason
}
// If the receive on t.dying succeeds, then
// it can only be because we have already closed it.
// If it blocks, then we know that it needs to be closed.
select {
case <-t.dying:
default:
close(t.dying)
}
}
// Killf works like Kill, but builds the reason providing the received
// arguments to fmt.Errorf. The generated error is also returned.
func (t *Tomb) Killf(f string, a ...interface{}) error {
err := fmt.Errorf(f, a...)
t.Kill(err)
return err
}
-
如果原因是dying ,那么判断下状态,如果状态依然是alive 那么出错panic
-
满足条件将reason 赋值给tomb 的reason,可见只能赋值一次,捕获到第一个error
-
关闭dying 管道,这样所有监听在上面的goroutine 就会全部返回,主要是default 执行的,上面还有case 是判断该channel 是否已经关闭,如果关闭了就直接返回了
err
// Err returns the reason for the goroutine death provided via Kill
// or Killf, or ErrStillAlive when the goroutine is still alive.
func (t *Tomb) Err() (reason error) {
t.init()
t.m.Lock()
reason = t.reason
t.m.Unlock()
return
}
-
返回reason
v2 版本的Go
// Go runs f in a new goroutine and tracks its termination.
//
// If f returns a non-nil error, t.Kill is called with that
// error as the death reason parameter.
//
// It is f's responsibility to monitor the tomb and return
// appropriately once it is in a dying state.
//
// It is safe for the f function to call the Go method again
// to create additional tracked goroutines. Once all tracked
// goroutines return, the Dead channel is closed and the
// Wait method unblocks and returns the death reason.
//
// Calling the Go method after all tracked goroutines return
// causes a runtime panic. For that reason, calling the Go
// method a second time out of a tracked goroutine is unsafe.
func (t *Tomb) Go(f func() error) {
t.init()
t.m.Lock()
defer t.m.Unlock()
select {
case <-t.dead:
panic("tomb.Go called after all goroutines terminated")
default:
}
t.alive++
go t.run(f)
}
-
如果t.dead 被关闭了,那么将会报错,因为这个通道必须等所有goroutines 执行完才会关闭
-
启动goroutine 执行任务,将执行的存活任务+1
v2 版本的 Run
func (t *Tomb) run(f func() error) {
err := f()
t.m.Lock()
defer t.m.Unlock()
t.alive--
if t.alive == 0 || err != nil {
t.kill(err)
if t.alive == 0 {
close(t.dead)
}
}
}
-
执行函数调用,如果有错误产生,或者并发调用的函数为0 ,将执行kill函数,结束所有goroutine
-
最后关闭dead,这样wait 就能直接退出,所以不需要done 了,要不然还的和waitgroup 配合,太麻烦了
总结
-
v1 版本让我感觉比较优雅的更好的是oklog 和errgroup
-
v2 版本则增加Go 这个关键字,降低了使用成本