通过三个例子,学习 Go 语言并发编程的利器 - gorouti
Go 语言(也称为 Golang)是一门由 Google 开发的编程语言,以其简洁、高效和对并发编程的内置支持而在编程领域享有盛名。
在 Go 语言中,goroutine
是一项强大的并发特性,用于轻量级线程的创建和管理。
本文将向没有接触过 Go 语言的朋友,介绍 goroutine 的概念、使用场合,并提供具体的例子以演示其用法。
1. Goroutine 的概念:
Goroutine 是 Go 语言中用于并发执行的轻量级线程。与传统的线程相比,goroutine 的创建和销毁成本很低,可以在同一个程序中轻松创建多个 goroutine 而不会导致性能下降。Goroutines 之间通过通道(channel
)进行通信,实现了并发编程的简洁和安全。
2. Goroutine的使用场合:
俗话说,红粉赠佳人,宝剑送烈士。既然 Goroutine 有如此突出的能力,并发编程领域就是它大显身手的舞台。
在下列这些应用场景里,我们都能看到 Goroutine 施展拳脚的身影。
- 并发执行任务: Goroutine 可用于同时执行多个任务,提高程序的性能。
- 非阻塞 I/O 操作: 在进行 I/O 操作时,可以使用 goroutine 确保其他任务继续执行,而不是同步等待 I/O 完成。
- 事件驱动编程: Goroutine 可用于处理事件,如监听 HTTP 请求、处理用户输入等。
- 并发算法: 实现一些需要并行计算的算法,通过 goroutine 可以更轻松地管理并发执行的部分。
- 定时任务: 使用 goroutine 和定时器可以实现定时执行的任务。
3. Goroutine 的具体例子:
我们通过一些具体的例子来说明。
例子1:简单的并发任务执行
作为热身,我们通过一些简单的 Hello World 级别的例子来熟悉 Goroutine 的用法。
源代码如下:
package main
import (
"fmt"
"sync"
)
func printNumbers(wg *sync.WaitGroup) {
defer wg.Done()
for i := 1; i <= 5; i++ {
fmt.Printf("%d ", i)
}
}
func printLetters(wg *sync.WaitGroup) {
defer wg.Done()
for char := 'a'; char <= 'e'; char++ {
fmt.Printf("%c ", char)
}
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
go printNumbers(&wg)
go printLetters(&wg)
wg.Wait()
}
将上述源代码另存为一个 .go
文件里,比如取名 1.go
, 然后执行命令行 go run 1.go
, 看到下面的输出:
在这个例子中,我们通过关键字 go
,创建了两个 goroutine,一个用于打印数字,另一个用于打印字母。sync.WaitGroup
函数调用,用于等待这两个 goroutine 执行完毕。这是 go 语言并发编程里,最常用的同步机制之一。
例子2:并发下载多个网页
源代码如下:
package main
import (
"fmt"
"io/ioutil"
"net/http"
"sync"
)
func fetch(url string, wg *sync.WaitGroup) {
defer wg.Done()
response, err := http.Get(url)
if err != nil {
fmt.Printf("Error fetching %s: %v\n", url, err)
return
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
fmt.Printf("Error reading response body from %s: %v\n", url, err)
return
}
fmt.Printf("Length of %s: %d\n", url, len(body))
}
func main() {
var wg sync.WaitGroup
urls := []string{"https://www.baidu.com", "https://cloud.tencent.com/", "https://www.qq.com/"}
for _, url := range urls {
wg.Add(1)
go fetch(url, &wg)
}
wg.Wait()
}
执行上面的源代码后,在控制台看到下列输出,打印了三个网站(百度,qq 和腾讯云社区)首页 html 文件的长度(length)。
这个例子展示了如何使用 goroutine 并发地
下载多个网页。每个网页都在单独的goroutine中进行下载,从而减少了完成下载任务所需花费的时间。
从源代码可以看出,如果是用 Java 这门传统的编程语言完成这个需求的编码工作,我们需要使用 Java 提供的 Thread 类和一些繁琐的同步机制代码。而使用 Go 语言,通过 go fetch(url, &wg)
这一行短小精悍的语言,就可以轻松完成 goroutine 的启动和根据 url 读取 HTML 页面数据的操作,代码大大简化。
例子3:通过通道实现生产者-消费者模型
生产者-消费者是操作系统课程里必学的概念之一。我们使用 func 关键字,分别定义了生产者和消费者两个函数。生产者函数,使用 time.Sleep 方法,每隔 0.5 秒钟生产一个正整数序列 1,2,3,4,5. 消费者函数,相应的每隔 0.5 秒钟消费一个正整数。
package main
import (
"fmt"
"sync"
"time"
)
func producer(ch chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for i := 1; i <= 5; i++ {
ch <- i
fmt.Printf("Produced: %d\n", i)
time.Sleep(time.Millisecond * 500) // 模拟生产过程的延迟
}
close(ch)
}
func consumer(ch <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for num := range ch {
fmt.Printf("Consumed: %d\n", num)
time.Sleep(time.Millisecond * 200) // 模拟消费过程的延迟
}
}
func main() {
var wg sync.WaitGroup
ch := make(chan int, 3) // 创建带缓冲通道,容量为3
wg.Add(2)
go producer(ch, &wg)
go consumer(ch, &wg)
wg.Wait()
}
执行上面的 go 源代码,输出如下,生产一个正整数序列,然后消费,以此类推。
这个例子演示了如何使用 goroutine 和通道实现生产者-消费者模型。生产者将数据发送到通道,而消费者从通道中接收并处理数据,通过 goroutine 实现了并发的生产和消费过程。
我们使用了带缓冲的通道,容量为3。带缓冲的通道允许在通道未被完全读取的情况下进行写入,这在生产者和消费者速度不一致时非常有用。
在生产者函数 producer
里,在ch
是一个只写的整数通道,wg
是一个 sync.WaitGroup
类型的指针,用于等待所有的goroutine完成。在这个函数中,生产者通过 ch <- i
向通道发送整数 i
,表示生产了一个产品。然后,通过 fmt.Printf
打印出生产的产品的信息,并通过 time.Sleep
模拟生产过程的延迟。最后,通过 close(ch)
关闭通道,表示生产者不再向通道发送数据。
再看消费者函数 consumer
. ch
是一个只读的整数通道,wg
是一个 sync.WaitGroup
类型的指针。在这个函数中,消费者通过 num := <-ch
从通道接收整数,表示消费了一个产品。然后,通过 fmt.Printf
打印出消费的产品的信息,并通过 time.Sleep
模拟消费过程的延迟。这个循环会一直执行,直到通道被关闭,此时 range ch
将会退出。
总结
通过本文介绍的这三个例子,我们可以看到 goroutine 的强大之处。通过goroutine 和 channel 的组合,以及 sync.WaitGroup
的使用,Go 语言提供了强大而简洁的并发编程机制。相信之前有过 Java 编程经验的开发者,对于 Java 和 Go 这两门编程语言,在实现同一需求所需的不同代码量的差异,更是深有体会。