ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Go 并发编程实战指南:goroutine、channel 与 select 全解析(《Build Web Application with Golang》第 2.7 节深度解读)

Go 并发编程实战指南:goroutine、channel 与 select 全解析(《Build Web Application with Golang》第 2.7 节深度解读) 文档教程【免费下载链接】build-web-application-with-golangA golang ebook intro how to build a web with golang项目地址https://gitcode.com/gh_mirrors/bu/build-web-application-with-golang点击查看免费下载导读本文以《Build Web Application with Golang》开源电子书第 2.7 节「Concorrência并发」为骨架系统讲解 Go 语言在语言层面提供的并发原语——goroutine、channel含缓冲与非缓冲、range/close、select 与超时控制以及runtime包中的 goroutine 管理函数。文中所有示例均可在仓库 pt-br/code/src/apps/ch.2.7 目录下找到可编译运行的完整源码读完你不仅能理解 Go 并发的核心机制还能直接动手运行这些示例为后续章节构建高并发 Web 服务打下基础。为什么说 Go 是21 世纪的 C 语言第 2.7 节开篇引用了业界对 Go 的经典评价Go 是 21 世纪的 C 语言。作者给出了两个理由Go 语言足够简单它只有 25 个关键字比英语字母表的 26 个字母还少完整关键字清单见第 2 章开篇 pt-br/02.0.md语言本身极易学习和阅读并发concorrência是当今世界的核心主题多核 CPU 早已普及、Web 服务天然需要同时处理海量请求而 Go 在语言层面原生支持并发不需要像其他语言那样依赖外部线程库或复杂的回调框架。本节的并发主题与本书第 8 章 Web 服务、第 13 章构建高可用系统等内容一脉相承在后续章节中你会看到 Web 应用中每个 HTTP 请求都运行在独立的 goroutine 中channel 与 select 则被用于请求处理、日志收集和优雅退出等场景。goroutine语言层面的轻量级并发goroutine 与系统线程的区别goroutineGo 协程与并发是 Go 设计核心的内建特性它和传统系统线程thread类似但工作方式截然不同数量与线程的映射十几条 goroutine 可能只对应 56 条底层线程——goroutine 由 Go 运行时runtime负责调度而非操作系统直接调度极低的内存开销一条 goroutine 通常只占用4~5 KB 的栈内存因此在一台普通计算机上轻松运行数千条 goroutine 毫无压力更轻量、更高效、更便捷相比系统线程goroutine 的创建、切换成本都低得多是构建高并发程序的理想单元。用go关键字创建 goroutine在 Go 中只需要在函数调用前加上go关键字即可创建一个新的 goroutinego hello(a, b, c)值得一提的是main()函数本身也是一条 goroutine。也就是说任何一个 Go 程序的入口天然运行在一条名为main的 goroutine 上。下面是最简单的 goroutine 示例对应仓库 pt-br/code/src/apps/ch.2.7/goroutine/main.gopackage main import ( fmt runtime ) func say(s string) { for i : 0; i 5; i { runtime.Gosched() fmt.Println(s) } } func main() { go say(world) // 创建一个新的 goroutine say(hello) // 当前 goroutine }程序输出每次运行顺序可能略有差异hello world hello world hello world hello world hello输出解析go say(world)启动了新的 goroutine主 goroutine 继续执行say(hello)。两个 goroutine 交替输出核心在于runtime.Gosched()——它的作用是让出 CPU让调度器去执行其他 goroutine之后某个时刻再回来继续执行。正是这个主动让出动作让两个 goroutine 得以交替运行。设计原则不要通过共享内存来通信上面的例子中两个 goroutine 共享了部分内存但作者建议遵循 Go 社区的经典设计箴言不要使用共享数据来通信要用通信来共享数据Dont communicate by sharing memory; share memory by communicating。这正是下一节 channel 存在的意义Go 鼓励通过 channel 在 goroutine 之间传递数据而不是依赖锁来保护共享变量。并行与并发GOMAXPROCS 的边界需要特别澄清一个概念上面的调度器只用一个线程来运行所有 goroutine因此它实现的只是并发concurrency而非并行parallelism。如果希望利用多核 CPU 实现真正的并行处理需要调用runtime.GOMAXPROCS(n)n为想要使用的 CPU 核心数如果n 1则设置不生效函数保持原有设置不变。说明原文档特别注明GOMAXPROCS在未来版本中可能被移除——从现代 Go 的演进看自 Go 1.5 起运行时默认即使用全部可用核心数开发者通常无需再手动调用此函数。在本书写作的年代手动设置是必要的。Channel通道goroutine 之间的通信管道channel 的本质与创建多个 goroutine 运行在同一个内存地址空间中访问共享内存时就必须保持同步。Go 提供了一种优雅的通信机制——channel。channel 可以类比为 Unix shell 中的双向管道既可以发送数据也可以接收数据。创建 channel 必须使用内建函数makeci : make(chan int) // 传递 int 的 channel cs : make(chan string) // 传递 string 的 channel cf : make(chan interface{}) // 传递任意类型interface{}的 channelchan是 channel 类型的关键字make(chan T)创建的元素类型为T的 channel。发送与接收-操作符channel 使用-操作符完成数据的发送与接收ch - v // 将值 v 发送到 channel ch v : -ch // 从 channel ch 接收数据并赋值给 v实战并发求和经典示例——两个 goroutine 各计算一半切片元素之和最后汇总对应仓库 pt-br/code/src/apps/ch.2.7/unbuffered_channel/main.gopackage main import fmt func sum(a []int, c chan int) { total : 0 for _, v : range a { total v } c - total // 将 total 发送到 channel c } func main() { a : []int{7, 2, 8, -9, 4, 0} c : make(chan int) go sum(a[:len(a)/2], c) // 计算前半部分 go sum(a[len(a)/2:], c) // 计算后半部分 x, y : -c, -c // 从 c 中接收两个结果 fmt.Println(x, y, xy) }程序输出17 -5 12前半部分 72817后半部分 -940-5合计 12。阻塞语义默认的同步机制默认情况下channel 的发送与接收都是阻塞的这让 goroutine 之间的同步变得极其简单接收阻塞一个 goroutine 从空 channel 接收数据value : -ch时会被阻塞直到有其他 goroutine 向该 channel 发送数据发送阻塞一个 goroutine 向 channel 发送数据ch - 5时会被阻塞直到有另一个 goroutine 接收了这份数据。这种发送-接收配对的同步语义让无缓冲 channel 天然成为 goroutine 之间的握手信号。缓冲 ChannelBuffered Channels上述无缓冲 channel 一次只能容纳一个元素。Go 同时提供缓冲 channel可以存储多个元素ch : make(chan type, n)其中n取值行为n 0无缓冲 channel阻塞n 0缓冲 channel可非阻塞地写入至多n个元素以ch : make(chan bool, 4)为例这个 channel 可以存储 4 个布尔元素。在无人接收的情况下我们可以连续发送 4 个元素而不阻塞但当尝试发送第 5 个元素且没有 goroutine 接收时发送方 goroutine 将被阻塞。动手验证对应仓库 pt-br/code/src/apps/ch.2.7/buffered_channel/main.gopackage main import fmt func main() { c : make(chan int, 2) // 把 2 改成 1 会报运行时错误改成 3 则没问题 c - 1 c - 2 fmt.Println(-c) fmt.Println(-c) }缓冲容量为2连续发送1、2都不会阻塞输出1、2缓冲容量改为1向c发送第二个元素时无人接收触发fatal error: all goroutines are asleep - deadlock!运行时错误缓冲容量改为3仍然正常工作。Range 与 Close迭代与关闭 channel用 range 读取 channelchannel 可以像 slice、map 一样使用range进行迭代读取。下面的斐波那契数列示例对应仓库 pt-br/code/src/apps/ch.2.7/range_and_close_channel/main.go展示了 producer-consumer 模式package main import ( fmt ) func fibonacci(n int, c chan int) { x, y : 1, 1 for i : 0; i n; i { c - x x, y y, xy } close(c) } func main() { c : make(chan int, 10) go fibonacci(cap(c), c) for i : range c { fmt.Println(i) } }输出为 10 个斐波那契数1 1 2 3 5 8 13 21 34 55关键机制for i : range c会持续从 channel 读取数据直到 channel 被关闭才停止。本例中生产者fibonacci发送完cap(c)即 10个数据后调用close(c)关闭 channel消费端的range循环随之自然结束。判断 channel 是否已关闭向已关闭的 channel 发送或接收数据都是不允许的。可以使用带双返回值的接收语法测试 channel 是否已关闭v, ok : -ch如果ok返回false说明该 channel 已无数据且已被关闭。两条重要的实践经验作者给出了两条必须牢记的 channel 使用经验永远在生产者一侧关闭 channel而不是消费者一侧——否则极易触发向已关闭 channel 发送数据导致的 panicchannel 不像文件无需频繁关闭——除非确定 channel 已经完全无用或需要退出range循环否则不要主动关闭。Select同时监听多个 channel多 channel 场景的选择器前面的例子都只使用一个 channel那么如何同时处理多个 channel 呢Go 提供了select关键字来监听多个 channel。select默认是阻塞的只有当一个 channel 有数据可发送或可接收时它才继续执行如果多个 channel同时就绪select会随机选择其中一个执行。下面是用select实现斐波那契数列生成与退出控制的示例对应仓库 pt-br/code/src/apps/ch.2.7/select_channel/main.gopackage main import fmt func fibonacci(c, quit chan int) { x, y : 1, 1 for { select { case c - x: x, y y, xy case -quit: fmt.Println(quit) return } } } func main() { c : make(chan int) quit : make(chan int) go func() { for i : 0; i 10; i { fmt.Println(-c) } quit - 0 }() fibonacci(c, quit) }运行流程匿名 goroutine 依次从c读取 10 个斐波那契数读取完毕后向quit发送0主 goroutine 中的fibonacci通过select同时监听c发送与quit接收只要c有接收方在等待就发送下一个斐波那契数一旦quit收到数据打印quit并return退出循环主函数随之结束。select 的 default 分支与switch类似select也支持default分支当所有 channel 都未就绪时会执行default分支而不再继续等待 channelselect { case i : -c: // 使用 i default: // 当 c 被阻塞时执行这里的代码 }default分支让select具备了非阻塞检查能力常用于轮询 channel 状态而不想被阻塞的场景。Timeout用 select 实现超时控制某些场景下goroutine 可能一直阻塞例如等待一个永远不会到来的数据进而拖垮整个程序。最简单的解决办法就是在select中设置超时——利用time.After在指定时间后返回一个 channel 值对应仓库 pt-br/code/src/apps/ch.2.7/timeout/main.gopackage main import ( fmt time ) func main() { c : make(chan int) o : make(chan bool) go func() { for { select { case v : -c: fmt.Println(v) case -time.After(5 * time.Second): fmt.Println(timeout) o - true break } } }() -o }运行逻辑子 goroutine 的select同时监听数据 channelc与定时器 channel。如果 5 秒内c一直没有数据到来time.After(5 * time.Second)分支被触发打印timeout并向o发送true主 goroutine 通过-o等待该信号后退出程序。这样整个程序最多等待 5 秒不会无限期挂起。注示例中的break只退出select而外层for循环会继续执行因此实际的终止信号由ochannel 承担。这是本书写作时代的一种教学式写法在现代 Go 中更常见的做法是在超时分支里直接return或使用context包实现可取消的超时控制。runtime 包goroutine 管理的五个核心函数runtime包提供若干处理 goroutine 的函数整理如下函数作用runtime.Goexit()退出当前 goroutine但已 defer 的函数仍会照常执行runtime.Gosched()让出 CPU让调度器执行其他 goroutine之后某个时刻再恢复执行runtime.NumCPU() int返回 CPU 核心数量runtime.NumGoroutine() int返回当前 goroutine 的数量runtime.GOMAXPROCS(n int) int设置希望使用的 CPU 核心数其中Gosched与GOMAXPROCS在前文 goroutine 示例中已实际使用Goexit与NumGoroutine常用于生产环境中的 goroutine 泄漏排查——例如通过定时打印runtime.NumGoroutine()可以直观观察程序是否随请求量增长不断积累未回收的 goroutine。动手运行本章示例仓库在 pt-br/code/src/apps/ch.2.7 目录下按主题整理了 6 个可编译运行的独立示例pt-br/code/src/apps/ch.2.7/ ├── goroutine/ # go 关键字创建 goroutine Gosched 让出 CPU ├── unbuffered_channel/ # 无缓冲 channel 的同步发送/接收 ├── buffered_channel/ # 缓冲 channel修改容量观察死锁 ├── range_and_close_channel/ # range 迭代 close 关闭 ├── select_channel/ # select 监听多 channel 退出信号 └── timeout/ # time.After 实现超时控制运行步骤按照 pt-br/code/readme.md 的说明将环境变量GOPATH指向pt-br/code目录以避免工作区workspace路径问题进入任一示例目录执行go run main.go建议动手修改在goroutine示例中去掉runtime.Gosched()观察输出顺序的变化在buffered_channel示例中把缓冲容量2改为1观察fatal error: all goroutines are asleep - deadlock!在timeout示例中把5 * time.Second改小观察超时分支更快触发。小结第 2.7 节完整覆盖了 Go 并发编程的六大核心知识点也是后续章节Web 请求处理、数据库连接池、Socket 编程等反复使用的基础设施goroutinego关键字创建的轻量级并发单元栈内存仅 4~5 KB由 Go 运行时调度channelgoroutine 间通信的首选机制遵循用通信共享数据的设计原则阻塞语义无缓冲 channel 的发送与接收天然同步阻塞缓冲 channelmake(chan T, n)可容纳n个元素容量耗尽才阻塞range/closerange持续读取直到 channel 关闭生产者负责关闭接收端可用v, ok : -ch检测关闭状态select 与超时select随机处理多个就绪 channeldefault提供非阻塞路径time.After实现超时保护runtime 管理函数Goexit、Gosched、NumCPU、NumGoroutine、GOMAXPROCS各司其职。掌握这些原语你就掌握了 Go 并发编程的核心模型接下来可以进入 pt-br/02.8.md第 2 章小结或继续阅读本仓库其他章节见证并发机制如何在真实 Web 应用中落地。赞分享文档教程【免费下载链接】build-web-application-with-golangA golang ebook intro how to build a web with golang项目地址https://gitcode.com/gh_mirrors/bu/build-web-application-with-golang点击查看免费下载相关推荐Go 并发编程实战goroutine、channel 与 select 全面解析Build Web Application with Golang · 第 2.7 章Go 并发编程实战goroutine、channel 与 select 全面解析Build Web Application with Golang · 第文档教程Go 并发编程实战goroutine 与 channel 全面解析build-web-application-with-golang 第 2.7 章Go 并发编程实战goroutine 与 channel 全面解析build web application with golang 第 2.7 章 本文文档教程深入 Go 并发编程goroutine、channel 与 select 机制详解源自 build-web-application-with-golang 第 2.7 章深入 Go 并发编程goroutine、channel 与 select 机制详解源自 build web application with golang文档教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表