Go并发模式之context(比done channel 更加强大)-取消goroutine的好的解决方案

时间:2019-04-19
本文章向大家介绍Go并发模式之context(比done channel 更加强大)-取消goroutine的好的解决方案,主要包括Go并发模式之context(比done channel 更加强大)-取消goroutine的好的解决方案使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
在并发程序中, 由于超时、取消、或系统的其他部分的故障 往往需要抢占操作。
done channel 可以在你的程序中流动并取消所有阻塞的并发操作。 看起来不错,但是还不完美,还不是很够用。
什么样的功能还需要呢?什么样功能才算完美呢?
如果我们可以在简单的通知上附加传递额外的信息; 如 为什么取消发生, 函数是否有需要完成的最后期限(超时), 这些情况下这些功能 非常有用。
context 包有如下方法:
todo


context(type 类型): 有:
一个Deadline 函数,用于指示在一定时间之后goroutine 是否被取消,
一个 Err方法, 如果goroutine 被取消,将返回非零。
一个Value 方法,
函数中取消有三种情况:

1。 goroutine 的 父 goroutine 可能想要取消它。
2。 一个goroutine 可能想要取消它的 子goroutine
3。 goroutine 中呢任何阻塞操作都必须是可抢占的, 以便它可以被取消。
下面看看 之前 使用 do channel 方式 有什么优缺点

func printGreeting(done <-chan interface{}) error{
	greeting, err := genGreeting(done)
	if err != nil{
		return err
	}
	fmt.Printf("%s world!\n", greeting)
	return nil
}

func genGreeting(done <-chan interface{})(string, error){
	switch locale, err := locale(done);{
	case err != nil:
		return "", err
	case locale == "EN/US":
		return "hello", nil
	}
	return "", fmt.Errorf("unsupported locale")
}

func locale(done <-chan interface{})(string, error){
	select{
	case <-done :
		return "", fmt.Errorf("canceled")
	case <-time.After(1*time.Minute):
	}
	return "EN/US", nil
}

func printFarewell(done <-chan interface{})  error{
	farewell, err := genFarewell(done)
	if err != nil{
		return err
	}
	fmt.Printf("%s world!\n", farewell)
	return nil
}

func genFarewell(done <-chan interface{})(string, error){
	switch locale,err := locale(done);{
	case err != nil:
		return "", err
	case locale == "EN/US":
		return "goodbye", nil
	}
	return "", fmt.Errorf("unsupproted locale")
}


func main() {

	var wg sync.WaitGroup
	done := make(chan interface{})
	defer close(done)

	wg.Add(1)
	go func() {
		defer wg.Done()
		if err := printGreeting(done); err != nil{
			fmt.Println("%v", err)
			return
		}
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()
		if err := printFarewell(done); err != nil{
			fmt.Printf("%v", err)
		}
	}()

	wg.Wait()
}
假设有这样需求:
假设genGreeting 只想在放弃调用locale之前等待 1s(超过1s 就不想调用了),超时时间 1s;
如果 printGreeting 不成功,我们也想取消 对 printFarewall的调用。 毕竟不打招呼,说再见就没有意义了。使用do channel 难以做到这点,下面 看看context 能不能满足这样的需求? 
func main() {
	var wg sync.WaitGroup
	//Background() 方法 返回一个空的上下文
	// withCancel 包装它 为了 获取 cancel/取消 方法/功能
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	wg.Add(1)
	go func() {
		defer wg.Done();
		if err := printGreeting(ctx); err != nil{
			fmt.Printf("cannot print greeting: %v\n", err)

			//在这一行,如果打印问候出错,将 取消上下文;  或说对上下文 环境 执行 cancel, 这样使用这个上下文或者从这个上下文衍生出来的上下文。
			//中  ct.Done()  将收到信号/收到值; 进而达到解除阻塞 的目的。
			cancel()
		}
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()
		if err := printFarewell(ctx); err != nil{
			fmt.Printf("cannot print farewell: %v\n", err)
		}
	}()
	wg.Wait()

}


func printGreeting(ctx context.Context) error{
	greeting, err := genGreeting(ctx)
	if err != nil{
		return err
	}
	fmt.Printf("%s world!\n", greeting)
	return nil
}

func genGreeting(ctx context.Context)(string, error){
	ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
	defer cancel()
	switch locale, err := locale(ctx);{
	case err != nil :
		return "", err
	case locale == "EN/US":
		return "hello", nil
	}
	return "", fmt.Errorf("unsupported locale")
}


func locale(ctx context.Context)(string, error){
	select{
	case <-ctx.Done():
		return "", ctx.Err()
	case <-time.After(1* time.Minute) :
	}
	return "EN/US", nil
}


func printFarewell(ctx context.Context) error{
	farewell, err := genFarewell(ctx)
	if err != nil{
		return err
	}
	fmt.Printf("%s world!\n", farewell)
	return nil
}

func genFarewell(ctx context.Context)(string, error){
	switch locale, err := locale(ctx);{
	case err != nil:
		return "", err
	case locale == "EN/US":
		return "goodbye", nil
	}
	return "", fmt.Errorf("unsupported locale")
}
//cannot print greeting: context deadline exceeded
//cannot print farewell: context canceled
总结: cancel()  或者 时间到了(context.WithTimeout(ctx, 1*time.Second)) 都会 使 ctx.Done() 收到值, 解除阻塞
那么 Deadline 又是在什么情况下。或有那些需求时会用到?
func locale(ctx context.Context)(string, error){
	if deadline, ok := ctx.Deadline(); ok{
		if deadline.Sub(time.Now().Add(1*time.Minute)) <= 0 {
			fmt.Println("end locale")
			return "", context.DeadlineExceeded
		}
	}

	select{
	case <- ctx.Done():
		return "", ctx.Err()
	case <-time.After(1*time.Minute):
	}
	return "EN/US", nil
}
//end locale
//cannot print greeting: context deadline exceeded
//cannot print farewell: context canceled

/**
在这里我们检查我们的上下文是否提供了截止。 如果提供了,并且 能够确定 当前时间 再执行下去一定会超时, 那么程序就立刻返回,不往下走业务逻辑;
可以返回上下文包中的特定错误, 即 DeadlineExceeded

在调用下一个 成本很高的函数的程序中, 这可能会节省大量的时间,它允许 该函数立即失败,而不必等待实际的超时发生。 前提/适用的情况是 必须知道
下级调用图 需要多长时间,这个可能实践起来非常困难。
图:todo
实际上 超时时间设置 肯定大于 正常情况下 程序的执行时间;如 假如 locale 一般 要执行 1min ;那么超时时间 就是 1min + ;(本例子中 是为了看到
效果 才将 超时时间设置为1s)
决定 deadline 是否满足 <=0 的关键 在于 now位置(它是变化的);实际上 意思是: 如果前面执行的时间太长了(出现意外情况);那么后面也就没必要
再执行了,因为执行了肯定会超时的!
如何在上下文中存储数据以及如何检索数据:
func main(){
	ProcessRequest("jane", "abc123")
}

func ProcessRequest(userID, authToken string){
	ctx := context.WithValue(context.Background(), "userID", userID)
	ctx = context.WithValue(ctx, "authToken", authToken)
	HandleResponse(ctx)
}


func HandleResponse(ctx context.Context){
	fmt.Printf("handling response for %v (%v)",
		ctx.Value("userID"), ctx.Value("authToken"))
}
存储 使用 context.WithValue(....);  检索使用 ctx.Value()
正确使用这个功能 ,需要注意的是:/要满足的条件是:
1。 使用的键 必须满足 可比较特性/特点; 也就是 使用运算符== 和 != 在使用时要返回正确的结果
2。 返回值必须安全, 才能从多个goroutine 访问。

具体怎么做呢?