首页 > 后端开发 > Golang > Go 大师的并发:上下文传播和取消的秘密揭晓

Go 大师的并发:上下文传播和取消的秘密揭晓

Susan Sarandon
发布: 2024-12-07 20:16:13
原创
904 人浏览过

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
作者最新文章
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板