Go 语言中的并发问题 | 豆包MarsCode AI刷题

106 阅读3分钟

Go 语言中的并发问题

Go 语言是一种擅长并发编程的语言,它内置了强大的并发原语,如 goroutine 和 channel,使得开发者能够轻松地编写并发程序。然而,即使有了这些强大的工具,在编写并发程序时仍然会遇到一些常见的问题。本文将探讨 Go 语言中的几个并发问题,并提供相应的解决方案。

1. 竞争条件(Race Condition)

竞争条件是并发编程中最常见的问题之一。当多个 goroutine 同时访问共享资源,并且访问顺序会影响程序的最终结果时,就会出现竞争条件。

示例:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var count int
    var wg sync.WaitGroup

    wg.Add(100)
    for i := 0; i < 100; i++ {
        go func() {
            defer wg.Done()
            count++
        }()
    }
    wg.Wait()
    fmt.Println("Final count:", count)
}

在上述示例中,100 个 goroutine 同时对 count 变量进行自增操作。由于缺乏同步,可能会出现最终结果不等于 100 的情况,这就是典型的竞争条件。

解决方案:

为了解决竞争条件,我们可以使用 Go 语言提供的同步原语,如 sync.Mutexsync.RWMutex

package main

import (
    "fmt"
    "sync"
)

func main() {
    var count int
    var mutex sync.Mutex
    var wg sync.WaitGroup

    wg.Add(100)
    for i := 0; i < 100; i++ {
        go func() {
            defer wg.Done()
            mutex.Lock()
            defer mutex.Unlock()
            count++
        }()
    }
    wg.Wait()
    fmt.Println("Final count:", count)
}

在上述示例中,我们使用 sync.Mutex 来保护 count 变量,确保每次只有一个 goroutine 能够访问它。这样就可以避免竞争条件的发生。

2. 死锁(Deadlock)

死锁是并发编程中另一个常见的问题。当两个或多个 goroutine 相互等待对方持有的资源时,就会发生死锁。

示例:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var mutex1, mutex2 sync.Mutex

    wg := sync.WaitGroup{}
    wg.Add(2)

    go func() {
        defer wg.Done()
        mutex1.Lock()
        fmt.Println("Locked mutex1")
        mutex2.Lock()
        fmt.Println("Locked mutex2")
        mutex2.Unlock()
        mutex1.Unlock()
    }()

    go func() {
        defer wg.Done()
        mutex2.Lock()
        fmt.Println("Locked mutex2")
        mutex1.Lock()
        fmt.Println("Locked mutex1")
        mutex1.Unlock()
        mutex2.Unlock()
    }()

    wg.Wait()
}

在上述示例中,两个 goroutine 分别尝试获取 mutex1mutex2,但是由于获取顺序不同,最终会导致死锁。

解决方案:

为了避免死锁,我们需要确保所有 goroutine 以相同的顺序获取资源。一种常见的方法是使用 sync.Locker 接口,并将所有锁定的顺序定义为一个全局常量。

package main

import (
    "fmt"
    "sync"
)

type lockOrder []sync.Locker

func (l lockOrder) Lock() {
    for _, locker := range l {
        locker.Lock()
    }
}

func (l lockOrder) Unlock() {
    for i := len(l) - 1; i >= 0; i-- {
        l[i].Unlock()
    }
}

func main() {
    mutex1, mutex2 := sync.Mutex{}, sync.Mutex{}
    locks := lockOrder{&mutex1, &mutex2}

    wg := sync.WaitGroup{}
    wg.Add(2)

    go func() {
        defer wg.Done()
        locks.Lock()
        fmt.Println("Locked both mutexes")
        locks.Unlock()
    }()

    go func() {
        defer wg.Done()
        locks.Lock()
        fmt.Println("Locked both mutexes")
        locks.Unlock()
    }()

    wg.Wait()
}

在上述示例中,我们定义了一个 lockOrder 类型,它实现了 sync.Locker 接口。所有 goroutine 都使用这个 lockOrder 来获取和释放锁,从而确保了获取资源的顺序一致性,避免了死锁的发生。

3. 内存泄漏

内存泄漏是另一个常见的并发问题。当 goroutine 无法正确地终止时,它们可能会一直占用内存,导致内存泄漏。

示例:

package main

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

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            select {
            case <-time.After(time.Minute):
                fmt.Println("Goroutine finished")
            }
        }()
    }

    wg.Wait()
    fmt.Println("All goroutines finished")
}

在上述示例中,我们创建了 1000 个 goroutine,每个 goroutine 都会等待 1 分钟。如果程序在 1 分钟内退出,那么这些 goroutine 将无法正常终止,从而导致内存泄漏。

解决方案:

为了避免内存泄漏,我们需要确保所有 goroutine 都能够正确地终止。一种常见的方法是使用 context 包来管理 goroutine 的生命周期。

package main

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

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

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            select {
            case <-ctx.Done():
                fmt.Println("Goroutine finished")
            case <-time.After(time.Minute):
                fmt.Println("Goroutine timed out")
            }
        }()
    }

    time.Sleep(30 * time.Second)
    cancel()
    wg.Wait()
    fmt.Println("All goroutines finished")
}

在上述示例中,我们使用 context.WithCancel 创建了一个可取消的上下文。在 30 秒后,我们调用 cancel() 函数来取消上下文,这将导致所有 goroutine 都收到取消信号并正常终止,从而避免了内存泄漏。

通过以上三个示例,我们探讨了 Go 语言中并发编程的一些常见问题,包括竞争条件、死锁和内存泄漏,并提供了相应的解决方案。掌握这些知识对于编写高质量的并发程序至关重要。