目录
Day 5: 并发革命 - Goroutine与Channel实战入门
并发编程是现代软件开发中的一个核心技术,尤其在需要高效处理大量任务或请求时,能够显著提升系统的性能与响应速度。Go语言作为一门支持并发编程的现代编程语言,其内建的并发模型为开发者提供了简洁而强大的工具。本篇博客将介绍Go语言中并发编程的基础概念,重点讲解Goroutine、Channel、select语句以及如何在Go中实现并发任务处理,最后通过实战练习展示如何实现并发计算素数和多任务生产者消费者模式。
1. Goroutine原理与Go关键字
1.1 Goroutine概述
在Go语言中,Goroutine 是一种轻量级的线程实现。它由Go运行时调度管理,允许你在同一进程中同时执行多个任务。与传统的操作系统线程相比,Goroutine占用的内存和启动的开销要小得多。Go通过调度器来管理成千上万的Goroutine,并为每个Goroutine分配极小的栈空间,通常初始栈大小仅为2KB。栈空间会根据需要动态扩展和收缩。
1.2 启动Goroutine
要启动一个Goroutine,只需要使用Go关键字go
,后跟一个函数调用。例如:
package main
import (
"fmt"
"time"
)
func sayHello() {
fmt.Println("Hello from Goroutine!")
}
func main() {
// 启动一个新的Goroutine
go sayHello()
// 主Goroutine等待一段时间,让其他Goroutine有机会执行
time.Sleep(1 * time.Second)
}
在这个示例中,go sayHello()
启动了一个新的Goroutine,该Goroutine会异步执行sayHello
函数。time.Sleep
使主程序在执行完后等待一段时间,以确保Goroutine有时间执行。
2. Channel基础
Channel是Go语言中实现不同Goroutine之间通信的机制,类似于管道,允许一个Goroutine将数据发送到另一个Goroutine。
2.1 无缓冲通道
无缓冲通道是最简单的通道类型。它要求发送数据的Goroutine和接收数据的Goroutine同步,也就是说,发送操作会阻塞直到接收操作完成,反之亦然。
package main
import (
"fmt"
)
func main() {
// 创建一个无缓冲通道
ch := make(chan int)
// 启动一个Goroutine发送数据
go func() {
ch <- 42 // 发送数据到通道
}()
// 接收数据并打印
value := <-ch
fmt.Println("Received:", value)
}
在这个例子中,主Goroutine等待从ch
通道接收到数据,直到另一个Goroutine将数据发送到该通道。
2.2 有缓冲通道
有缓冲通道允许多个数据项被存储在通道中,发送操作不会立即阻塞,直到通道的缓冲区被填满。接收操作只有在缓冲区中有数据时才会阻塞。
package main
import (
"fmt"
)
func main() {
// 创建一个缓冲区大小为2的通道
ch := make(chan int, 2)
// 启动Goroutine发送数据
go func() {
ch <- 42
ch <- 84
fmt.Println("Data sent to channel")
}()
// 接收数据并打印
fmt.Println("Received:", <-ch)
fmt.Println("Received:", <-ch)
}
在这个例子中,发送操作不会被阻塞,因为通道的缓冲区可以容纳两个数据项。
3. select语句与超时控制
select
语句可以在多个Channel操作中选择一个准备好的操作来执行。它类似于switch
语句,但它用于Channel的多路复用。select
语句可以帮助我们处理多个并发操作,增强代码的可读性和灵活性。
3.1 select语句基本使用
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "Hello from ch1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "Hello from ch2"
}()
// 使用select语句等待两个通道的消息
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
}
}
在这个示例中,select
会等待ch1
或ch2
通道中的数据。因为ch1
会先收到数据,所以输出将是Hello from ch1
。
3.2 超时控制
我们可以通过在select
语句中使用time.After
来实现超时控制。如果某个操作在指定时间内没有完成,就可以通过超时机制来进行处理。
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
go func() {
time.Sleep(2 * time.Second)
ch <- "Task completed"
}()
select {
case msg := <-ch:
fmt.Println(msg)
case <-time.After(1 * time.Second):
fmt.Println("Timeout: Task took too long")
}
}
在这个例子中,time.After(1 * time.Second)
会在1秒后触发,导致select
语句超时并输出Timeout: Task took too long
。
4. 实战代码
4.1 并发计算素数
下面是一个使用Go并发计算素数的简单示例。我们将使用多个Goroutine来并行计算多个范围内的素数。
// 声明包名为 main,这是可执行程序的入口包
package main
import (
// 导入 fmt 包,用于格式化输入输出
"fmt"
// 导入 math 包,用于执行数学运算,这里主要使用了开方函数
"math"
// 导入 sync 包,用于实现并发控制,这里使用了 WaitGroup 来等待所有 goroutine 完成
"sync"
)
// isPrime 函数用于判断一个整数是否为素数
// 素数是指大于 1 且只能被 1 和自身整除的正整数
// 参数 n 是待判断的整数
// 返回值为布尔类型,如果 n 是素数则返回 true,否则返回 false
func isPrime(n int) bool {
if n <= 1 {
return false
}
for i := 2; i <= int(math.Sqrt(float64(n))); i++ {
if n%i == 0 {
return false
}
}
return true
}
// findPrimes 函数用于在指定的整数范围内查找素数
// 它会遍历从 start 到 end 的所有整数,并调用 isPrime 函数判断每个数是否为素数
// 如果是素数,则将其打印输出
// 参数 start 是查找范围的起始整数
// 参数 end 是查找范围的结束整数
// 参数 wg 是一个指向 sync.WaitGroup 的指针,用于通知主 goroutine 该函数的执行已完成
func findPrimes(start, end int, wg *sync.WaitGroup) {
defer wg.Done()
for i := start; i <= end; i++ {
if isPrime(i) {
fmt.Println(i)
}
}
}
// main 函数是程序的入口点
// 该函数启动两个 goroutine 分别在不同的整数范围内查找素数
// 并使用 sync.WaitGroup 等待这两个 goroutine 完成
func main() {
var wg sync.WaitGroup
wg.Add(2)
// 启动两个Goroutine,分别计算1-50和51-100之间的素数
go findPrimes(1, 50, &wg)
go findPrimes(51, 100, &wg)
// 等待两个Goroutine完成
wg.Wait()
}
这个例子中,我们使用sync.WaitGroup
来确保两个Goroutine都完成工作后再退出程序。每个Goroutine会处理一个范围内的素数计算任务,最终输出1到100之间的所有素数。
4.2 多任务生产者消费者
以下是一个经典的生产者-消费者问题的并发实现。我们使用Channel来在生产者和消费者之间传递数据。
// 声明包名为 main,这是可执行程序的入口包
package main
import (
// 导入 fmt 包,用于格式化输入输出
"fmt"
// 导入 time 包,用于处理时间相关的操作
"time"
)
// producer 函数作为生产者,负责向通道中发送数据
// 参数 ch 是一个整型通道,用于接收生产者生产的数据
func producer(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i
fmt.Println("Produced:", i)
time.Sleep(time.Second)
}
close(ch)
}
// consumer 函数作为消费者,负责从通道中接收数据
// 参数 ch 是一个整型通道,从中获取生产者生产的数据
func consumer(ch chan int) {
for item := range ch {
fmt.Println("Consumed:", item)
}
}
// main 函数是程序的入口点
// 该函数创建一个无缓冲的整型通道,启动生产者和消费者 goroutine,
// 并让主 goroutine 休眠一段时间,以确保生产者和消费者有足够的时间完成任务
func main() {
ch := make(chan int)
go producer(ch)
go consumer(ch)
// 等待消费者完成
time.Sleep(6 * time.Second)
}
在这个示例中,生产者不断生成数据并将其放入通道中,消费者从通道中取出数据并消费。使用time.Sleep
让消费者有足够时间完成工作。
结语
通过本篇博客,我们学习了Go语言中的并发编程基础,包括Goroutine、Channel、select语句的使用,并通过实际案例展示了如何应用这些知识来解决实际问题。并发编程是一个复杂且重要的领域,掌握它将极大提升你在Go语言开发中的能力。希望这篇博客对你有所帮助,欢迎评论交流!