当前位置: 首页 > article >正文

第 15 章 -Go 语言 并发编程

在 Go 语言中,goroutine 是实现并发编程的核心机制之一。接下来将简要介绍 goroutine 的定义和使用方法,Go 语言的并发模型,以及并发编程的优缺点。

Goroutine 的定义和使用

定义

  • Goroutine 是由 Go 运行时管理的轻量级线程。它是由 Go 运行时环境调度的一个函数或方法调用,可以在同一个操作系统线程上并发执行多个 goroutine。
  • 每个 goroutine 都有自己的栈,初始大小非常小(大约为 2KB),可以根据需要动态增长或缩小。这使得创建成千上万个 goroutine 成为可能,而不会消耗过多内存。

使用

  • 创建一个 goroutine 非常简单,只需在调用函数前加上关键字 go 即可。例如:
    go func() {
        fmt.Println("Hello from a goroutine!")
    }()
    
  • 上述代码启动了一个新的 goroutine,并立即返回主程序继续执行。新启动的 goroutine 将独立于主程序运行。
  • 要等待所有 goroutine 完成,通常可以使用 sync.WaitGroup 或者通道(channel)来同步。

并发模型

Go 语言采用了 CSP(Communicating Sequential Processes,通信顺序进程)并发模型,通过通道(channels)来传递数据,而不是共享内存。这种模型鼓励通过消息传递来实现并发,从而避免了传统多线程编程中的锁竞争问题,提高了程序的可读性和可维护性。

  • 通道(Channel):是 Go 语言中用于 goroutine 之间通信和同步的机制。通过通道,一个 goroutine 可以向另一个 goroutine 发送数据,接收方 goroutine 可以从通道接收数据。通道是类型安全的,只能传输特定类型的值。

并发的优缺点

优点

  • 提高效率:并发可以充分利用多核处理器的能力,加快程序执行速度。
  • 响应性:通过并发处理任务,可以使应用程序在执行长时间操作时仍然保持用户界面的响应性。
  • 资源利用率:合理利用系统资源,如 CPU 和 I/O,提高整体性能。

缺点

  • 复杂性增加:并发编程增加了程序的复杂度,尤其是当涉及到多个 goroutine 之间的交互时。
  • 调试困难:由于并发程序的行为具有不确定性,因此调试并发错误比单线程程序更加困难。
  • 潜在的竞态条件:如果并发控制不当,可能会导致竞态条件,即多个 goroutine 同时访问和修改同一块数据,可能导致数据不一致或程序崩溃。

总之,Go 语言的并发模型和 goroutine 提供了一种强大且简洁的方式来编写高效、可扩展的应用程序。然而,正确地设计和实现并发程序需要对并发概念有深刻的理解。

我们可以进一步深入探讨 Go 语言中的并发编程相关话题,包括更具体的示例、最佳实践以及一些高级特性。

更具体的示例

使用 sync.WaitGroup 等待多个 Goroutine

当你需要确保所有的 Goroutine 在程序结束前完成工作时,可以使用 sync.WaitGroup。下面是一个简单的例子:

package main

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

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // 告诉 WaitGroup 我已经完成了
    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 <= 5; i++ {
        wg.Add(1) // 增加 WaitGroup 的计数
        go worker(i, &wg)
    }

    wg.Wait() // 等待所有工作完成
}
通过 Channel 通信

Goroutines 之间可以通过 Channel 进行通信,这是 Go 语言推荐的方式。下面是一个简单的生产者-消费者模式的例子:

package main

import (
    "fmt"
)

func producer(ch chan<- int) {
    for i := 1; i <= 3; i++ {
        ch <- i // 发送数据到 channel
    }
    close(ch) // 关闭 channel 表示没有更多的数据发送
}

func consumer(ch <-chan int) {
    for num := range ch {
        fmt.Println("Received:", num)
    }
}

func main() {
    ch := make(chan int)

    go producer(ch)
    go consumer(ch)

    // 主 goroutine 需要等待其他 goroutine 完成
    var input string
    fmt.Scanln(&input)
}

最佳实践

  1. 避免共享状态:尽可能使用通道进行数据交换,而不是共享变量。
  2. 使用上下文(Context):在长时间运行的任务中,使用 context.Context 来传递取消信号和其他请求范围的数据。
  3. 限制并发数量:使用 sync.WaitGroupsemaphore(信号量)来限制同时运行的 Goroutine 数量,防止资源耗尽。
  4. 错误处理:确保每个 Goroutine 都能妥善处理错误,并且能够优雅地关闭。
  5. 测试:使用 Go 的测试框架和工具(如 testing 包和 race detector)来检测并修复竞态条件。

高级特性

Select 语句

select 语句允许在一个 goroutine 中监听多个通道上的通信操作。当多个 case 都准备好时,会随机选择一个执行。如果没有 case 准备好,那么 select 会阻塞直到某个 case 准备好。如果有一个 default 分支,那么 select 不会阻塞。

package main

import (
    "fmt"
    "time"
)

func main() {
    c1 := make(chan string)
    c2 := make(chan string)

    go func() {
        time.Sleep(time.Second * 1)
        c1 <- "one"
    }()

    go func() {
        time.Sleep(time.Second * 2)
        c2 <- "two"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-c1:
            fmt.Println("received:", msg1)
        case msg2 := <-c2:
            fmt.Println("received:", msg2)
        }
    }
}
Context 包

context 包提供了上下文对象,用于携带截止时间、取消信号以及其他请求范围的值。这对于构建大型应用时管理请求的生命周期非常有用。

package main

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

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    select {
    case <-time.After(1 * time.Second):
        fmt.Println("Operation completed before the timeout")
    case <-ctx.Done():
        fmt.Println("Operation timed out")
    }
}

我们可以继续深入探讨一些更高级的主题,包括 Goroutine 调度、内存模型、以及一些常见的并发模式和陷阱。

Goroutine 调度

调度器的工作原理

Go 的调度器负责管理和调度 Goroutine。它使用 M:N 调度模型,其中 M 表示操作系统线程,N 表示 Goroutine。具体来说:

  • P (Processor):代表一个逻辑处理器,每个 P 可以运行多个 Goroutine。
  • M (Machine):代表一个操作系统线程。
  • G (Goroutine):代表一个轻量级的协程。

调度器的主要职责包括:

  • 创建和销毁 Goroutine
  • 将 Goroutine 分配给 P
  • 在不同的 M 之间迁移 Goroutine
  • 处理抢占和阻塞
调度器的配置

你可以通过环境变量和运行时参数来调整调度器的行为:

  • GOMAXPROCS:设置可用的逻辑处理器数量,默认值为机器的 CPU 核心数。

    export GOMAXPROCS=4
    
  • GOGC:设置垃圾回收的触发阈值,默认值为 100。

    export GOGC=50
    

内存模型

Go 语言的内存模型定义了多个 Goroutine 之间内存访问的规则。主要原则包括:

  • 发生顺序:程序中的每个操作都有一个发生顺序,这个顺序决定了操作的可见性。
  • 同步操作:通过同步原语(如通道、互斥锁等)来保证操作的顺序性和可见性。
  • 数据竞争:当两个 Goroutine 同时访问同一个变量且至少有一个是写操作时,会发生数据竞争。Go 提供了 -race 标志来检测数据竞争。

常见的并发模式

工作池模式

工作池模式用于限制并发任务的数量,避免资源耗尽。通常使用一个固定大小的 Goroutine 池来处理任务。

package main

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

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Println("worker", id, "processing job", j)
        time.Sleep(time.Second)
        results <- j * 2
    }
}

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

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

    var wg sync.WaitGroup

    for w := 1; w <= numWorkers; w++ {
        wg.Add(1)
        go func(w int) {
            worker(w, jobs, results)
            wg.Done()
        }(w)
    }

    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)

    wg.Wait()
    close(results)

    for r := range results {
        fmt.Println("result", r)
    }
}
扇出/扇入模式

扇出/扇入模式用于将任务分发给多个 Goroutine 处理,然后收集结果。适用于并行处理大量数据的场景。

package main

import (
    "fmt"
    "sync"
)

func process(data int, wg *sync.WaitGroup) {
    defer wg.Done()
    // 模拟处理数据
    fmt.Println("Processing data:", data)
}

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

    // 扇出
    for _, d := range data {
        wg.Add(1)
        go process(d, &wg)
    }

    // 扇入
    wg.Wait()
    fmt.Println("All tasks completed")
}

常见的并发陷阱

数据竞争

数据竞争是最常见的并发问题之一。当多个 Goroutine 同时访问同一个变量且至少有一个是写操作时,会导致数据不一致。

package main

import (
    "fmt"
    "sync"
)

var count int
var mu sync.Mutex

func increment(wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 0; i < 1000; i++ {
        mu.Lock()
        count++
        mu.Unlock()
    }
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go increment(&wg)
    }
    wg.Wait()
    fmt.Println("Final count:", count)
}
死锁

死锁发生在多个 Goroutine 相互等待对方释放资源时。常见的死锁原因包括循环等待和资源分配顺序不当。

package main

import (
    "fmt"
    "sync"
)

var (
    mu1 sync.Mutex
    mu2 sync.Mutex
)

func main() {
    go func() {
        mu1.Lock()
        fmt.Println("Locked mu1")
        mu2.Lock()
        fmt.Println("Locked mu2")
        mu2.Unlock()
        mu1.Unlock()
    }()

    go func() {
        mu2.Lock()
        fmt.Println("Locked mu2")
        mu1.Lock()
        fmt.Println("Locked mu1")
        mu1.Unlock()
        mu2.Unlock()
    }()

    var input string
    fmt.Scanln(&input)
}
饿死

饿死是指某些 Goroutine 由于资源不足或调度不公平而无法获得执行机会。可以通过设置合理的超时和优先级来避免。

package main

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

func longRunningTask(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("Task cancelled")
            return
        default:
            fmt.Println("Working...")
            time.Sleep(time.Second)
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    go longRunningTask(ctx)

    time.Sleep(6 * time.Second)
    fmt.Println("Main function finished")
}

总结

Go 语言的并发模型和 Goroutine 提供了强大的工具来编写高效、可扩展的程序。通过理解调度器的工作原理、内存模型、常见的并发模式和陷阱,你可以更好地设计和实现并发程序。希望这些内容对你有所帮助!


http://www.kler.cn/a/397234.html

相关文章:

  • 网络基础(4)传输层
  • springboot实现简单的数据查询接口(无实体类)
  • Docker占用空间太大磁盘空间不足清理妙招
  • 源码解析-Spring Eureka(更新ing)
  • 同比缩放,64的倍数,最大值
  • 微信小程序navigateTo:fail webview count limit exceed
  • C# 常用三方库
  • 主界面获取个人信息客户端方
  • 归并排序(C语言)
  • python基础知识(四)——发送请求、接口关联
  • 问:说说SpringDAO及ORM的用法?
  • MySQL技巧之跨服务器数据查询:基础篇-A数据库与B数据库查询合并--封装到存储过程中
  • Spring Boot基础教学:创建第一个Spring Boot项目
  • 背景替换大模型图像处理gradio部署服务
  • Vue 项目打包后环境变量丢失问题(清除缓存),区分.env和.env.*文件
  • 革新人脸图片智能修复
  • 【算法】【优选算法】前缀和(上)
  • ‌REST风格(Representational State Transfer)
  • 神经网络的正则化(一)
  • 设计模式:工厂方法模式和策略模式
  • “南海明珠”-黄岩岛(民主礁)领海基线WebGIS绘制实战
  • C# x Unity 从玩家控制类去分析命令模式该如何使用
  • 精通rust宏系列教程-调试过程宏
  • stm32 内部温度传感器使用
  • 封装一个省市区的筛选组件
  • 【提高篇】3.3 GPIO(三,工作模式详解 上)