go的singleflight学习

方法

Do

DoChan

Forget

使用示例

go 复制代码
sg "golang.org/x/sync/singleflight"

func TestDo(t *testing.T) {
	var g sg.Group
	v, err, _ := g.Do("key", func() (interface{}, error) {
		return "bar", nil
	})
	if got, want := fmt.Sprintf("%v (%T)", v, v), "bar (string)"; got != want {
		t.Errorf("Do = %v; want %v", got, want)
	}
	if err != nil {
		t.Errorf("Do error = %v", err)
	}
}

func TestDoErr(t *testing.T) {
	var g sg.Group
	someErr := errors.New("Some error")
	v, err, _ := g.Do("key", func() (interface{}, error) {
		return nil, someErr
	})
	if err != someErr {
		t.Errorf("Do error = %v; want someErr %v", err, someErr)
	}
	if v != nil {
		t.Errorf("unexpected non-nil value %#v", v)
	}
}

func TestDoDupSuppress(t *testing.T) {
	var g sg.Group
	var wg1, wg2 sync.WaitGroup
	c := make(chan string, 1)
	var calls int32
	// 原子读取calls,如果为1则将wg1 -1,同时从chan中读取内容,然后再写回,
	// sleep 10mills 后,返回 chan 中的内容
	fn := func() (interface{}, error) {
		if atomic.AddInt32(&calls, 1) == 1 {
			fmt.Println("[fn] first call")
			// First invocation.
			// 第一次调用
			wg1.Done()
		}
		fmt.Println("[fn] call")

		v := <-c
		c <- v // pump; make available for any future calls

		// 模拟耗时的下游请求,让更多的协程等待sg的Do结果
		time.Sleep(10 * time.Millisecond) // let more goroutines enter Do

		return v, nil
	}

	const n = 10
	wg1.Add(1)
	for i := 0; i < n; i++ {
		curI := i
		wg1.Add(1)
		wg2.Add(1)
		go func() {
			fmt.Println("[TestDoDupSuppress] curI", curI)
			defer wg2.Done() // 维持协程的结束
			wg1.Done()       // 维持协程的开始
			// 位置1(不是下一行的标识)
			v, err, _ := g.Do("key", fn) // 位置1的下一个位置
			if err != nil {
				t.Errorf("Do error: %v", err)
				return
			}
			if s, _ := v.(string); s != "bar" {
				t.Errorf("Do = %T %v; want %q", v, v, "bar")
			}
		}()
	}
	wg1.Wait() // 全部协程都就绪了,保证所有协程进入位置1
	// 此时只有一个协程进入了Do中,等待在chan上

	c <- "bar"

	wg2.Wait()
	// 此时所有的协程都处理结束

	// 此时的calls一定处于[1, n)
	got := atomic.LoadInt32(&calls)
	fmt.Println("got", got)
	if got <= 0 || got >= n {
		t.Errorf("number of calls = %d; want over 0 and less than %d", got, n)
	}
}

// Test that singleflight behaves correctly after Forget called.
// See https://github.com/golang/go/issues/31420
// 忘记一个key,forget about a key
// 下一次调用时,可以继续使用Do
// Future calls to Do for this key will call the function
// 而不是等待一个更早的请求去直接拿结果
// rather than waiting for an earlier call to complete
func TestForget(t *testing.T) {
	var g sg.Group

	var (
		firstStarted  = make(chan struct{}) // 没有缓冲的通道
		unblockFirst  = make(chan struct{})
		firstFinished = make(chan struct{})
	)

	go func() {
		g.Do("key", func() (i interface{}, e error) {
			// 关闭一个已关闭的channel会导致panic,所以在关闭之前,需要确认channel是否已被关闭
			// 或者处理可能出现的panic
			// 注意
			// 关闭channel并不是必须的
			// 当不会再向channel写入数据时,可以关闭channel
			// 当其他goroutine试图从该channel接收时,它们会立即接收到一个零值而不再阻塞
			close(firstStarted)
			<-unblockFirst
			close(firstFinished)
			return
		})
	}()
	<-firstStarted  // 第一次请求卡在这里等待结果
	g.Forget("key") // 第一次请求的Do还卡在<-unblockFirst上,此时执行Forget

	unblockSecond := make(chan struct{}) // 第二次请求,卡在 <-unblockSecond 中
	secondResult := g.DoChan("key", func() (i interface{}, e error) {
		<-unblockSecond
		return 2, nil
	})

	close(unblockFirst) // 通知第一次请求的Do中执行<-unblockFirst
	<-firstFinished     // 等待第一次请求中的Do执行完成

	// 第三次请求,返回3
	thirdResult := g.DoChan("key", func() (i interface{}, e error) {
		return 3, nil
	})

	close(unblockSecond) // 通知第二次请求的Do执行完成
	r2 := <-secondResult // 读取第二次的结果
	if r2.Val != 2 {
		t.Errorf("We should receive result produced by second call, expected: 2, got %d", r2.Val)
	}
	r3 := <-thirdResult // 读取第三次的结果
	if r3.Val != 2 {
		t.Errorf("We should receive result produced by second call, expected: 2, got %d", r3.Val)
	}
}

// TestDoChan 结果存储到 Chan 中
func TestDoChan(t *testing.T) {
	var g sg.Group
	ch := g.DoChan("key", func() (interface{}, error) {
		return "bar", nil
	})

	res := <-ch
	v := res.Val
	err := res.Err
	if got, want := fmt.Sprintf("%v (%T)", v, v), "bar (string)"; got != want {
		t.Errorf("Do = %v; want %v", got, want)
	}
	if err != nil {
		t.Errorf("Do error = %v", err)
	}
}

// Test singleflight behaves correctly after Do panic.
// See https://github.com/golang/go/issues/41133,这个issues有意思
// 如果panic,则所有协程都panic
func TestPanicDo(t *testing.T) {
	var g sg.Group
	fn := func() (interface{}, error) {
		panic("invalid memory address or nil pointer dereference")
	}

	const n = 5
	waited := int32(n)
	panicCount := int32(0) // 如果panic,则所有协程都panic
	done := make(chan struct{})
	for i := 0; i < n; i++ {
		go func() {
			defer func() {
				// defer中先panicCount、再waited
				// 主函数判断的时候是先判断 waited,再判断 panicCount
				if err := recover(); err != nil {
					t.Logf("Got panic: %v\n%s", err, debug.Stack())
					atomic.AddInt32(&panicCount, 1)
				}

				if atomic.AddInt32(&waited, -1) == 0 {
					close(done)
				}
			}()

			g.Do("key", fn)
		}()
	}

	select {
	case <-done: // 关闭通道
		if panicCount != n {
			t.Errorf("Expect %d panic, but got %d", n, panicCount)
		}
	case <-time.After(time.Second): // 1秒后还没有执行完成
		t.Fatalf("Do hangs")
	}
}

// runtime.Goexit() 用于终止调用它的 goroutine
// 该函数不返回任何值。当执行此函数时,goroutine 将立即停止执行
// 但是并不会影响其他的 goroutine
// 随后,程序将恢复执行其他的 goroutine
// 如果主 goroutine 执行了这个函数,所有的 goroutine 将停止
//
// 这个函数通常在需要提前结束一个 goroutine 或者在特定条件下需要关闭 goroutine 时使用
// 但通常情况下,通过 return 或者到达函数尾部来结束一个 goroutine
// 使用 runtime.Goexit() 非常罕见
//
func TestGoexitDo(t *testing.T) {
	var g sg.Group
	fn := func() (interface{}, error) {
		fmt.Println("[fn] before goexit")
		runtime.Goexit()
		return nil, nil
	}

	const n = 5
	waited := int32(n)
	done := make(chan struct{})
	for i := 0; i < n; i++ {
		go func() {
			var err error
			defer func() {
				if err != nil {
					t.Errorf("Error should be nil, but got: %v", err)
				}
				tmp := atomic.AddInt32(&waited, -1)
				fmt.Println("waited", tmp)
				if tmp == 0 {
					close(done)
				}
			}()
			// g中使用匿名函数执行fn,所以当fn中直接runtime.Goexit(),则该协程被kill
			// 将不执行下一行打印,而是直接执行defer
			val, err, shared := g.Do("key", fn)
			fmt.Printf("val %v, err %v, shared %v\n", jsonx.ToString(val), err, shared)
		}()
	}

	select {
	case <-done:
		fmt.Println("finish")
	case <-time.After(time.Second):
		t.Fatalf("Do hangs")
	}
}
相关推荐
Swift社区1 小时前
在 Swift 中实现字符串分割问题:以字典中的单词构造句子
开发语言·ios·swift
没头脑的ht1 小时前
Swift内存访问冲突
开发语言·ios·swift
没头脑的ht1 小时前
Swift闭包的本质
开发语言·ios·swift
wjs20241 小时前
Swift 数组
开发语言
stm 学习ing2 小时前
FPGA 第十讲 避免latch的产生
c语言·开发语言·单片机·嵌入式硬件·fpga开发·fpga
湫ccc3 小时前
《Python基础》之字符串格式化输出
开发语言·python
Red Red3 小时前
网安基础知识|IDS入侵检测系统|IPS入侵防御系统|堡垒机|VPN|EDR|CC防御|云安全-VDC/VPC|安全服务
网络·笔记·学习·安全·web安全
mqiqe4 小时前
Python MySQL通过Binlog 获取变更记录 恢复数据
开发语言·python·mysql
AttackingLin4 小时前
2024强网杯--babyheap house of apple2解法
linux·开发语言·python