go的singleFlight学习

Package singleflight provides a duplicate function call suppression mechanism
“golang.org/x/sync/singleflight”

原来底层是 waitGroup,我还以为等待的协程主动让出 cpu 了,没想到 waitGroup.Wait() 阻塞了
doCall 不但返回值是 func 的 val 和 error,而且 doCall 内部也给 chan 写入了一遍
这样外部的同步 Do 和异步 DoChan 都能复用了

当 Do->doCall 执行 fn 发生 panic 时:
对首发请求,直接在 defer 中把 fn 中捕获的 panic 进行回放
对非首发请求,c.wg.Wait() 结束之后,对 c.err 进行断言,判断是否是一个 panic 错误,如是则回放
这样就保证了首发请求和非首发请求都发生了 panic

一个协程对waitGroup进行Add(1)操作后,多个协程都可以监听它的读

package singleflight

import (
	"bytes"
	"errors"
	"fmt"
	"runtime"
	"runtime/debug"
	"sync"
)

// errGoexit indicates the runtime.Goexit was called in
// the user given function.
// 用户给定的函数中,调用了 runtime.Goexit
var errGoexit = errors.New("runtime.Goexit was called")

// A panicError is an arbitrary value recovered from a panic
// with the stack trace during the execution of given function.
// 执行给定函数期间,panicError 是一个从 panic 中收到的任意值
// 带有栈追踪
type panicError struct {
	// value 中存储 error
	value interface{}
	stack []byte
}

// Error implements error interface.
func (p *panicError) Error() string {
	return fmt.Sprintf("%v\n\n%s", p.value, p.stack)
}

func (p *panicError) Unwrap() error {
	err, ok := p.value.(error)
	if !ok {
		return nil
	}

	return err
}

func newPanicError(v interface{}) error {
	stack := debug.Stack()

	// The first line of the stack trace is of the form "goroutine N [status]:"
	// but by the time the panic reaches Do the goroutine may no longer exist
	// and its status will have changed. Trim out the misleading line.
	// 去掉误导性的信息
	// 栈帧第一行,是"goroutine N [status]:"的信息
	if line := bytes.IndexByte(stack[:], '\n'); line >= 0 {
		stack = stack[line+1:]
	}
	return &panicError{value: v, stack: stack}
}

// call is an in-flight or completed singleflight.Do call
type call struct {
	wg sync.WaitGroup

	// These fields are written once before the WaitGroup is done
	// and are only read after the WaitGroup is done.
	// WaitGroup Done 之前,这两个字段只会被写入一次
	// WaitGroup Done 之后,才能读取
	val interface{}
	err error

	// These fields are read and written with the singleflight
	// mutex held before the WaitGroup is done, and are read but
	// not written after the WaitGroup is done.
	// WaitGroup Done 之前,singleflight mutex 持有它的期间,这些字段被读取和写入
	// WaitGroup Done 之后,仅用于读取,不再被写入
	dups  int
	chans []chan<- Result
}

// Group represents a class of work and forms a namespace in
// which units of work can be executed with duplicate suppression.
// Group 代表一个 work 类,形成一个 namespace
// 在该命名空间中(in which),可以通过重复抑制(duplicate suppression)来执行工作单元
type Group struct {
	mu sync.Mutex       // protects m
	m  map[string]*call // lazily initialized
}

// Result holds the results of Do, so they can be passed
// on a channel.
type Result struct {
	Val    interface{}
	Err    error
	Shared bool
}

// Do executes and returns the results of the given function, making
// sure that only one execution is in-flight for a given key at a
// time. If a duplicate comes in, the duplicate caller waits for the
// original to complete and receives the same results.
// The return value shared indicates whether v was given to multiple callers.
// 
// duplicate caller 会等待在 singleFlight 上,等待最开始的任务执行结束
// 返回的值 shared ,指示是否要将结果共享给其他 caller
func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
	g.mu.Lock()
	if g.m == nil {
		g.m = make(map[string]*call)
	}
	if c, ok := g.m[key]; ok {
		c.dups++
		g.mu.Unlock()

		// 等待 waitGroup Done
		c.wg.Wait()

		// 此时一定是 waitGroup Done 了
	
		// 发生了 panic ,不能吞掉panic错误
		// 发生了 error 
		if e, ok := c.err.(*panicError); ok {
			panic(e)
		} else if c.err == errGoexit {
			// 优雅地退出 goroutine,防止对上游协程产生干扰
			runtime.Goexit()
		}
		
		// 返回最终结果
		return c.val, c.err, true
	}

	// 第一次进来的时候,执行这里
	c := new(call)
	// waitGroup 计数从 0 -> 1
	c.wg.Add(1)
	g.m[key] = c
	g.mu.Unlock()

	g.doCall(c, key, fn)
	return c.val, c.err, c.dups > 0
}

// doCall handles the single call for a key.
func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
	normalReturn := false
	recovered := false

	// use double-defer to distinguish panic from runtime.Goexit,
	// more details see https://golang.org/cl/134395
	// double-defer 以区分panic 和runtime.Goexit
	defer func() {
		// the given function invoked runtime.Goexit
		// 把当前的堆栈给记录下来
		// normalReturn=true,正常结束
		// normalReturn=false && recovered == true,panic,需要外部还原panic的堆栈
		// normalReturn=false && recovered == false,go协程主动退出,需要制造一个err
		if !normalReturn && !recovered {
			c.err = errGoexit
		}

		g.mu.Lock()
		defer g.mu.Unlock()
		c.wg.Done()
		if g.m[key] == c {
			delete(g.m, key)
		}

		if e, ok := c.err.(*panicError); ok {
			// In order to prevent the waiting channels from being blocked forever,
			// needs to ensure that this panic cannot be recovered.
			if len(c.chans) > 0 {
				// goroutine的崩溃不会影响主goroutine或其他goroutine。
				go panic(e)
				// 能让panic爆出来
				select {} // Keep this goroutine around so that it will appear in the crash dump.
			} else {
				panic(e)
			}
		} else if c.err == errGoexit {
			// Already in the process of goexit, no need to call again
		} else {
			// Normal return
			for _, ch := range c.chans {
				ch <- Result{c.val, c.err, c.dups > 0}
			}
		}
	}()

	func() {
		defer func() {
			if !normalReturn {
				// Ideally, we would wait to take a stack trace until we've determined
				// whether this is a panic or a runtime.Goexit.
				// 理想情况下,会等到确定是否是 panic/runtime.Goexit 后才进行堆栈跟踪
				//
				// Unfortunately, the only way we can distinguish the two is to see
				// whether the recover stopped the goroutine from terminating, and by
				// the time we know that, the part of the stack trace relevant to the
				// panic has been discarded.
				// 不幸的是,我们区分两者的唯一方法是查看 recover 是否阻止了 goroutine 终止
				// 而当我们知道这一点时,与 panic 相关的堆栈跟踪部分已被丢弃。
				// 把 recover 拦住之后,返回一个 error ,然后在外部再进行放大,杀人于无形,让外部不知道singleFlight
				if r := recover(); r != nil {
					c.err = newPanicError(r)
				}
			}
		}()

		c.val, c.err = fn()
		normalReturn = true
	}()

	if !normalReturn {
		recovered = true
	}
}

// Forget tells the singleflight to forget about a key.  Future calls
// to Do for this key will call the function rather than waiting for
// an earlier call to complete.
// 如果不想等之前的 singleflight 返回,则在 map[string]*call 中删除之前的 key 
func (g *Group) Forget(key string) {
	g.mu.Lock()
	delete(g.m, key)
	g.mu.Unlock()
}

时序分析
首发请求,先在Do中制造call,然后 c.wg.Add(1),然后将其放到map中

c.wg.Add(1)
g.m[key] = c

首发结束时,在doCall的defer中,先 c.wg.Done(),然后将任务从map中移除:delete(g.m, key)

c.wg.Done()
if g.m[key] == c {
	delete(g.m, key)
}

首发请求和首发结束都在锁的操作下执行。
所以抢到锁的时候,要么是首发请求执行请求的开始,要么是首发请求执行请求的结束

附录

一石激起千层浪

sync.WaitGroup 反向运用

func TestDemo333(t *testing.T) {
	var wg sync.WaitGroup
	wg.Add(1)

	for i := 1; i <= 3; i++ {
		go func(taskID int) {
			fmt.Println(i, "before wait")
			wg.Wait()
			fmt.Println(i, "wait finish")
		}(i)
	}

	time.Sleep(4 * time.Second)
	fmt.Println("main before send done")
	wg.Done() // 在协程结束时,调用Done方法
	fmt.Println("main after send done")
	select {}
}

debug.Stack()

属于 runtime/debug 包
用于获取当前程序的堆栈跟踪(stack trace),通常用于调试和错误处理

当调用 stack := debug.Stack() 时,实际上是在获取当前程序的堆栈信息,并将其存储在一个字符串类型的变量 stack 中。
这个字符串包含了程序在调用 debug.Stack() 时的调用栈信息,包括函数名、文件名和行号等。

package main

import (
    "fmt"
    "runtime/debug"
)

func main() {
    stack := debug.Stack()
    fmt.Println(string(stack))
}

func functionA() {
    functionB()
}

func functionB() {
    stack := debug.Stack()
    fmt.Println(string(stack))
}

示例中定义两个函数 functionA 和 functionB。
在 functionB 中,调用了 debug.Stack() 并打印了堆栈信息。
当运行这个程序时,你会看到类似以下的输出:

goroutine 1 [running]:
main.functionB()
    /path/to/your/project/main.go:14 +0x8e
main.functionA()
    /path/to/your/project/main.go:7 +0x56
main.main()
    /path/to/your/project/main.go:22 +0x4a
runtime.main()
    /usr/local/go/src/runtime/proc.go:225 +0x235
runtime.goexit()
    /usr/local/go/src/runtime/asm_amd64.s:1571 +0x1

这个输出显示了程序在调用 debug.Stack() 时的堆栈跟踪信息。
这对于调试程序和查找错误非常有用。

runtime.Goexit()

属于 runtime 包,用于退出当前的 goroutine。
当调用 runtime.Goexit() 时,当前正在执行的 goroutine 会立即终止,但不会对其他 goroutine 产生影响。

runtime.Goexit() 的使用场景通常包括:
(1) 优雅地退出 goroutine:
在某些情况下,可能需要在满足特定条件时退出 goroutine,而不是等待它自然完成。
使用 runtime.Goexit() 可以实现这一点。
(2) 避免 panic 引起的异常退出:
如果 goroutine 中发生了 panic,它会向上传播并可能影响到其他 goroutine。
在这种情况下,使用 runtime.Goexit() 可以优雅地退出当前 goroutine
避免 panic 对其他 goroutine 的影响
(3) 控制 goroutine 的生命周期:
在某些复杂的并发场景中,可能需要手动控制 goroutine 的生命周期。
通过在适当的时候调用 runtime.Goexit(),可以实现这一点。

package main

import (
    "fmt"
    "runtime"
    "time"
)

var someCondition = true

func main() {
    go func() {
        for {
            fmt.Println("Running...")
            time.Sleep(1 * time.Second)
            if someCondition {
                fmt.Println("Exiting...")
                runtime.Goexit()
            }
        }
    }()

    time.Sleep(5 * time.Second)
    fmt.Println("Main function finished.")
}

在这个示例中,启动了一个 goroutine,它会每隔一秒钟打印 “Running…”。
当 someCondition 为 true 时,goroutine 会打印 “Exiting…” 并调用 runtime.Goexit() 退出。
主函数在等待 5 秒钟后结束。

过度使用 runtime.Goexit() 可能会导致代码难以理解和维护。
在大多数情况下,使用 channel 和其他同步机制来控制 goroutine 的生命周期是更好的选择。

DoChan的学习

// DoChan is like Do but returns a channel that will receive the
// results when they are ready.
//
// The returned channel will not be closed.
func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result {
	ch := make(chan Result, 1)
	g.mu.Lock()
	if g.m == nil {
		g.m = make(map[string]*call)
	}
	if c, ok := g.m[key]; ok {
		c.dups++
		c.chans = append(c.chans, ch)
		g.mu.Unlock()
		return ch
	}
	c := &call{chans: []chan<- Result{ch}}
	c.wg.Add(1)
	g.m[key] = c
	g.mu.Unlock()

	go g.doCall(c, key, fn)

	return ch
}
  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值