首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >轻松掌握 Go 语言并发模型

轻松掌握 Go 语言并发模型

作者头像
FunTester
发布2025-07-14 16:16:24
发布2025-07-14 16:16:24
11200
代码可运行
举报
文章被收录于专栏:FunTesterFunTester
运行总次数:0
代码可运行

现代应用程序通常需要同时处理海量任务,如果您的应用程序没有为此做好准备,它可能会变得笨重且响应迟缓。这就是并发如此重要的原因。Go 语言(通常称为 Golang)内置了并发功能,让您可以编写更快、更灵活的软件,而无需陷入线程管理的泥潭。

在本指南中,我们将首先回顾传统程序如何按顺序执行任务,以及为什么这种方式会导致性能瓶颈。接着,我们将通过 goroutine 和通道探索 Go 的并发模型,对比并发与并行的区别,并通过实际示例(包括一个小型的真实用例)加深理解。如果您已经掌握了基本的 Go 语法,但希望深入了解 goroutine,那么这篇文章正是为您准备的。

传统程序执行:逐行执行

大多数传统程序会逐条执行指令,并阻塞后续指令,直到当前指令完成为止。这种方式简单直观,但当任务耗时较长时,效率会大打折扣。

下面是个例子:

代码语言:javascript
代码运行次数:0
运行
复制
#include <stdio.h>

int main() {
    printf("Step 1: Fetch user data\n");
    printf("Step 2: Process user data\n");
    printf("Step 3: Save user data\n");
    return ;
}

预期输出:

代码语言:javascript
代码运行次数:0
运行
复制
Step 1: Fetch user data
Step 2: Process user data
Step 3: Save user data

这种顺序执行的流程虽然清晰,但如果“获取用户数据”需要几秒钟,整个程序就会停滞不前,等待该操作完成。对于需要同时处理多个用户、进程或数据源的系统来说,这无疑是一个性能瓶颈。

为什么并发如此重要

假设您正在构建一个需要同时处理多个请求的 Web 服务。如果服务器逐个处理这些请求,每个请求都可能需要排队等待,即使有其他任务或 CPU 内核可用。并发允许您的程序在任务之间灵活切换,充分利用等待时间(例如 I/O 等待),从而保持应用程序的高响应性。

在深入探讨 Go 的并发特性之前,我们需要澄清两个经常被混淆的概念:并发并行

  • • 并发:同时处理多个任务,可能在单个 CPU 核心上交替执行。并发更像是一个人同时处理多个任务,例如边煮饭边听音乐。
  • • 并行:在不同的 CPU 核心上同时执行多个任务。并行则更像是两个人分别做饭和听音乐,互不干扰。

在 Go 中,goroutine 让您可以轻松构建并发程序。如果您在多核系统上运行 Go 程序(或使用 runtime.GOMAXPROCS),Go 会自动在可用的 CPU 核心上并行调度这些 goroutine。但即使在单核上,并发性也能确保任务不会不必要地相互阻塞。

Go 中使用 Goroutines 实现并发

Go 引入了 goroutine,这是一种轻量级的并发执行单元。您只需在函数调用前加上 go 关键字,即可启动一个 goroutine。

示例:基本 Goroutines

代码语言:javascript
代码运行次数:0
运行
复制
package main

import (
    "fmt"
    "time"
)

func task(name string) {
    for i := ; i <= ; i++ {
        fmt.Println(name, "running", i)
        time.Sleep(time.Millisecond * )
    }
}

func main() {
    go task("FunTester Task 1")
    go task("FunTester Task 2")

    // 防止主函数提前退出
    time.Sleep(time.Second * )
    fmt.Println("Main function completed")
}

预期输出:

代码语言:javascript
代码运行次数:0
运行
复制
FunTester Task 1 running 1
FunTester Task 2 running 1
FunTester Task 1 running 2
FunTester Task 2 running 2
FunTester Task 1 running 3
FunTester Task 2 running 3
Main function completed

可以看到,“Task 1”和“Task 2”的输出是交错的,这正是并发性的体现。

Goroutines 与 channel

Go 并发模型的核心思想是通过通信来共享内存,而不是直接共享内存。通道(channel) 是 goroutine 之间传递数据的安全方式。

无缓冲通道的创建方式如下:

代码语言:javascript
代码运行次数:0
运行
复制
ch := make(chan int)

当您向无缓冲通道发送数据时,发送方会阻塞,直到接收方接收到数据。

示例:无缓冲通道
代码语言:javascript
代码运行次数:0
运行
复制
package main

import (
    "fmt"
    "time"
)

func sendData(ch chan<- int, data int) {
    fmt.Println("Sending", data)
    ch <- data // 阻塞直到接收
    fmt.Println("Finished sending", data)
}

func receiveData(ch <-chan int) {
    val := <-ch // 阻塞直到接收到数据
    fmt.Println("Received", val)
}

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

    go sendData(ch, )
    go receiveData(ch)

    time.Sleep(time.Second)
    fmt.Println("Done")
}

预期输出:

代码语言:javascript
代码运行次数:0
运行
复制
Sending 10
Received 10
Finished sending 10
Done

避免竞争条件

当多个 goroutine 同时访问共享数据,且至少有一个 goroutine 修改该数据时,就会发生竞争条件。为了避免这种情况,Go 提供了多种同步机制,例如 互斥锁(Mutex) 和 通道

示例:使用互斥锁
代码语言:javascript
代码运行次数:0
运行
复制
package main

import (
    "fmt"
    "sync"
)

type safeCounter struct {
    mu    sync.Mutex
    count int
}

func (sc *safeCounter) increment() {
    sc.mu.Lock()
    sc.count++
    sc.mu.Unlock()
}

func main() {
    sc := &safeCounter{}
    var wg sync.WaitGroup
    wg.Add()

    go func() {
        defer wg.Done()
        for i := ; i < ; i++ {
            sc.increment()
        }
    }()

    go func() {
        defer wg.Done()
        for i := ; i < ; i++ {
            sc.increment()
        }
    }()

    wg.Wait()
    fmt.Println("Final count:", sc.count)
}

并发 Web 服务器

以下是一个简单的并发 Web 服务器示例,用于计算某个数字的阶乘。通过为每个请求启动一个 goroutine,服务器可以同时处理多个请求,而不会阻塞。

代码语言:javascript
代码运行次数:0
运行
复制
package main

import (
    "fmt"
    "log"
    "net/http"
    "strconv"
    "sync"
)

func factorial(n int) int {
    if n <=  {
        return 
    }
    return n * factorial(n-1)
}

func main() {
    var wg sync.WaitGroup

    http.HandleFunc("/factorial", func(w http.ResponseWriter, r *http.Request) {
        nStr := r.URL.Query().Get("n")
        n, err := strconv.Atoi(nStr)
        if err != nil {
            http.Error(w, "Invalid number", http.StatusBadRequest)
            return
        }

        wg.Add()
        go func(num int) {
            defer wg.Done()
            result := factorial(num)
            fmt.Fprintf(w, "Factorial(%d) = %d\n", num, result)
        }(n)
    })

    log.Println("Server starting at :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

最佳实践

  1. 1. 使用同步工具:如 sync.WaitGroup、通道和 context.Context,避免使用 time.Sleep 来协调 goroutine。通过这些工具可以更精确地控制并发行为,减少不必要的等待。
  2. 2. 限制 Goroutine 数量:尽管 goroutine 轻量,但过多的 goroutine 仍可能导致资源耗尽。例如,您可以使用缓冲通道或工作池来限制并发任务的数量。
  3. 3. 使用缓冲通道进行速率限制:避免因大量任务写入共享资源而导致阻塞。例如,处理海量数据时,可以通过缓冲通道控制任务的处理速度。
  4. 4. 防止 Goroutine 泄漏:使用 context.WithCancel 或其他机制确保不再需要的 goroutine 被正确终止。泄漏的 goroutine 会占用系统资源,影响程序性能。
  5. 5. 检查竞争条件:在开发和测试中使用 -race 标志检测潜在的竞争条件。竞争条件可能导致数据不一致或程序崩溃,需特别注意。
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-07-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 FunTester 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 传统程序执行:逐行执行
  • 为什么并发如此重要
  • Go 中使用 Goroutines 实现并发
  • Goroutines 与 channel
  • 避免竞争条件
  • 并发 Web 服务器
    • 最佳实践
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档