12-常用標准庫之-context


一 Context是什么

1.1 介紹

Context,翻譯為"上下文",context包定義了Context接口類型,其接口方法定義了跨API和進程之間的執行最后期限、取消信號和其他請求范圍的值

在並發程序中,由於超時、取消操作或者一些異常情況,往往需要進行搶占操作或者中斷后續操作
context常用的使用場景:
  1. 一個請求對應多個goroutine之間的數據交互
  2. 超時控制
  3. 上下文控制

1.2 Context接口方法

context.Context是一個接口,該接口定義了四個需要實現的方法。具體如下:

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}

其中:

  • Deadline:是獲取設置的截止時間的意思,第一個返回值是截止時間,到了這個時間點,Context 會自動發起取消請求;第二個返回值 ok==false 時表示沒有設置截止時間,如果需要取消的話,需要調用取消函數進行取消。
  • Done:該方法返回一個只讀的 chan,類型為 struct{},我們在 goroutine 中,如果該方法返回的 chan 可以讀取,則意味着parent context已經發起了取消請求,我們通過 Done 方法收到這個信號后,就應該做清理操作,然后退出 goroutine,釋放資源。
  • Err 方法返回取消的錯誤原因,因為什么 Context 被取消。
  • Value方法獲取該 Context 上綁定的值,是一個鍵值對,所以要通過一個 Key 才可以獲取對應的值,這個值一般是線程安全的

1.3 兩個頂級Context

context包提供兩種頂級的上下文類型,這兩個內置的上下文對象作為最頂層的partent context,衍生出更多的子上下文對象:

func Background() Context

context.Background()返回非零的空上下文。它從不被取消,沒有值,也沒有最后期限。它通常由主函數、初始化和測試使用,並且作為傳入請求的頂級上下文。

func TODO() Context

context.TODO()返回非零的空上下文。當不清楚要使用哪個上下文或者它還不可用時(,應該使用context.TODO()。

兩者區別

本質來講兩者區別不大,其源碼實現是一樣的,只不過使用場景不同,context.Background()通常由主函數、初始化和測試使用,是頂級Context;context.TODO()通常用於主協程外的其他協程向下傳遞,分析工具可識別它在調用棧中傳播

1.4 派生Context(With系列函數)

除以上兩種頂級Context類型,context包提供四種創建可派生Context類型的函數

WithCancel

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

WithCancel函數返回具有新done通道的父級副本。當調用返回的cancel函數或關閉父上下文的done通道時(以先發生者為准),將關閉返回的上下文的done通道。
取消此上下文將釋放與其關聯的資源,因此代碼應在此上下文中運行的操作完成后立即調用Cancel。

示例通過context控制多個協程停止:

package main

import (
	"context"
	"fmt"
	"time"
)

func task(ctx context.Context, s string) {
lqz:
	for {
		select {
		case <-ctx.Done():
			fmt.Println("task:我收到取消指令,我結束了")
			break lqz  // 結束掉 label位置的循環
		default:
			fmt.Println("打印一次傳入的值:", s)
			time.Sleep(1 * time.Second)

		}
	}
}
func main() {
	parent := context.Background()
	ctx, cancle := context.WithCancel(parent)
	go task(ctx, "lqz is Nb")
	go task(ctx, "lqz is handsome")
	time.Sleep(5 * time.Second) // 睡個5s鍾,發現上面兩句話不停打印
	cancle()  // 通過ctx控制,上面兩個go協程關閉
	time.Sleep(5 * time.Second)  // 睡個5s鍾,發現確實被停止了,不打印了

}

WithDeadline

 func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
// 注意第二個參數是時間對象

WithDeadline函數返回父上下文的副本,其截止時間調整為不遲於d。如果父上下文的截止時間早於d,則WithDeadline(Parent,d)在語義上等同於父上下文。當截止時間到期、調用返回的cancel函數或關閉父上下文的done通道(以先發生者為准)時,返回的上下文的done通道將關閉。
取消此上下文將釋放與其關聯的資源,因此代碼應在此上下文中運行的操作完成后立即調用Cancel。

官方使用示例:
這個例子傳遞一個具有任意截止時間的上下文,告訴一個阻塞函數一旦到達它就應該放棄它的工作。

package main

import (
	"context"
	"fmt"
	"time"
)

func task(ctx context.Context, s string) {
lqz:
	for {
		select {
		case <-ctx.Done():
			fmt.Println("task:我收到取消指令,我結束了")
			fmt.Println(ctx.Err())
			// 正常到時間:context deadline exceeded
			// 手動調用cancel :context canceled

			break lqz // 結束掉 label位置的循環

		default:
			fmt.Println("打印一次傳入的值:", s)
			time.Sleep(1 * time.Second)

		}
	}
}
func task2(ctx context.Context, s string) {
lqz:
	for {
		select {
		case <-ctx.Done():
			fmt.Println("task:我收到取消指令,我結束了")
			fmt.Println(ctx.Err())
			// 正常到時間:context deadline exceeded
			// 手動調用cancel :context canceled

			break lqz // 結束掉 label位置的循環
		case <-time.After(1 * time.Second):
			fmt.Println("1s時間到了,打印:",s)
			fmt.Println(ctx.Err()) // 執行到此,如果還沒到結束時間,Err為nil
		}
	}
}
func main() {
	// 1 正常到時間
	//parent := context.Background()
	//t:=time.Now().Add(5*time.Second) // 5s后的時間
	//ctx, _ := context.WithDeadline(parent,t)
	//go task(ctx, "lqz is Nb")
	//time.Sleep(10 * time.Second) // 睡個10s鍾,由於5s結束,后5s沒有輸出

	// 2 手動調用cancle取消
	//parent := context.Background()
	//t := time.Now().Add(5 * time.Second) // 5s后的時間
	//ctx, cancel := context.WithDeadline(parent, t)
	//go task(ctx, "lqz is Nb")
	//time.Sleep(3 * time.Second) // 睡個3s鍾,由於5s還沒到,手動結束
	//cancel()
	//time.Sleep(7 * time.Second) // 再睡7s看輸出

	//3 1s后輸出一次內容的另一種寫法
	parent := context.Background()
	t := time.Now().Add(5 * time.Second) // 5s后的時間
	ctx, cancel := context.WithDeadline(parent, t)
	go task2(ctx, "lqz is Nb")
	time.Sleep(3 * time.Second) // 睡個3s鍾,由於5s還沒到,手動結束
	cancel()
	time.Sleep(7 * time.Second) // 再睡7s看輸出


}


WithTimeout

 func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
// 注意第二個參數是time.Duration 時間間隔

WithTimeout 返回 WithDeadline(parent, time.Now().Add(timeout))。取消此上下文將釋放與其關聯的資源,因此代碼應在此上下文中運行的操作完成后立即調用取消:

這個例子傳遞一個帶有超時的上下文,告訴一個阻塞函數它應該在超時結束后放棄它的工作。

package main

import (
	"context"
	"fmt"
	"time"
)

func task(ctx context.Context) {
	select {
	case <-ctx.Done():
		fmt.Println("task:我結束了")
		// cancle函數取消會打印context canceled
		// 到時間取消會打印:context deadline exceeded
		fmt.Println(ctx.Err())
	case <-time.After(1 * time.Second):
		fmt.Println("1s時間到了")
		fmt.Println(ctx.Err()) // 執行到此,如果還沒到結束時間,Err為nil

	}

}

func main() {
	//ctx, cancle := context.WithTimeout(context.Background(), 1*time.Second) // 打印
	ctx, cancle := context.WithTimeout(context.Background(), 2*time.Second)
	go task(ctx)

	time.Sleep(3*time.Second)
	cancle()
	time.Sleep(3*time.Second)


}

WithValue

 func WithValue(parent Context, key, val interface{}) Context

WithValue返回父級的副本,可為上下文設置一個鍵值對。
只對傳輸進程和API的請求范圍數據使用上下文值,而不用於向函數傳遞可選參數。
提供的鍵必須是可比較的,並且不應是字符串或任何其他內置類型,以避免使用上下文的包之間發生沖突。WithValue的用戶應該為鍵定義自己的類型。為了避免在分配給接口時進行分配,上下文鍵通常具有具體的類型結構。或者,導出的上下文鍵變量的靜態類型應該是指針或接口。

示例:

package main

import (
	"context"
	"fmt"
	"time"
)

func task(ctx context.Context) {
	fmt.Println(ctx.Value("name"))
	select {
	case <-ctx.Done():
		fmt.Println("task:我結束了")
		fmt.Println(ctx.Err())
	case <-time.After(1 * time.Second):
		fmt.Println("1s時間到了")
		fmt.Println(ctx.Err())

	}

}

func main() {
	ctx, _ := context.WithTimeout(context.Background(), 1*time.Second) // 1s超時的ctx
	ctx = context.WithValue(ctx, "name", "lqz")
	go task(ctx)
	time.Sleep(5*time.Second)
}

二 Context使用示例

2.1 控制10s后,所有協程退出

使用context包來實現線程安全退出或超時的控制:控制10s后,所有協程退出

package main

import (
	"context"
	"fmt"
	"strconv"
	"sync"
	"time"
)

func task(ctx context.Context, s string, wg *sync.WaitGroup) {
	defer wg.Done()
	for {
		select {
		case <-ctx.Done():
			fmt.Println(s, "--->我結束了")
			//fmt.Println(ctx.Err())
			return
		default:
			fmt.Println(s)
			time.Sleep(1 * time.Second)

		}
	}

}

func main() {
	var wg sync.WaitGroup
	ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
	for i := 0; i < 10; i++ {
		wg.Add(1)
		s := fmt.Sprintf("我是第:%v 個任務", strconv.Itoa(i))
		go task(ctx, s, &wg)
	}
	wg.Wait()

}

當並發體超時或main主動停止工作者Goroutine時,每個工作者都可以安全退出。

2.2 控制某個go協程執行5次就結束

// 控制goroutine 執行5次結束
func main() {
	// 定義一個運行次數變量
	runCount := 0
	//定義一個waitgroup,等待goroutine執行完成
	var wg sync.WaitGroup
	// 初始化context
	parent := context.Background()
	// 傳入初始化的ctx,返回ctx和cancle函數
	ctx, cancle := context.WithCancel(parent)
	wg.Add(1) // 增加一個任務
	go func() {
		for {
			select {
			case <-ctx.Done():
				fmt.Println("任務結束")
				return
			default:
				fmt.Printf("任務執行了%d次\n", runCount)
				runCount++
			}
			// 執行了5次,使用ctx的取消函數將任務取消
			if runCount >= 5 {
				cancle()
				wg.Done() // goroutine執行完成
			}

		}
	}()

	wg.Wait() //等待所有任務完成

}

2.3 打印100個素數

Go語言是帶內存自動回收特性的,因此內存一般不會泄漏。當main函數不再使用管道時后台Goroutine有泄漏的風險。我們可以通過context包來避免這個問題,下面是防止內存泄露的素數篩實現:

// 返回生成自然數序列的管道: 2, 3, 4, ...
func GenerateNatural(ctx context.Context) chan int {
    ch := make(chan int)
    go func() {
        for i := 2; ; i++ {
            select {
            //父協程cancel()時安全退出該子協程
            case <- ctx.Done():
                return
            //生成的素數發送到管道
            case ch <- i:
            }
        }
    }()
    return ch
}

// 管道過濾器: 刪除能被素數整除的數
func PrimeFilter(ctx context.Context, in <-chan int, prime int) chan int {
    out := make(chan int)
    go func() {
        for {
            if i := <-in; i%prime != 0 {
                select {
                //父協程cancel()時安全退出該子協程
                case <- ctx.Done():
                    return
                case out <- i:
                }
            }
        }
    }()
    return out
}

func main() {
    // 使用一個可由父協程控制子協程安全退出的Context。
    ctx, cancel := context.WithCancel(context.Background())

    ch := GenerateNatural(ctx) // 自然數序列: 2, 3, 4, ...
    
    for i := 0; i < 100; i++ {
        // 新出現的素數打印出來
        prime := <-ch 
        fmt.Printf("%v: %v\n", i+1, prime)
        // 基於新素數構造的過濾器
        ch = PrimeFilter(ctx, ch, prime) 
    }
    
    //輸出100以內符合要求的素數后安全退出所有子協程
    cancel()
}

當main函數完成工作前,通過調用cancel()來通知后台Goroutine退出,這樣就避免了Goroutine的泄漏。

三 使用Context的注意事項

  • 推薦以參數的方式顯示傳遞Context
  • 以Context作為參數的函數方法,應該把Context作為第一個參數。
  • 給一個函數方法傳遞Context的時候,不要傳遞nil,如果不知道傳遞什么,就使用context.TODO()
  • Context的Value相關方法應該傳遞請求域的必要數據,不應該用於傳遞可選參數
  • Context是線程安全的,可以放心的在多個goroutine中傳遞


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM