深入解析 gopkg.in/tomb

介绍

  • 都是进行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 这个关键字,降低了使用成本

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值