0

0

使用Go Channel实现并发临界区的严格交替执行

花韻仙語

花韻仙語

发布时间:2025-10-28 14:34:01

|

415人浏览过

|

来源于php中文网

原创

使用Go Channel实现并发临界区的严格交替执行

本文探讨了如何在go语言中,利用双通道(dual channel)机制,确保多个并发协程(goroutines)中的临界区(critical sections)严格按照预设的顺序交替执行。通过为每个协程分配一个接收通道和一个发送通道,实现了一种令牌传递模式,有效解决了并发资源访问的同步问题,并展示了该模式的实现细节及其可扩展性。

在Go语言的并发编程中,我们经常会遇到需要多个协程访问共享资源(即临界区)的情况。通常,我们会使用互斥锁(sync.Mutex)来保证临界区在某一时刻只有一个协程在执行。然而,在某些特定场景下,我们可能需要更精细的控制,例如要求两个或多个临界区严格按照特定的顺序交替执行:CS1 -> CS2 -> CS1 -> CS2,依此类推。本文将介绍一种利用Go语言通道(channel)的强大特性,实现这种严格交替执行模式的有效方法。

问题描述

假设我们有两个并发运行的Go协程 f1 和 f2,它们各自包含一个临界区(CS1和CS2):

func f1() {
    // ... some code
    // critical section 1 (CS1)
    // ... critical section code
    // end critical section 1
    // ... more code
}

func f2() {
    // ... some code
    // critical section 2 (CS2)
    // ... critical section code
    // end critical section 2
    // ... more code
}

我们的目标是确保 CS1 只能在 CS2 执行之后执行,反之亦然,形成一个严格的 CS1, CS2, CS1, CS2... 的交替序列。

双通道机制:核心思想

为了实现这种严格的交替执行,我们可以采用一种“令牌传递”的模式,其中Go通道充当了传递令牌的媒介。核心思想如下:

  1. 每个参与交替的协程都拥有两个通道: 一个用于接收“执行令牌”(do 通道),另一个用于发送“完成令牌”(next 通道)。
  2. 接收令牌: 一个协程在进入其临界区之前,会尝试从其 do 通道接收一个令牌。如果通道为空,它将阻塞,直到有令牌可用。
  3. 发送令牌: 当一个协程完成其临界区的执行后,它会将一个令牌发送到下一个协程的 do 通道中。
  4. 初始化: 在程序启动时,需要向第一个要执行的协程的 do 通道中发送一个初始令牌,以启动整个交替序列。

通过这种机制,令牌在协程之间循环传递,确保了只有持有令牌的协程才能进入其临界区,从而实现了严格的交替执行。

Removal.AI
Removal.AI

AI移出图片背景工具

下载

实现细节与示例代码

下面是使用双通道机制实现两个协程临界区交替执行的完整Go语言示例代码。为了更好地演示,我们在临界区中加入了打印语句和短暂的延迟,并让协程持续循环执行。

package main

import (
    "fmt"
    "time"
)

// f1 协程负责执行临界区1
func f1(id int, do chan bool, next chan bool) {
    for { // 持续循环以演示交替执行
        <-do // 等待接收令牌,表示轮到f1执行
        fmt.Printf("Goroutine %d: Executing Critical Section 1\n", id)
        // --------------------------------------------------
        // critical section 1 (CS1)
        // 这里放置f1的临界区代码
        time.Sleep(100 * time.Millisecond) // 模拟临界区工作
        // end critical section 1
        // --------------------------------------------------
        next <- true // 完成CS1后,将令牌传递给下一个协程
    }
}

// f2 协程负责执行临界区2
func f2(id int, do chan bool, next chan bool) {
    for { // 持续循环以演示交替执行
        <-do // 等待接收令牌,表示轮到f2执行
        fmt.Printf("Goroutine %d: Executing Critical Section 2\n", id)
        // --------------------------------------------------
        // critical section 2 (CS2)
        // 这里放置f2的临界区代码
        time.Sleep(100 * time.Millisecond) // 模拟临界区工作
        // end critical section 2
        // --------------------------------------------------
        next <- true // 完成CS2后,将令牌传递给下一个协程
    }
}

func main() {
    // 创建两个缓冲大小为1的通道,用于传递令牌
    cf1 := make(chan bool, 1) // f1的接收通道
    cf2 := make(chan bool, 1) // f2的接收通道

    // 初始令牌:向cf1发送一个令牌,让f1首先开始执行
    cf1 <- true

    // 启动两个协程
    go f1(1, cf1, cf2) // f1接收cf1的令牌,完成后将令牌发送给cf2
    go f2(2, cf2, cf1) // f2接收cf2的令牌,完成后将令牌发送给cf1

    // 保持主协程运行,否则程序会立即退出,无法观察到子协程的交替执行
    select {} // 阻塞主协程,直到程序被手动终止
}

代码解析

  1. f1 和 f2 函数:

    • 这两个函数现在都接受两个 chan bool 类型的参数:do 和 next。
    • 临界区代码:一旦接收到令牌,协程就可以安全地执行其临界区代码。
    • next
    • for {} 循环:为了实现持续的交替执行,我们将整个逻辑包裹在一个无限循环中。
    • id 参数:仅用于在打印输出时区分不同的协程。
  2. main 函数:

    • cf1 := make(chan bool, 1) 和 cf2 := make(chan bool, 1):创建了两个带缓冲(大小为1)的布尔型通道。缓冲大小为1至关重要,它确保了令牌在通道中可以被存储一次,实现“一进一出”的传递。
    • cf1
    • go f1(1, cf1, cf2):启动 f1 协程,并将 cf1 作为其 do 通道(接收令牌),cf2 作为其 next 通道(发送令牌)。
    • go f2(2, cf2, cf1):启动 f2 协程,并将 cf2 作为其 do 通道,cf1 作为其 next 通道。这样就形成了一个环路:f1 完成后令牌给 f2,f2 完成后令牌给 f1。
    • select {}:这个语句会阻塞 main 协程,使其不会立即退出。如果没有它,main 函数会执行完毕并终止程序,导致 f1 和 f2 协程没有足够的时间运行。

注意事项与扩展性

  1. 通道缓冲: 使用缓冲大小为1的通道是实现令牌传递模式的关键。如果使用无缓冲通道,发送操作在没有接收者时会立即阻塞,这可能导致死锁或无法启动。缓冲为1允许令牌在发送者和接收者之间短暂地“停留”。
  2. 主协程阻塞: 在实际应用中,select {} 只是一个简单的阻塞 main 协程的方法。更健壮的生产级代码可能会使用 sync.WaitGroup 来等待所有协程完成,或者使用一个上下文(context.Context)来管理协程的生命周期。
  3. 错误处理: 示例代码未包含错误处理。在真实的临界区代码中,需要考虑可能发生的错误和异常情况,并确保令牌能够正确传递,即使临界区执行失败。
  4. 扩展到N个协程: 这种模式可以很容易地扩展到N个协程的交替执行。你只需要创建N个通道,并将它们连接成一个环形链表。例如,f1 将令牌传给 f2,f2 传给 f3,...,fN 传回给 f1。
  5. 公平性: 这种基于通道的令牌传递机制天然保证了严格的公平性,因为令牌是按顺序传递的。

总结

通过巧妙地利用Go语言的通道机制,我们可以实现对并发临界区访问的严格交替控制。双通道模式提供了一种清晰、高效且可扩展的解决方案,它将复杂的同步问题转化为简单的令牌传递逻辑。理解并掌握这种模式,将有助于开发者在Go并发编程中构建更加健壮和精确的同步机制

相关专题

更多
Go中Type关键字的用法
Go中Type关键字的用法

Go中Type关键字的用法有定义新的类型别名或者创建新的结构体类型。本专题为大家提供Go相关的文章、下载、课程内容,供大家免费下载体验。

233

2023.09.06

go怎么实现链表
go怎么实现链表

go通过定义一个节点结构体、定义一个链表结构体、定义一些方法来操作链表、实现一个方法来删除链表中的一个节点和实现一个方法来打印链表中的所有节点的方法实现链表。

444

2023.09.25

go语言编程软件有哪些
go语言编程软件有哪些

go语言编程软件有Go编译器、Go开发环境、Go包管理器、Go测试框架、Go文档生成器、Go代码质量工具和Go性能分析工具等。本专题为大家提供go语言相关的文章、下载、课程内容,供大家免费下载体验。

246

2023.10.13

0基础如何学go语言
0基础如何学go语言

0基础学习Go语言需要分阶段进行,从基础知识到实践项目,逐步深入。php中文网给大家带来了go语言相关的教程以及文章,欢迎大家前来学习。

693

2023.10.26

Go语言实现运算符重载有哪些方法
Go语言实现运算符重载有哪些方法

Go语言不支持运算符重载,但可以通过一些方法来模拟运算符重载的效果。使用函数重载来模拟运算符重载,可以为不同的类型定义不同的函数,以实现类似运算符重载的效果,通过函数重载,可以为不同的类型实现不同的操作。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

191

2024.02.23

Go语言中的运算符有哪些
Go语言中的运算符有哪些

Go语言中的运算符有:1、加法运算符;2、减法运算符;3、乘法运算符;4、除法运算符;5、取余运算符;6、比较运算符;7、位运算符;8、按位与运算符;9、按位或运算符;10、按位异或运算符等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

228

2024.02.23

go语言开发工具大全
go语言开发工具大全

本专题整合了go语言开发工具大全,想了解更多相关详细内容,请阅读下面的文章。

280

2025.06.11

go语言引用传递
go语言引用传递

本专题整合了go语言引用传递机制,想了解更多相关内容,请阅读专题下面的文章。

158

2025.06.26

Java 桌面应用开发(JavaFX 实战)
Java 桌面应用开发(JavaFX 实战)

本专题系统讲解 Java 在桌面应用开发领域的实战应用,重点围绕 JavaFX 框架,涵盖界面布局、控件使用、事件处理、FXML、样式美化(CSS)、多线程与UI响应优化,以及桌面应用的打包与发布。通过完整示例项目,帮助学习者掌握 使用 Java 构建现代化、跨平台桌面应用程序的核心能力。

36

2026.01.14

热门下载

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

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Go 教程
Go 教程

共32课时 | 3.7万人学习

Go语言实战之 GraphQL
Go语言实战之 GraphQL

共10课时 | 0.8万人学习

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

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号