深入探究 Go 中的并发性(不使用 Channels)

Go 的并发模型是其最突出的功能之一。虽然通道经常成为 goroutine 通信的焦点,但理解并发的核心,而不依赖通道,对于掌握 Go 至关重要。这篇文章深入探讨了 goroutine、`sync` 包和同步的实用模式。

并发与并行

并发是指同时管理多个任务,而并行是指同时执行多个任务。Go 专为并发而设计,因此可以轻松构建能够独立处理多个操作的程序。

Goroutines:构建块

**goroutine** 是 Go 运行时管理的轻量级线程。创建 goroutine 非常简单,只需在函数调用前加上“go”前缀即可。

package main  

import (  
    "fmt"  
    "time"  
)  

func task(name string) {  
    for i := 0; i < 5; i++ {  
        fmt.Printf("Task %s is running: %d\n", name, i)  
        time.Sleep(time.Millisecond * 500)  
    }  
}  

func main() {  
    go task("A")  
    go task("B")  

    // Let the main function wait for goroutines to finish  
    time.Sleep(time.Second * 3)  
    fmt.Println("Main function exiting")  
}

无通道同步

使用 sync.WaitGroup

`sync.WaitGroup` 是一个强大的工具,可以等待多个 goroutine 完成其工作。

package main  

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

func worker(id int, wg *sync.WaitGroup) {  
    defer wg.Done() // Decrement the counter when the goroutine completes  
    fmt.Printf("Worker %d starting\n", id)  
    time.Sleep(time.Second)  
    fmt.Printf("Worker %d done\n", id)  
}  

func main() {  
    var wg sync.WaitGroup  

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

    wg.Wait() // Wait for all goroutines to finish  
    fmt.Println("All workers completed")  
}

使用 sync.Mutex

当多个 goroutine 访问共享数据时,可能会发生竞争条件。`sync.Mutex` 可确保一次只有一个 goroutine 可以访问代码的关键部分。

package main  

import (  
    "fmt"  
    "sync"  
)  

type Counter struct {  
    value int  
    mu    sync.Mutex  
}  

func (c *Counter) Increment() {  
    c.mu.Lock()  
    c.value++  
    c.mu.Unlock()  
}  

func main() {  
    counter := &Counter{}  
    var wg sync.WaitGroup  

    for i := 0; i < 10; i++ {  
        wg.Add(1)  
        go func() {  
            defer wg.Done()  
            counter.Increment()  
        }()  
    }  

    wg.Wait()  
    fmt.Printf("Final Counter Value: %d\n", counter.value)  
}

实用模式

无通道的工作池

工作池是一种模式,多个工作器同时执行任务。工作器可以从受互斥锁保护的共享切片访问任务,而不是通过通道。

package main  

import (  
    "fmt"  
    "sync"  
)  

func worker(id int, tasks *[]int, mu *sync.Mutex, wg *sync.WaitGroup) {  
    defer wg.Done()  

    for {  
        mu.Lock()  
        if len(*tasks) == 0 {  
            mu.Unlock()  
            return  
        }  
        task := (*tasks)[0]  
        *tasks = (*tasks)[1:]  
        mu.Unlock()  

        fmt.Printf("Worker %d processing task %d\n", id, task)  
    }  
}  

func main() {  
    tasks := []int{1, 2, 3, 4, 5}  
    var mu sync.Mutex  
    var wg sync.WaitGroup  

    for i := 1; i <= 3; i++ {  
        wg.Add(1)  
        go worker(i, &tasks, &mu, &wg)  
    }  

    wg.Wait()  
    fmt.Println("All tasks processed")  
}

并发陷阱

  • 竞争条件:当多个 goroutine 访问和修改共享数据时,竞争条件可能会导致不可预测的行为。sync.Mutex 和 sync/atomic 等工具有助于缓解这种情况。
  • 死锁:当 goroutine 无限期地等待彼此锁定的资源时发生。仔细规划对于避免死锁至关重要。
  • 饥饿:如果其他 goroutine 占据了大部分资源,则某个 goroutine 可能会被无限期阻塞。
  • 结论

    即使没有通道,Go 中的并发功能也非常强大。通过掌握 goroutines、`sync` 包和常见模式,您可以编写高效、高性能的程序。试用这些工具,您很快就会明白为什么 Go 是并发编程的首选!