3 并发编程之并发模式与设计
在上一篇中,我们深入探讨了 Go 语言中的 Channel
,并理解了它在协程间通信中的重要作用。现在,我们将继续展开我们的并发编程知识,讨论 Go 语言中常见的并发模式与设计。这些模式是实现高效且易于维护的并发程序的基石。
并发模式概述
在 Go 语言的并发编程中,有数种常用的模式,每种模式都有其特定的用途和适用场景。以下是一些主要的并发模式:
- Worker Pool(工作池模式)
- Pipeline(管道模式)
- Publish/Subscribe(发布/订阅模式)
- Fan-Out, Fan-In(多输出,多输入模式)
接下来,我们将一一深入探讨这些模式。
1. Worker Pool(工作池模式)
工作池是一种通过限制并发工作的数量来控制资源消耗的模式。在这一模式中,我们会创建一个固定数量的工作协程,它们从一个共享队列中获取任务并执行。
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, jobs <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
time.Sleep(time.Second) // 模拟工作
}
}
func main() {
var wg sync.WaitGroup
jobs := make(chan int, 100) // 创建一个缓冲channel
// 启动 3 个工人
for w := 1; w <= 3; w++ {
wg.Add(1)
go worker(w, jobs, &wg)
}
// 发送 9 个工作到工作池
for j := 1; j <= 9; j++ {
jobs <- j
}
close(jobs) // 关闭渠道,表示没有更多的工作
wg.Wait() // 等待所有工人完成
}
在上面的例子中,我们创建了 3 个工作协程,每个协程从 jobs
通道中获取任务并处理。这种模式有助于控制并发的数量,从而避免过多的资源竞争。
2. Pipeline(管道模式)
管道模式可以理解为将多个处理步骤以流水线的形式连接起来。每个步骤都可以独立处理数据,数据通过 Channel
流动,从一个步骤传递到下一个步骤。
package main
import (
"fmt"
)
// 定义一个生产者
func produce(nums []int, jobs chan<- int) {
for _, n := range nums {
jobs <- n // 将数据发送到 jobs channel
}
close(jobs)
}
// 定义一个处理者
func square(jobs <-chan int, results chan<- int) {
for job := range jobs {
results <- job * job // 处理并返回结果
}
close(results)
}
func main() {
jobs := make(chan int, 5)
results := make(chan int, 5)
go produce([]int{1, 2, 3, 4, 5}, jobs)
go square(jobs, results)
// 提取并打印结果
for result := range results {
fmt.Println(result)
}
}
在这个例子中,我们定义了一个 produce
函数来生产数据,和一个 square
函数来处理数据。数据从生产者通过 jobs
通道流入处理者,最后处理结果通过 results
通道传递给主程序。这样使得每个处理步骤都可以独立运行,提升了模块化和可维护性。
3. Publish/Subscribe(发布/订阅模式)
发布/订阅模式允许发布者和订阅者之间的解耦。发布者将消息发布到一个通道,多个订阅者接收这些消息并做出响应。
package main
import (
"fmt"
"sync"
)
// 定义订阅者
func subscriber(id int, messages <-chan string, wg *sync.WaitGroup) {
defer wg.Done()
for msg := range messages {
fmt.Printf("Subscriber %d received: %s\n", id, msg)
}
}
func main() {
var wg sync.WaitGroup
messages := make(chan string)
// 启动 2 个订阅者
for i := 1; i <= 2; i++ {
wg.Add(1)
go subscriber(i, messages, &wg)
}
// 发布消息
messages <- "Hello World"
messages <- "Go Concurrency"
close(messages) // 关闭channel,表示没有更多消息
wg.Wait() // 等待所有订阅者完成
}
在上面的示例中,多个订阅者从同一个 messages
通道中接收消息,这样可以简单地扩展系统,使得消息的发送方与接收方之间不再直接耦合。
4. Fan-Out, Fan-In(多输出,多输入模式)
Fan-Out
是指多个协程并行处理任务,而 Fan-In
是将多个协程的结果集中到一个通道中。这种模式能有效利用 CPU,提高程序的并发性能。
示例
package main
import (
"fmt"
"sync"
)
// 定义计算平方的函数
func square(num int, results chan<- int) {
results <- num * num
}
func main() {
var wg sync.WaitGroup
numbers := []int{1, 2, 3, 4, 5}
results := make(chan int)
// Fan-Out
for _, num := range numbers {
wg.Add(1)
go func(n int) {
defer wg.Done()
square(n, results) // 将计算后的结果发送到 results channel
}(num)
}
// 启动一个 goroutine 来关闭 results channel
go func() {
wg.Wait()
close(results)
}()
// Fan-In
for result := range results {
fmt.Println(result)
}
}
在这个示例中,我们先启动多个协程来处理数字的平方(Fan-Out
),然后通过一个额外的 for
循环从 results
通道中接收所有结果(Fan-In
)。
总结
通过以上对并发模式的深入解析,我们可以看到,合理的并发模式设计能够有效提升 Go 程序的性能与可读性。在实际开发中,选择合适的并发模式可以帮助我们更好地管理复杂的并发操作,并提高系统的