<返回目录     Powered by claud/xia兄

第8课: Goroutines - 并发编程基础

什么是Goroutine?

Goroutine是Go语言实现并发的核心机制,是一种轻量级的线程。与传统操作系统线程相比,goroutine的创建和销毁成本极低,一个Go程序可以轻松创建成千上万个goroutine。

Goroutine的特点

创建Goroutine

使用go关键字即可启动一个新的goroutine。

基本语法

package main

import (
    "fmt"
    "time"
)

func sayHello() {
    fmt.Println("Hello from goroutine!")
}

func main() {
    // 启动一个goroutine
    go sayHello()

    // 主goroutine等待,否则程序会立即退出
    time.Sleep(time.Second)
    fmt.Println("Main function")
}

匿名函数goroutine

package main

import (
    "fmt"
    "time"
)

func main() {
    // 使用匿名函数启动goroutine
    go func() {
        fmt.Println("匿名goroutine执行")
    }()

    // 带参数的匿名goroutine
    go func(msg string) {
        fmt.Println(msg)
    }("Hello, Goroutine!")

    time.Sleep(time.Second)
}

Goroutine的工作原理

GMP调度模型

Go运行时使用GMP模型来调度goroutine:

调度原理:

GOMAXPROCS

package main

import (
    "fmt"
    "runtime"
)

func main() {
    // 获取当前GOMAXPROCS值
    fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))

    // 设置使用的CPU核心数
    runtime.GOMAXPROCS(4)

    // 获取CPU核心数
    fmt.Println("CPU核心数:", runtime.NumCPU())

    // 获取当前goroutine数量
    fmt.Println("Goroutine数量:", runtime.NumGoroutine())
}

Goroutine同步

使用WaitGroup

sync.WaitGroup用于等待一组goroutine完成。

package main

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

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // 完成时调用Done()

    fmt.Printf("Worker %d 开始工作\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d 完成工作\n", id)
}

func main() {
    var wg sync.WaitGroup

    // 启动5个worker
    for i := 1; i <= 5; i++ {
        wg.Add(1) // 增加计数
        go worker(i, &wg)
    }

    // 等待所有goroutine完成
    wg.Wait()
    fmt.Println("所有worker完成")
}

使用Channel同步

package main

import "fmt"

func worker(done chan bool) {
    fmt.Println("工作中...")
    // 模拟工作
    fmt.Println("工作完成")
    done <- true // 发送完成信号
}

func main() {
    done := make(chan bool)
    go worker(done)

    <-done // 等待完成信号
    fmt.Println("主程序退出")
}

Goroutine通信

共享内存 vs 消息传递

Go语言推荐使用channel进行goroutine间通信,而不是共享内存。

Go的并发哲学:

"不要通过共享内存来通信,而应该通过通信来共享内存"

Don't communicate by sharing memory; share memory by communicating.

使用Mutex保护共享数据

package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    mu    sync.Mutex
    value int
}

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

func (c *Counter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.value
}

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

    // 启动100个goroutine,每个增加计数器1000次
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for j := 0; j < 1000; j++ {
                counter.Increment()
            }
        }()
    }

    wg.Wait()
    fmt.Println("最终计数:", counter.Value()) // 100000
}

Goroutine泄漏

Goroutine泄漏是指goroutine无法正常退出,导致资源无法释放。

常见泄漏场景

// 错误示例:goroutine永远阻塞
func leak() {
    ch := make(chan int)
    go func() {
        val := <-ch // 永远等待,因为没有发送者
        fmt.Println(val)
    }()
    // goroutine泄漏!
}

// 正确示例:使用context控制生命周期
func noLeak(ctx context.Context) {
    ch := make(chan int)
    go func() {
        select {
        case val := <-ch:
            fmt.Println(val)
        case <-ctx.Done():
            return // 正常退出
        }
    }()
}

实战示例

示例1:并发下载

package main

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

func download(url string, wg *sync.WaitGroup) {
    defer wg.Done()

    fmt.Printf("开始下载: %s\n", url)
    time.Sleep(time.Second * 2) // 模拟下载
    fmt.Printf("完成下载: %s\n", url)
}

func main() {
    urls := []string{
        "https://example.com/file1.zip",
        "https://example.com/file2.zip",
        "https://example.com/file3.zip",
        "https://example.com/file4.zip",
    }

    var wg sync.WaitGroup

    for _, url := range urls {
        wg.Add(1)
        go download(url, &wg)
    }

    wg.Wait()
    fmt.Println("所有文件下载完成")
}

示例2:并发计算

package main

import (
    "fmt"
    "sync"
)

func sum(numbers []int, result chan int) {
    sum := 0
    for _, num := range numbers {
        sum += num
    }
    result <- sum
}

func main() {
    numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    // 分成两部分并发计算
    mid := len(numbers) / 2
    result := make(chan int, 2)

    go sum(numbers[:mid], result)
    go sum(numbers[mid:], result)

    // 收集结果
    sum1, sum2 := <-result, <-result
    total := sum1 + sum2

    fmt.Println("总和:", total) // 55
}

示例3:工作池模式

package main

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

func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()

    for job := range jobs {
        fmt.Printf("Worker %d 处理任务 %d\n", id, job)
        time.Sleep(time.Millisecond * 500)
        results <- job * 2
    }
}

func main() {
    const numJobs = 10
    const numWorkers = 3

    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)

    var wg sync.WaitGroup

    // 启动worker池
    for w := 1; w <= numWorkers; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }

    // 发送任务
    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)

    // 等待所有worker完成
    go func() {
        wg.Wait()
        close(results)
    }()

    // 收集结果
    for result := range results {
        fmt.Println("结果:", result)
    }
}

性能考虑

Goroutine的开销

项目 Goroutine 系统线程
初始栈大小 2KB 1-2MB
创建时间 ~微秒级 ~毫秒级
上下文切换 ~纳秒级 ~微秒级
最大数量 数十万+ 数千

最佳实践

常见错误:

实践练习

  1. 编写程序,启动10个goroutine并发打印数字
  2. 实现一个并发的网页爬虫
  3. 使用goroutine实现生产者-消费者模式
  4. 编写一个并发的文件处理程序
  5. 实现一个限制并发数量的工作池
  6. 使用goroutine实现超时控制
  7. 编写程序检测goroutine泄漏
  8. 实现一个并发安全的缓存系统