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