MST
星途 面试题库

面试题:Go并发任务中WaitGroup与Context的资源泄漏场景分析

在使用WaitGroup和Context管理复杂并发任务时,可能会出现资源泄漏的情况。请举例说明可能导致资源泄漏的场景有哪些,例如goroutine未正确退出、资源未释放等,并阐述如何通过合理使用WaitGroup和Context以及相关的错误处理机制来避免这些资源泄漏问题。
30.1万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

可能导致资源泄漏的场景

  1. goroutine未正确退出
    • 场景:假设在一个函数中,使用WaitGroup启动多个goroutine执行任务。如果某个goroutine在执行过程中遇到错误,没有正确处理并退出,而是进入一个无限循环,那么这个goroutine将一直运行,造成资源泄漏。例如:
package main

import (
    "fmt"
    "sync"
)

func worker(wg *sync.WaitGroup) {
    defer wg.Done()
    // 模拟一个可能出错的任务
    if true {
        for {
            // 这里应该是处理错误并退出,但是进入了无限循环
        }
    }
    fmt.Println("Worker finished")
}

func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    go worker(&wg)
    wg.Wait()
    fmt.Println("All workers finished")
}
  • 分析:上述代码中,worker函数在遇到特定条件(这里简单假设为true)时,进入无限循环,没有调用wg.Done(),导致main函数中的wg.Wait()永远不会返回,同时这个goroutine也不会结束,浪费系统资源。
  1. 资源未释放
    • 场景:当goroutine中使用了外部资源,如文件描述符、数据库连接等,在goroutine异常结束时,如果没有正确释放这些资源,就会造成资源泄漏。例如:
package main

import (
    "database/sql"
    "fmt"
    "sync"

    _ "github.com/go - sql - driver/mysql"
)

func databaseWorker(wg *sync.WaitGroup, db *sql.DB) {
    defer wg.Done()
    // 从数据库连接池获取连接
    conn, err := db.Conn(nil)
    if err!= nil {
        // 这里没有关闭连接就返回,导致连接泄漏
        return
    }
    // 这里假设处理数据库操作
    // 没有正确关闭连接
}

func main() {
    db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/test")
    if err!= nil {
        panic(err)
    }
    defer db.Close()

    var wg sync.WaitGroup
    wg.Add(1)
    go databaseWorker(&wg, db)
    wg.Wait()
    fmt.Println("All workers finished")
}
  • 分析:在databaseWorker函数中,获取数据库连接后,如果发生错误,没有关闭连接就返回,这会导致数据库连接一直占用,造成资源泄漏。

避免资源泄漏的方法

  1. 使用Context管理goroutine生命周期
    • 示例
package main

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

func worker(ctx context.Context, wg *sync.WaitGroup) {
    defer wg.Done()
    for {
        select {
        case <-ctx.Done():
            return
        default:
            // 执行任务
            fmt.Println("Working...")
            time.Sleep(time.Second)
        }
    }
}

func main() {
    var wg sync.WaitGroup
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    wg.Add(1)
    go worker(ctx, &wg)
    wg.Wait()
    fmt.Println("All workers finished")
}
  • 解释:通过context.WithTimeout创建一个带有超时的Context,在worker函数中使用select监听ctx.Done()通道。当Context超时或者被取消时,ctx.Done()通道会被关闭,worker函数可以及时退出,避免goroutine无限运行。
  1. 正确处理错误并释放资源
    • 示例
package main

import (
    "database/sql"
    "fmt"
    "sync"

    _ "github.com/go - sql - driver/mysql"
)

func databaseWorker(wg *sync.WaitGroup, db *sql.DB) {
    defer wg.Done()
    // 从数据库连接池获取连接
    conn, err := db.Conn(nil)
    if err!= nil {
        // 错误处理并关闭连接
        fmt.Println("Error getting connection:", err)
        return
    }
    defer conn.Close()
    // 这里假设处理数据库操作
}

func main() {
    db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/test")
    if err!= nil {
        panic(err)
    }
    defer db.Close()

    var wg sync.WaitGroup
    wg.Add(1)
    go databaseWorker(&wg, db)
    wg.Wait()
    fmt.Println("All workers finished")
}
  • 解释:在databaseWorker函数中,获取数据库连接后,使用defer语句确保无论是否发生错误,连接都会被正确关闭。这样可以避免资源泄漏。同时,在错误处理部分,可以根据实际情况进行日志记录等操作。
  1. 结合WaitGroup和Context
    • 示例
package main

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

func complexWorker(ctx context.Context, wg *sync.WaitGroup) {
    defer wg.Done()
    for {
        select {
        case <-ctx.Done():
            return
        default:
            // 执行复杂任务,例如调用其他可能产生资源的函数
            fmt.Println("Doing complex work...")
            time.Sleep(time.Second)
        }
    }
}

func main() {
    var wg sync.WaitGroup
    ctx, cancel := context.WithCancel(context.Background())

    for i := 0; i < 3; i++ {
        wg.Add(1)
        go complexWorker(ctx, &wg)
    }

    // 模拟一段时间后取消任务
    time.Sleep(2 * time.Second)
    cancel()
    wg.Wait()
    fmt.Println("All workers finished")
}
  • 解释:这里通过context.WithCancel创建一个可取消的Context,启动多个goroutine执行复杂任务。在每个goroutine中监听ctx.Done()通道以实现及时退出。同时使用WaitGroup确保所有goroutine完成后再继续执行后续代码,避免资源泄漏。