MST

星途 面试题库

面试题:Go闭包在并发编程中提升效率的考量

在Go的并发编程环境下,使用闭包提升代码效率需要注意哪些问题?比如资源竞争、内存管理等方面,请详细阐述。
17.7万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

资源竞争问题

  1. 共享变量:闭包可能会引用外部作用域的变量,若多个 goroutine 同时访问和修改这些共享变量,会引发资源竞争。例如:
package main

import (
    "fmt"
)

func main() {
    num := 0
    var funcs []func()
    for i := 0; i < 10; i++ {
        funcs = append(funcs, func() {
            num = num + 1
            fmt.Println(num)
        })
    }
    for _, f := range funcs {
        go f()
    }
    // 这里为了让程序运行一会以便看到并发效果,实际应用中可以采用更优雅的同步方式
    select {}
}

在此例中,num 是共享变量,多个 goroutine 同时修改它,结果不可预测。解决办法是使用互斥锁(sync.Mutex):

package main

import (
    "fmt"
    "sync"
)

func main() {
    var mu sync.Mutex
    num := 0
    var funcs []func()
    for i := 0; i < 10; i++ {
        funcs = append(funcs, func() {
            mu.Lock()
            num = num + 1
            fmt.Println(num)
            mu.Unlock()
        })
    }
    for _, f := range funcs {
        go f()
    }
    select {}
}
  1. 数据结构:对于复杂的数据结构,如 map,在并发闭包中使用也需注意。Go 语言的 map 不是线程安全的,若多个 goroutine 通过闭包同时读写 map,会导致程序崩溃。例如:
package main

import (
    "fmt"
)

func main() {
    m := make(map[string]int)
    var funcs []func()
    for i := 0; i < 10; i++ {
        key := fmt.Sprintf("key%d", i)
        funcs = append(funcs, func() {
            m[key] = i
            fmt.Println(m[key])
        })
    }
    for _, f := range funcs {
        go f()
    }
    select {}
}

解决方法是使用 sync.Map,它是线程安全的 map 实现:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var m sync.Map
    var funcs []func()
    for i := 0; i < 10; i++ {
        key := fmt.Sprintf("key%d", i)
        funcs = append(funcs, func() {
            m.Store(key, i)
            value, _ := m.Load(key)
            fmt.Println(value)
        })
    }
    for _, f := range funcs {
        go f()
    }
    select {}
}

内存管理问题

  1. 闭包捕获变量的生命周期:闭包会延长它所捕获变量的生命周期。若闭包持有大对象的引用,且该闭包在 goroutine 中长期运行,可能导致内存无法及时释放。例如:
package main

import (
    "fmt"
    "time"
)

func bigObjectGenerator() []byte {
    return make([]byte, 1024*1024) // 生成 1MB 的大对象
}

func main() {
    var funcs []func()
    for i := 0; i < 10; i++ {
        data := bigObjectGenerator()
        funcs = append(funcs, func() {
            fmt.Println(len(data))
            time.Sleep(time.Second)
        })
    }
    for _, f := range funcs {
        go f()
    }
    time.Sleep(2 * time.Second)
}

在此例中,data 是大对象,闭包持有其引用,在 goroutine 运行期间,这些内存不会被释放。优化方法是在闭包使用完数据后,将其设置为 nil,以便垃圾回收器回收内存:

package main

import (
    "fmt"
    "time"
)

func bigObjectGenerator() []byte {
    return make([]byte, 1024*1024)
}

func main() {
    var funcs []func()
    for i := 0; i < 10; i++ {
        data := bigObjectGenerator()
        funcs = append(funcs, func() {
            fmt.Println(len(data))
            data = nil
            time.Sleep(time.Second)
        })
    }
    for _, f := range funcs {
        go f()
    }
    time.Sleep(2 * time.Second)
}
  1. 避免无意义的闭包嵌套:多层闭包嵌套可能会使代码逻辑复杂,同时增加内存开销。因为每一层闭包都可能捕获外部变量,导致更多的内存占用。例如:
package main

import "fmt"

func outer() func() {
    num := 10
    return func() {
        inner := func() {
            fmt.Println(num)
        }
        inner()
    }
}

func main() {
    f := outer()
    f()
}

在此例中,inner 闭包嵌套在 outer 返回的闭包内,这种嵌套可能是不必要的,可简化为:

package main

import "fmt"

func outer() func() {
    num := 10
    return func() {
        fmt.Println(num)
    }
}

func main() {
    f := outer()
    f()
}

闭包与 goroutine 泄漏

  1. 未结束的 goroutine:若闭包启动的 goroutine 没有正确结束,会导致 goroutine 泄漏。例如:
package main

import (
    "fmt"
    "time"
)

func main() {
    var funcs []func()
    for i := 0; i < 10; i++ {
        funcs = append(funcs, func() {
            go func() {
                for {
                    fmt.Println("Leaking goroutine")
                    time.Sleep(time.Second)
                }
            }()
        })
    }
    for _, f := range funcs {
        f()
    }
    time.Sleep(2 * time.Second)
}

在这个例子中,闭包内启动的 goroutine 是一个无限循环,不会结束,导致 goroutine 泄漏。解决方法是提供一种退出机制,如使用 context.Context

package main

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

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    var funcs []func()
    for i := 0; i < 10; i++ {
        funcs = append(funcs, func() {
            go func(ctx context.Context) {
                for {
                    select {
                    case <-ctx.Done():
                        return
                    default:
                        fmt.Println("Running goroutine")
                        time.Sleep(time.Second)
                    }
                }
            }(ctx)
        })
    }
    for _, f := range funcs {
        f()
    }
    time.Sleep(3 * time.Second)
}