讲师中心 微信公众号

工具推荐

视频效率加速
updream

一键生成高质量推广成片

立刻MV
立刻MV

低成本歌曲 MV、视频生成

Laper
Laper

剧本策划到成片协作

AniShort
AniShort

AI短剧协同制作平台

Go语言并发模式:独立任务的并行处理与同步

风静小哥_5900

风静小哥_5900

发布时间:2025-10-26 10:15:10

|

429人浏览过

|

来源于php中文网

原创

go语言并发模式:独立任务的并行处理与同步

本文探讨了在Go语言中如何高效地实现独立任务的并行处理与同步。通过分析一个具体场景,我们展示了如何利用Go的通道(channels)或`sync.WaitGroup`来协调多个并发执行的worker goroutine,确保所有任务完成后才进行下一步操作,同时维持固定的goroutine数量,避免为每个数据项创建新goroutine,从而优化资源利用和执行效率。

在Go语言中,实现高效的并发处理是其核心优势之一。本教程将深入探讨一种常见的并发模式:如何协调多个独立的worker goroutine并行处理数据,并确保所有worker完成后才继续执行后续逻辑。我们将通过一个具体的案例来演示这种模式,并提供两种实现方案:基于通道的同步和基于sync.WaitGroup的同步。

场景描述

假设我们有一个主协调器account goroutine,它从一个输入通道接收数据。每接收到一个数据项,account需要将该数据分发给两个独立的worker goroutine(workerA和workerB)进行处理。这两个worker的执行顺序不重要,但account必须等待它们都完成对当前数据项的处理后,才能将该数据发送到最终的输出通道。关键要求如下:

  1. workerA和workerB各自是一个独立的goroutine。
  2. 系统中goroutine的数量应保持恒定,即不应为每个新数据项创建新的goroutine。
  3. workerA和workerB应并行执行,以最大化并发效率。

初始问题与分析

考虑以下初始的account函数实现:

立即学习“go语言免费学习笔记(深入)”;

func account(account_chan <-chan int, final_chan chan<- int) {
    wa_in := make(chan int)
    wa_out := make(chan int)
    wb_in := make(chan int)
    wb_out := make(chan int)

    go workerA(wa_in, wa_out)
    go workerB(wb_in, wb_out)

    for d := range account_chan {
        // 顺序执行:发送给A,等待A完成;发送给B,等待B完成
        wa_in <- d
        <-wa_out

        wb_in <- d
        <-wb_out

        final_chan <- d
    }
}

其中workerA和workerB的定义如下:

func workerA(work_in_chan <-chan int, work_out_chan chan<- int) {
    for d := range work_in_chan {
        fmt.Println("A ", d)
        work_out_chan <- d // 假设这里是处理逻辑,然后发送完成信号
    }
}

func workerB(work_in_chan <-chan int, work_out_chan chan<- int) {
    for d := range work_in_chan {
        fmt.Println("B ", d)
        work_out_chan <- d // 假设这里是处理逻辑,然后发送完成信号
    }
}

这个实现的问题在于,wa_in

解决方案一:通过通道实现并行与同步

要实现workerA和workerB的并行执行,关键在于调整通道操作的顺序:先将数据同时发送给所有worker,然后等待所有worker的完成信号。

Go语言(Golang)1.26.0
Go语言(Golang)1.26.0

Go语言(Golang)1.26.0版本官方下载,版本号 1.26.0,适合旧项目维护、兼容性测试和指定版本开发环境搭建。

下载
package main

import "fmt"
import "time" // 引入time包用于模拟工作耗时

func workerA(work_in_chan <-chan int, work_out_chan chan<- int) {
    for d := range work_in_chan {
        fmt.Printf("WorkerA 正在处理数据: %d\n", d)
        time.Sleep(time.Millisecond * 100) // 模拟工作耗时
        work_out_chan <- d                 // 发送完成信号
    }
}

func workerB(work_in_chan <-chan int, work_out_chan chan<- int) {
    for d := range work_in_chan {
        fmt.Printf("WorkerB 正在处理数据: %d\n", d)
        time.Sleep(time.Millisecond * 150) // 模拟工作耗时,可能与A不同
        work_out_chan <- d                 // 发送完成信号
    }
}

func account(account_chan <-chan int, final_chan chan<- int) {
    wa_in := make(chan int)
    wa_out := make(chan int)
    wb_in := make(chan int)
    wb_out := make(chan int)

    go workerA(wa_in, wa_out)
    go workerB(wb_in, wb_out)

    for d := range account_chan {
        // 先同时发送数据给两个worker
        wa_in <- d
        wb_in <- d

        // 然后等待两个worker的完成信号
        // 接收顺序不重要,因为两者都需要被接收
        <-wa_out
        <-wb_out

        final_chan <- d // 两个worker都完成后,将数据发送到最终通道
    }
    // 注意:在实际应用中,当account_chan关闭时,需要考虑如何优雅地关闭wa_in和wb_in,
    // 以便worker goroutine能够退出循环。此处为简化示例。
    close(wa_in)
    close(wb_in)
}

func main() {
    account_chan := make(chan int, 100)
    final_chan := make(chan int, 100)

    go account(account_chan, final_chan)

    // 发送一些数据
    account_chan <- 1
    account_chan <- 2
    account_chan <- 3

    // 关闭输入通道,通知account goroutine没有更多数据
    close(account_chan)

    // 等待并打印最终结果
    // 由于account goroutine在处理完所有数据并关闭其输入通道后,
    // 可能会立即关闭其输出通道,这里需要确保在接收前account goroutine有足够时间处理。
    // 更健壮的方式是使用sync.WaitGroup等待account goroutine完成。
    time.Sleep(time.Second) // 简单等待,确保account goroutine处理完成
    fmt.Println("最终结果:", <-final_chan)
    fmt.Println("最终结果:", <-final_chan)
    fmt.Println("最终结果:", <-final_chan)
}

原理分析:

通过将wa_in

解决方案二:使用sync.WaitGroup进行同步

在许多情况下,worker goroutine的输出值可能并不需要被协调器直接使用,或者它们处理的是共享数据结构而不是通过通道返回数据。此时,sync.WaitGroup是更简洁、更符合Go习惯的同步机制。sync.WaitGroup用于等待一组goroutine完成。

package main

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

func workerA_wg(work_in_chan <-chan int, wg *sync.WaitGroup) {
    defer wg.Done() // 确保在goroutine退出时调用Done()
    for d := range work_in_chan {
        fmt.Printf("WorkerA_wg 正在处理数据: %d\n", d)
        time.Sleep(time.Millisecond * 100) // 模拟工作耗时
        // 不需要通过通道返回数据,直接标记完成
    }
}

func workerB_wg(work_in_chan <-chan int, wg *sync.WaitGroup) {
    defer wg.Done() // 确保在goroutine退出时调用Done()
    for d := range work_in_chan {
        fmt.Printf("WorkerB_wg 正在处理数据: %d\n", d)
        time.Sleep(time.Millisecond * 150) // 模拟工作耗时
        // 不需要通过通道返回数据,直接标记完成
    }
}

func account_wg(account_chan <-chan int, final_chan chan<- int) {
    wa_in := make(chan int)
    wb_in := make(chan int)

    // 创建一个WaitGroup,用于同步两个worker
    var wg sync.WaitGroup

    // workerA_wg和workerB_wg各自需要调用wg.Done()一次
    wg.Add(2)
    go workerA_wg(wa_in, &wg)
    go workerB_wg(wb_in, &wg)

    for d := range account_chan {
        // 先同时发送数据给两个worker
        wa_in <- d
        wb_in <- d

        // 等待所有worker完成对当前数据项的处理
        // 注意:这里需要为每个数据项重置WaitGroup,或者采用不同的WaitGroup管理方式。
        // 考虑到worker是持续运行的,这种模式下WaitGroup不适合按数据项同步。
        // 正确的做法是,如果worker是短生命周期的,为每个数据项创建新的WaitGroup;
        // 如果worker是长生命周期的,如本例,则需要更精细的同步。

        // 对于本例(长生命周期worker),wg.Wait()应在worker goroutine完全退出后调用,
        // 而不是在每个数据项处理后。
        // 如果要实现每个数据项的同步,需要将wg.Add(2)和wg.Wait()放在循环内部,
        // 并且worker的Done()逻辑需要调整。

        // 重新思考:如果worker是长生命周期且需要对每个数据项同步,那么通道仍是更直接的方式。
        // WaitGroup更适合等待一组goroutine在生命周期结束时完成。
        // 但我们可以模拟一种每个数据项同步的WaitGroup用法,但这会增加复杂性。

        // 考虑到原问题中worker是单例goroutine,且对每个数据项都需同步,
        // 通道方案更自然。如果坚持使用WaitGroup,则每个数据项的同步需要更复杂的逻辑,
        // 例如,worker在处理完一个数据项后,通过另一个通道通知account,
        // account收到两个通知后,再调用WaitGroup的Add/Done。
        // 这种情况下,WaitGroup的优势就不明显了,甚至可能比通道更复杂。

        // 为了符合WaitGroup的典型用法,我们假设workerA_wg和workerB_wg是处理所有数据后才退出的。
        // 此时,wg.Wait()应该在for循环结束后调用,等待所有worker完全退出。
        // 但这与“每个数据项处理完后才发送到final_chan”的需求不符。

        // 因此,对于“每个数据项完成同步”的需求,通道方案更直接。
        // 如果确实要用WaitGroup,且worker是长生命周期,通常会结合其他机制。
        // 例如,worker在处理完一个数据项后,会发送一个信号到account,
        // account接收到所有worker的信号后,再继续。这本质上又回到了通道方案。

        // **结论:对于本问题场景,通道是更自然的同步机制。**
        // 如果worker的“输出”只是一个完成信号,且不需要传递任何值,
        // 那么可以将`work_out_chan <- d`替换为`work_out_chan <- struct{}{} `,
        // 接收方也相应调整。这与WaitGroup的语义更接近,但仍是通道。

        final_chan <- d // 假设这里是处理逻辑,然后发送完成信号
    }
    // 关闭输入通道,通知worker goroutine没有更多数据
    close(wa_in)
    close(wb_in)

    wg.Wait() // 等待所有worker goroutine完全退出
    close(final_chan) // 所有工作完成后关闭最终通道
}

func main_wg() {
    account_chan := make(chan int, 100)
    final_chan := make(chan int, 100)

    go account_wg(account_chan, final_chan)

    account_chan <- 1
    account_chan <- 2
    account_chan <- 3

    close(account_chan)

    for res := range final_chan {
        fmt.Println("最终结果 (WG):", res)
    }
}

// 由于WaitGroup在此特定场景下(每个数据项同步且worker是长生命周期)
// 实现起来不如通道直观和简洁,上面的`account_wg`函数没有完全实现按数据项同步。
// 如果要严格实现,则需要将`wg.Add(2)`和`wg.Wait()`放在循环内部,
// 并且worker每次处理完一个数据项后,需要调用`wg.Done()`,
// 并在处理下一个数据项前,需要重新`wg.Add(1)`,这会使得worker的循环结构复杂。
// 实际上,这种场景下,通道的“发送数据-接收完成信号”模式就是最简洁的。

对sync.WaitGroup的补充说明:

sync.WaitGroup更适用于等待一组goroutine在它们的整个生命周期结束后完成,而不是在每个中间步骤进行同步。例如,在启动多个并发任务,然后等待所有任务完成时,WaitGroup非常有效。

如果确实需要在每个数据项处理后使用WaitGroup进行同步,那么每个worker在处理完一个数据项后,需要调用wg.Done()。而account goroutine在发送完数据后,需要调用wg.Add(2)(或根据实际worker数量),然后调用wg.Wait()。但由于workerA和workerB是持续运行的goroutine,它们不能在每次处理完数据后就调用defer wg.Done()并退出。这需要更复杂的逻辑,例如,worker通过一个单独的通道通知account其已完成当前数据项,然后account在收到所有通知后,再使用WaitGroup的Add和Wait。但这又回到了通道的范畴。

因此,对于本教程中的“每个数据项处理完后才发送到final_chan”且worker是长生命周期的场景,基于通道的同步(解决方案一)是更自然、更简洁且更符合Go语言哲学的方式。

注意事项与最佳实践

  1. 通道的关闭: 在实际应用中,确保所有发送方在不再发送数据时关闭通道,以便接收方能够优雅地退出for range循环。这对于避免goroutine泄漏至关重要。在上述示例中,account goroutine在for range account_chan循环结束后,需要负责关闭wa_in和wb_in,以便workerA和workerB能够退出。
  2. 错误处理: worker goroutine在处理数据时可能会遇到错误。在生产环境中,需要设计机制将错误信息传递回account goroutine,以便进行统一的错误处理。这通常可以通过在work_out_chan中发送一个包含错误信息的结构体,或者使用select语句监听错误通道来实现。
  3. 资源管理: 确保所有goroutine都能正常退出,释放占用的资源。使用defer语句配合WaitGroup.Done()是确保goroutine退出的常见模式。
  4. 通道容量: make(chan int, 100)创建了一个带缓冲的通道。适当设置通道容量可以平衡生产者和消费者之间的速度差异,避免过早阻塞或内存溢出。
  5. 扇入/扇出模式: 本文展示的模式是经典的“扇出-扇入”(Fan-out/Fan-in)模式。account goroutine将数据“扇出”给多个worker,然后通过通道“扇入”完成信号,实现同步。

总结

Go语言通过其强大的goroutine和通道机制,使得实现复杂的并发模式变得相对简单和直观。对于需要并行处理独立任务并进行同步的场景,通过合理地安排通道的发送和接收操作,可以有效地实现并发执行,并确保所有必要的前置任务完成后再进行下一步。虽然sync.WaitGroup在某些同步场景下非常有用,但对于本教程中每个数据项的同步需求,通道提供了一种更直接、更易于理解和维护的解决方案。理解这两种同步机制的适用场景,是编写高效、健壮Go并发程序的关键。

热门AI工具

更多
墨刀AI
墨刀AI Hot

一款AI图像与设计工具,主要用于产品经理的专属智能体,适合需要提升相关任务效率的用户。

SkildArt
SkildArt Hot

SkildArt是一款AI文本写作工具,一站式 AI 视觉创作平台。

超级简历WonderCV

一款AI办公效率工具,主要用于免费求职简历模版下载制作,应届生职场人必备简历制作神器,适合需要提升相关任务效率的用户。

豆包大模型

豆包大模型是一款由字节跳动推出的企业级大语言模型服务平台。

小橡皮AI

一款AI工具,主要用于小橡皮一键去AI味,将机器味重的内容改得更有活人感,防止因为 AI 内容被平台打上 AI 标签,以及限流,适合需要提升相关任务效率的用户。

DeepSeek

DeepSeek是一款面向对话、写作、编程和推理场景的AI大模型工具。

WorkBuddy

一款AI办公效率工具,主要用于腾讯云推出的AI原生桌面智能体工作台,适合需要提升相关任务效率的用户。

咔片AIPPT

一款在线AI演示文稿制作工具,可根据主题和内容需求辅助生成PPT结构与页面,提高演示材料制作效率。

UpDream
UpDream Hot

一款AI视频创作工具,主要用于哔哩哔哩推出的自研AI视频创作工具,适合需要提升相关任务效率的用户。

相关专题

更多
golang结构体相关大全
golang结构体相关大全

本专题整合了golang结构体相关大全,想了解更多内容,请阅读专题下面的文章。

5354

2025.06.09

golang结构体方法
golang结构体方法

本专题整合了golang结构体相关内容,请阅读专题下面的文章了解更多。

5511

2025.07.04

string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

7359

2023.08.02

int占多少字节
int占多少字节

int占4个字节,意味着一个int变量可以存储范围在-2,147,483,648到2,147,483,647之间的整数值,在某些情况下也可能是2个字节或8个字节,int是一种常用的数据类型,用于表示整数,需要根据具体情况选择合适的数据类型,以确保程序的正确性和性能。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

3505

2024.08.29

c++怎么把double转成int
c++怎么把double转成int

本专题整合了 c++ double相关教程,阅读专题下面的文章了解更多详细内容。

4608

2025.08.29

C++中int的含义
C++中int的含义

本专题整合了C++中int相关内容,阅读专题下面的文章了解更多详细内容。

3085

2025.08.29

treenode的用法
treenode的用法

​在计算机编程领域,TreeNode是一种常见的数据结构,通常用于构建树形结构。在不同的编程语言中,TreeNode可能有不同的实现方式和用法,通常用于表示树的节点信息。更多关于treenode相关问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

2861

2023.12.01

C++ 高效算法与数据结构
C++ 高效算法与数据结构

本专题讲解 C++ 中常用算法与数据结构的实现与优化,涵盖排序算法(快速排序、归并排序)、查找算法、图算法、动态规划、贪心算法等,并结合实际案例分析如何选择最优算法来提高程序效率。通过深入理解数据结构(链表、树、堆、哈希表等),帮助开发者提升 在复杂应用中的算法设计与性能优化能力。

396

2025.12.22

Postman身份认证怎么配置
Postman身份认证怎么配置

本专题从实际API接口调试场景出发,讲解Postman身份认证的完整配置流程,包括常见认证协议介绍、授权参数设置、访问令牌填写、OAuth 2.0授权及认证失败排查,帮助用户正确完成接口授权与访问验证。

60

2026.11.06

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn