首頁 > 後端開發 > Golang > Go 大師的並發:上下文傳播和取消的秘密揭曉

Go 大師的並發:上下文傳播和取消的秘密揭曉

Susan Sarandon
發布: 2024-12-07 20:16:13
原創
890 人瀏覽過

Master Go

Go 的並發模型改變了遊戲規則,但管理複雜的並發操作可能很棘手。這就是上下文傳播和取消的用武之地。這些強大的工具讓我們能夠建立健壯的、可取消的操作,跨越多個 goroutine 甚至網路邊界。

讓我們從基礎開始。 context 套件提供了一種跨 API 邊界和進程之間攜帶截止日期、取消訊號和請求範圍值的方法。這是控制長時間運行的操作和優雅地關閉服務的秘密武器。

這是一個使用上下文進行取消的簡單範例:

func longRunningOperation(ctx context.Context) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
            // Do some work
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    if err := longRunningOperation(ctx); err != nil {
        log.Printf("Operation cancelled: %v", err)
    }
}
登入後複製
登入後複製

在此範例中,我們建立一個超時時間為 5 秒的上下文。如果操作未在該時間內完成,則會自動取消。

但是上下文不只適用於逾時。我們可以使用它在多個 goroutine 之間傳播取消訊號。這對於管理複雜的工作流程非常有用。

考慮一個我們正在建構分散式事務系統的場景。我們可能在單一事務中涉及多個微服務,我們需要確保如果任何部分失敗,整個事務都會回滾。

以下是我們如何使用上下文來建構它:

func performTransaction(ctx context.Context) error {
    // Start the transaction
    tx, err := db.BeginTx(ctx, nil)
    if err != nil {
        return err
    }
    defer tx.Rollback() // Will be no-op if tx.Commit() is called

    // Perform multiple operations
    if err := operation1(ctx); err != nil {
        return err
    }
    if err := operation2(ctx); err != nil {
        return err
    }
    if err := operation3(ctx); err != nil {
        return err
    }

    // If we've made it this far, commit the transaction
    return tx.Commit()
}

func operation1(ctx context.Context) error {
    // Make an HTTP request to another service
    req, err := http.NewRequestWithContext(ctx, "GET", "http://service1.example.com", nil)
    if err != nil {
        return err
    }
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    // Process the response...
    return nil
}
登入後複製
登入後複製

在此範例中,我們使用上下文在資料庫操作和 HTTP 請求之間傳播取消。如果上下文在任何時候被取消(由於逾時或明確取消),則所有操作都將終止,並且資源將被清理。

但是如果我們需要對取消進行更細粒度的控制怎麼辦?這就是自訂上下文類型的用武之地。我們可以建立自己的上下文類型來攜帶特定於網域的取消訊號。

這是帶有「優先權」值的自訂上下文的範例:

type priorityKey struct{}

func WithPriority(ctx context.Context, priority int) context.Context {
    return context.WithValue(ctx, priorityKey{}, priority)
}

func GetPriority(ctx context.Context) (int, bool) {
    priority, ok := ctx.Value(priorityKey{}).(int)
    return priority, ok
}

func priorityAwareOperation(ctx context.Context) error {
    priority, ok := GetPriority(ctx)
    if !ok {
        priority = 0 // Default priority
    }

    // Use the priority to make decisions...
    switch priority {
    case 1:
        // High priority operation
    case 2:
        // Medium priority operation
    default:
        // Low priority operation
    }

    return nil
}
登入後複製
登入後複製

此自訂上下文可讓我們傳播優先順序資訊以及取消訊號,使我們能夠更好地控制並發操作。

現在,我們來談談優雅關閉。當我們建立長期運行的服務時,正確處理關閉訊號至關重要,以確保我們不會留下任何掛起的操作或未清理的資源。

以下是我們如何使用上下文來實現正常關閉:

func main() {
    // Create a context that's cancelled when we receive an interrupt signal
    ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
    defer cancel()

    // Start our main service loop
    errChan := make(chan error, 1)
    go func() {
        errChan <- runService(ctx)
    }()

    // Wait for either the service to exit or a cancellation signal
    select {
    case err := <-errChan:
        if err != nil {
            log.Printf("Service exited with error: %v", err)
        }
    case <-ctx.Done():
        log.Println("Received shutdown signal. Gracefully shutting down...")
        // Perform any necessary cleanup
        // Wait for ongoing operations to complete (with a timeout)
        cleanupCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
        defer cancel()
        if err := performCleanup(cleanupCtx); err != nil {
            log.Printf("Cleanup error: %v", err)
        }
    }
}

func runService(ctx context.Context) error {
    // Run your service here, respecting the context for cancellation
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
            // Do some work
        }
    }
}

func performCleanup(ctx context.Context) error {
    // Perform any necessary cleanup operations
    // This could include closing database connections, flushing buffers, etc.
    return nil
}
登入後複製

此設定可確保我們的服務在收到中斷訊號時可以正常關閉,從而有時間清理資源並完成任何正在進行的操作。

Go 上下文系統最強大的方面之一是它能夠跨網路邊界傳播取消。這在建立操作可能跨越多個服務的分散式系統時特別有用。

讓我們來看一個如何在微服務架構中實現這一點的範例:

func longRunningOperation(ctx context.Context) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
            // Do some work
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    if err := longRunningOperation(ctx); err != nil {
        log.Printf("Operation cancelled: %v", err)
    }
}
登入後複製
登入後複製

在此範例中,我們將根據查詢參數建立一個帶有逾時的上下文。然後,該上下文將透過所有後續 API 呼叫進行傳播。如果達到逾時,所有正在進行的操作都會被取消,並向客戶端傳回一個錯誤。

這種模式確保我們不會有任何在客戶端放棄等待回應後仍持續很長時間的「失控」操作。它是建立響應迅速、資源高效的分散式系統的關鍵部分。

並發系統中的錯誤處理可能很棘手,但上下文也可以提供幫助。透過使用上下文,我們可以確保錯誤正確傳播,並且即使發生錯誤也能清理資源。

以下是我們如何處理並發操作中的錯誤的範例:

func performTransaction(ctx context.Context) error {
    // Start the transaction
    tx, err := db.BeginTx(ctx, nil)
    if err != nil {
        return err
    }
    defer tx.Rollback() // Will be no-op if tx.Commit() is called

    // Perform multiple operations
    if err := operation1(ctx); err != nil {
        return err
    }
    if err := operation2(ctx); err != nil {
        return err
    }
    if err := operation3(ctx); err != nil {
        return err
    }

    // If we've made it this far, commit the transaction
    return tx.Commit()
}

func operation1(ctx context.Context) error {
    // Make an HTTP request to another service
    req, err := http.NewRequestWithContext(ctx, "GET", "http://service1.example.com", nil)
    if err != nil {
        return err
    }
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    // Process the response...
    return nil
}
登入後複製
登入後複製

在此範例中,我們使用通道將錯誤從 goroutine 傳回 main 函數。我們還在檢查取消的上下文。這確保我們能夠處理來自操作本身的錯誤和來自上下文的取消。

上下文的一個經常被忽視的方面是它攜帶請求範圍值的能力。這對於跨 API 邊界傳播請求 ID、身份驗證令牌或其他元資料等內容非常有用。

這是我們如何使用它的範例:

type priorityKey struct{}

func WithPriority(ctx context.Context, priority int) context.Context {
    return context.WithValue(ctx, priorityKey{}, priority)
}

func GetPriority(ctx context.Context) (int, bool) {
    priority, ok := ctx.Value(priorityKey{}).(int)
    return priority, ok
}

func priorityAwareOperation(ctx context.Context) error {
    priority, ok := GetPriority(ctx)
    if !ok {
        priority = 0 // Default priority
    }

    // Use the priority to make decisions...
    switch priority {
    case 1:
        // High priority operation
    case 2:
        // Medium priority operation
    default:
        // Low priority operation
    }

    return nil
}
登入後複製
登入後複製

在此範例中,我們使用中間件將請求 ID 新增至上下文。然後可以檢索此請求 ID 並在接收此上下文的任何後續處理程序或函數中使用。

在我們結束時,值得注意的是,雖然上下文是一個強大的工具,但它並不是靈丹妙藥。過度使用上下文可能會導致程式碼難以理解和維護。明智地使用上下文並仔細設計 API 非常重要。

請記住,上下文的主要用途應該是跨 API 邊界攜帶截止日期、取消訊號和請求範圍的值。它並不意味著成為將可選參數傳遞給函數的通用機制。

總之,掌握 Go 的並發模型(包括上下文傳播和取消)是建立健壯、高效且可擴展的應用程式的關鍵。透過利用這些工具,我們可以創建能夠優雅地處理複雜工作流程、有效管理資源並智慧地回應不斷變化的條件的系統。隨著我們不斷突破並發程式設計的極限,這些技術在我們的工具箱中將變得更加重要。


我們的創作

一定要看看我們的創作:

投資者中心 | 智能生活 | 時代與迴響 | 令人費解的謎團 | 印度教 | 精英開發 | JS學校


我們在媒體上

科技無尾熊洞察 | 時代與迴響世界 | 投資人中央媒體 | 令人費解的謎團 | | 令人費解的謎團 | >科學與時代媒介 |

現代印度教

以上是Go 大師的並發:上下文傳播和取消的秘密揭曉的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:dev.to
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板