Go 利用上下文進行併發計算
在 Go 編程中,上下文(context
)是一個非常重要的概念,它包含了與請求相關的信息,如截止日期和取消信息,以及在請求處理管道中傳遞的其他數據。在併發編程中,特別是在處理請求時,正確處理上下文可以確保我們尊重和執行請求中設定的限制,如截止時間。
讓我們通過一些代碼示例來探討如何在併發計算中使用上下文,以及如何在處理請求時尊重上下文所設定的截止日期和取消要求。
// download 函數用於下載給定 URL 的內容。
func download(ctx context.Context, url string) (string, error) {...}
download
函數嘗試獲取給定 URL 的內容。然而,需要注意的是,每個 URL 的下載內容可能不同,因此下載所需的時間也可能不同。如果在截止日期之前未能完成 URL 的下載,該函數將返回一個錯誤(截止日期錯誤)。
現在,假設我們需要下載許多 URL,並且我們只有有限的時間來完成這些下載。我們可以使用 errgroup
來併發地進行下載,如果超過截止時間,我們將取消所有併發操作。
// downloadAll 函數併發地下載給定 URL 的內容。
func downloadAll(ctx context.Context, urls []string) ([]string, error) {
results := make([]string, len(urls))
g, ctx := errgroup.WithContext(ctx)
for i := range len(urls) {
g.Go(func() error {
content, err := download(ctx, urls[i])
if err != nil {
return err
}
results[i] = content
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
在這個示例中,downloadAll
函數同時下載每個給定的 URL,並將相同的上下文傳遞給 download
函數。如果下載任何一個 URL 所需的時間超過了設定的截止時間,download
函數將失敗,從而導致整個併發流程也失敗,downloadAll
將返回一個截止日期錯誤。
除了下載這些 URL,我們還需要處理下載的內容。例如,我們可能要對每個 URL 的內容應用某個過濾器(謂詞)。
// filter 函數檢查給定內容是否符合給定的謂詞。
func filter(content string, pred func(string) bool) bool {
return pred(content)
}
請注意,過濾器既不需要上下文,也不進行任何跨邊界調用。過濾器函數不關心上游處理的截止日期。
使用 filter
函數,我們可以定義一個過濾所有內容的函數。
// filterAll 函數同時過濾所有給定的內容。
func filterAll(contents []string, pred func(string) bool) []string {
type Result struct {
content string
ok bool
}
results := make([]Result, len(contents))
g := errgroup.Group{}
for i, content := range contents {
g.Go(func() error {
ok := filter(contents[i], pred)
results[i] = Result{content: content, ok: ok}
return nil
})
}
g.Wait()
var filtered []string
for _, r := range results {
if r.ok {
filtered = append(filtered, r.content)
}
}
return filtered
}
filterAll
函數調用 filter
函數來應用謂詞到每個內容上,但謂詞的應用可能會花費一些時間,可能超過上下文設置的截止時間。由於 filter
函數不使用上下文,因此它不會因爲截止日期錯誤而失敗。
我們需要重新定義 filterAll
,使其使用上下文並檢查其中的錯誤,而不管 filter
函數是否使用了上下文。
// filterAll 函數同時過濾所有內容,並檢查上下文中的錯誤。
func filterAll(ctx context.Context, contents []string, pred func(string) bool) ([]string, error) {
type Result struct {
content string
ok bool
}
results := make([]Result, len(contents))
g, ctx := errgroup.WithContext(ctx)
for i, content := range contents {
g.Go(func() error {
if err := ctx.Err(); err != nil {
return err
}
ok := filter(contents[i], pred)
results[i] = Result{content: content, ok: ok}
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
var filtered []string
for _, r := range results {
if r.ok {
filtered = append(filtered, r.content)
}
}
return filtered, nil
}
我們的新實現 filterAll
函數會檢查上下文中的任何錯誤,即使上下文並未直接傳遞給下游函數(在本例中爲 filter
)。如果發生了與上下文相關的截止日期(或任何其他錯誤),整個過濾過程就會失敗。
現在,讓我們完成對所有內容的處理。
// processURLs 函數下載每個 URL 的內容並對其進行過濾。
//
// 處理必須在上下文截止日期內完成。
func processURLs(ctx context.Context, urls []string) ([]string, error) {
contents, err := downloadAll(ctx, urls)
if err != nil {
return nil, err
}
filtered, err := filterAll(ctx, contents, somePredicate)
return filtered, err
}
如果任何一個下載操作花費的時間過長,那麼在嘗試獲取內容時就會發生截止日期錯誤,因爲上下文被直接用於 API 調用。因此,downloadAll
函數也會失敗,進而導致 processURLs
失敗。
如果所有的 URL 在截止日期內都被正確下載,我們將繼
續對它們進行過濾。在對每個下載內容進行過濾時,不使用上下文,但 filterAll
函數明確地檢查上下文中的錯誤,如果發生了與上下文相關的截止日期(或任何其他錯誤),整個過濾過程就會失敗。
有時候,僅僅使用 errgroup.WithContext
是不足以檢測到上下文中的截止日期或其他問題的,特別是當上下文未直接使用時。因此,我們應該定期檢查是否仍在時間限制內,否則就會失敗。
最後,我們可以通過編寫 filterAll
的測試來確保我們正確地處理了類似的情況,以確保我們尊重與上下文相關的任何錯誤。
func TestContextError(t *testing.T) {
ctx, done := context.WithTimeout(context.Background(), time.Nanosecond)
defer done()
// 生成我們想要應用過濾器的一些數據。
var contents []string = testingContent()
_, err := filterAll(ctx, contents, thePredicate)
if err == nil {
t.Errorf("filterAll() = %v, want error", err)
}
}
請注意,在測試中,我們期望 filterAll
會失敗,因爲我們設置的超時時間只有一納秒。因此,上下文應該因爲超過截止時間而發生錯誤。如果在啓動 Goroutine 進行下載內容過濾時不檢查 context.Err()
,我們將永遠不會處理此類錯誤。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/dlSmW0LQVrOnPx1VOSpaZg