Go语言中的并发编程:管道、协程与通道的高级应用
1. h1s.go代码解析
在之前的学习中,我们了解到h1s.go使用通道和协程来处理Unix信号。具体来说,作为协程执行的匿名函数通过一个无限
for
循环从
sigs
通道读取信号。每当有我们感兴趣的信号时,协程就会从
sigs
通道读取并处理该信号。
2. 管道的概念与应用
Go程序很少只使用单个通道,一种常见的使用多个通道的技术是管道(pipeline)。管道是一种连接协程的方法,通过通道使一个协程的输出成为另一个协程的输入。使用管道有以下好处:
- 程序中存在持续的数据流,因为不需要等待所有任务完成才开始执行协程和通道。
- 使用更少的变量,从而节省内存空间,因为不需要保存所有数据。
- 简化程序设计,提高可维护性。
下面是一个处理整数管道的
pipelines.go
程序示例,它由五个部分组成:
package main
import (
"fmt"
"os"
"path/filepath"
"strconv"
)
func genNumbers(min, max int64, out chan<- int64) {
var i int64
for i = min; i <= max; i++ {
out <- i
}
close(out)
}
func findSquares(out chan<- int64, in <-chan int64) {
for x := range in {
out <- x * x
}
close(out)
}
func calcSum(in <-chan int64) {
var sum int64
sum = 0
for x2 := range in {
sum = sum + x2
}
fmt.Printf("The sum of squares is %d\n", sum)
}
func main() {
if len(os.Args) != 3 {
fmt.Printf("usage: %s n1 n2\n", filepath.Base(os.Args[0]))
os.Exit(1)
}
n1, _ := strconv.ParseInt(os.Args[1], 10, 64)
n2, _ := strconv.ParseInt(os.Args[2], 10, 64)
if n1 > n2 {
fmt.Printf("%d should be smaller than %d\n", n1, n2)
os.Exit(10)
}
naturals := make(chan int64)
squares := make(chan int64)
go genNumbers(n1, n2, naturals)
go findSquares(squares, naturals)
calcSum(squares)
}
该程序的执行流程如下:
1.
main
函数读取命令行参数,创建必要的通道变量
naturals
和
squares
。
2. 启动
genNumbers
协程生成指定范围内的整数,并将其发送到
naturals
通道。
3. 启动
findSquares
协程从
naturals
通道读取整数,计算其平方,并将结果发送到
squares
通道。
4. 调用
calcSum
函数从
squares
通道读取平方数并计算总和。
运行
pipelines.go
的示例输出如下:
$ go run pipelines.go
usage: pipelines n1 n2
exit status 1
$ go run pipelines.go 3 2
3 should be smaller than 2
exit status 10
$ go run pipelines.go 3 20
The sum of squares is 2865
$ go run pipelines.go 1 20
The sum of squares is 2870
$ go run pipelines.go 20 20
The sum of squares is 400
3. 改进版的wc.go:dWC.go
为了使用协程处理文件输入,我们开发了
dWC.go
工具,它由四个部分组成:
package main
import (
"bufio"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"sync"
)
func count(filename string) {
var err error
var numberOfLines int = 0
var numberOfCharacters int = 0
var numberOfWords int = 0
f, err := os.Open(filename)
if err != nil {
fmt.Printf("%s\n", err)
return
}
defer f.Close()
r := bufio.NewReader(f)
for {
line, err := r.ReadString('\n')
if err == io.EOF {
break
} else if err != nil {
fmt.Printf("error reading file %s\n", err)
}
numberOfLines++
r := regexp.MustCompile("[^\\s]+")
for range r.FindAllString(line, -1) {
numberOfWords++
}
numberOfCharacters += len(line)
}
fmt.Printf("\t%d\t", numberOfLines)
fmt.Printf("%d\t", numberOfWords)
fmt.Printf("%d\t", numberOfCharacters)
fmt.Printf("%s\n", filename)
}
func main() {
if len(os.Args) == 1 {
fmt.Printf("usage: %s <file1> [<file2> [... <fileN]]\n",
filepath.Base(os.Args[0]))
os.Exit(1)
}
var waitGroup sync.WaitGroup
for _, filename := range os.Args[1:] {
waitGroup.Add(1)
go func(filename string) {
count(filename)
defer waitGroup.Done()
}(filename)
}
waitGroup.Wait()
}
dWC.go
的工作流程如下:
1.
main
函数检查命令行参数,如果参数不足则输出使用说明并退出。
2. 为每个输入文件启动一个协程调用
count
函数进行处理。
3.
count
函数打开文件,逐行读取并统计行数、单词数和字符数,最后输出统计结果。
运行
dWC.go
的示例输出如下:
$ go run dWC.go /tmp/swtag.log /tmp/swtag.log doesnotExist
open doesnotExist: no such file or directory
48 275 3571 /tmp/swtag.log
48 275 3571 /tmp/swtag.log
然而,
dWC.go
存在一些问题,例如协程之间没有通信,输出可能会混乱,并且无法计算总数。
4. 支持计算总数的dWCtotal.go
为了解决
dWC.go
无法计算总数的问题,我们开发了
dWCtotal.go
,它使用通道和管道来实现协程之间的通信。该程序由五个部分组成:
package main
import (
"bufio"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
)
type File struct {
Filename string
Lines int
Words int
Characters int
Error error
}
func process(files []string, out chan<- File) {
for _, filename := range files {
var fileToProcess File
fileToProcess.Filename = filename
fileToProcess.Lines = 0
fileToProcess.Words = 0
fileToProcess.Characters = 0
out <- fileToProcess
}
close(out)
}
func count(in <-chan File, out chan<- File) {
for y := range in {
filename := y.Filename
f, err := os.Open(filename)
if err != nil {
y.Error = err
out <- y
continue
}
defer f.Close()
r := bufio.NewReader(f)
for {
line, err := r.ReadString('\n')
if err == io.EOF {
break
} else if err != nil {
fmt.Printf("error reading file %s", err)
y.Error = err
out <- y
continue
}
y.Lines = y.Lines + 1
r := regexp.MustCompile("[^\\s]+")
for range r.FindAllString(line, -1) {
y.Words = y.Words + 1
}
y.Characters = y.Characters + len(line)
}
out <- y
}
close(out)
}
func calculate(in <-chan File) {
var totalWords int = 0
var totalLines int = 0
var totalChars int = 0
for x := range in {
totalWords = totalWords + x.Words
totalLines = totalLines + x.Lines
totalChars = totalChars + x.Characters
if x.Error == nil {
fmt.Printf("\t%d\t", x.Lines)
fmt.Printf("%d\t", x.Words)
fmt.Printf("%d\t", x.Characters)
fmt.Printf("%s\n", x.Filename)
}
}
fmt.Printf("\t%d\t", totalLines)
fmt.Printf("%d\t", totalWords)
fmt.Printf("%d\ttotal\n", totalChars)
}
func main() {
if len(os.Args) == 1 {
fmt.Printf("usage: %s <file1> [<file2> [... <fileN]]\n",
filepath.Base(os.Args[0]))
os.Exit(1)
}
files := make(chan File)
values := make(chan File)
go process(os.Args[1:], files)
go count(files, values)
calculate(values)
}
dWCtotal.go
的执行流程如下:
1.
main
函数检查命令行参数,创建
files
和
values
通道。
2. 启动
process
协程将文件名封装成
File
结构体发送到
files
通道。
3. 启动
count
协程从
files
通道读取
File
结构体,打开文件并统计行数、单词数和字符数,然后将结果发送到
values
通道。
4.
calculate
函数从
values
通道读取统计结果,计算总数并输出。
运行
dWCtotal.go
的示例输出如下:
$ go run dWCtotal.go /tmp/swtag.log
48 275 3571 /tmp/swtag.log
48 275 3571 total
$ go run dWCtotal.go /tmp/swtag.log /tmp/swtag.log doesNotExist
48 275 3571 /tmp/swtag.log
48 275 3571 /tmp/swtag.log
96 550 7142 total
5. 性能基准测试
为了比较
wc.go
、
wc(1)
、
dWC.go
和
dWCtotal.go
的性能,我们使用
time(1)
工具对处理相对较大文件的命令进行了测试。测试结果表明,原始的
wc(1)
工具是最快的,
dWC.go
比
dWCtotal.go
和
wc.go
快,除了
dWC.go
之外,另外两个Go版本的性能相同。
6. 练习题
- 创建一个管道,读取文本文件,查找给定单词的出现次数,并计算该单词在所有文件中的总出现次数。
-
尝试优化
dWCtotal.go的性能。 - 创建一个简单的Go程序,使用通道实现乒乓游戏,通过命令行参数定义乒乓的总次数。
下面是一个简单的mermaid流程图,展示
pipelines.go
的执行流程:
graph LR
A[main函数] --> B[读取命令行参数]
B --> C[创建通道naturals和squares]
C --> D[启动genNumbers协程]
C --> E[启动findSquares协程]
D --> F[生成整数发送到naturals通道]
F --> E
E --> G[计算平方数发送到squares通道]
G --> H[calcSum函数计算总和]
通过以上内容,我们学习了Go语言中管道、协程和通道的使用,以及如何利用它们来实现并发编程和处理文件输入。同时,我们还通过性能测试了解了不同实现方式的性能差异。在后续的学习中,我们将进一步探讨Go语言中与协程和通道相关的高级特性。
Go语言中的并发编程:管道、协程与通道的高级应用
7. 协程高级特性概述
在Go语言中,协程是其最重要的特性之一,而通道则极大地提升了协程的能力。接下来,我们将深入探讨一些高级特性,包括不同类型的通道、共享内存与互斥锁的使用,以及如何处理程序超时等问题。
8. Go调度器
之前我们提到,内核调度器负责程序线程的执行顺序,但这并不完全准确。实际上,Go运行时拥有自己的调度器,采用m:n调度技术,即使用n个操作系统线程来执行m个协程,通过多路复用的方式实现。由于Go调度器只处理单个程序的协程,其操作成本更低、速度更快。
9. sync包的应用
在处理并发编程时,
sync
包是非常有用的。在这部分内容中,我们将重点学习
sync.Mutex
和
sync.RWMutex
类型及其支持的函数。这两种类型主要用于保护共享数据,避免多个协程同时访问和修改同一数据而导致的数据不一致问题。
10. select关键字的使用
select
语句在Go语言中类似于通道的
switch
语句,它允许一个协程同时等待多个通信操作。使用
select
关键字的主要优势在于,同一个函数可以通过单个
select
语句处理多个通道,并且可以实现非阻塞操作。
下面是一个使用
select
关键字的
useSelect.go
程序示例,它由五个部分组成:
package main
import (
"fmt"
"math/rand"
"os"
"path/filepath"
"strconv"
"time"
)
func createNumber(max int, randomNumberChannel chan<- int, finishedChannel chan bool) {
for {
select {
case randomNumberChannel <- rand.Intn(max):
case x := <-finishedChannel:
if x {
close(finishedChannel)
close(randomNumberChannel)
return
}
}
}
}
func main() {
rand.Seed(time.Now().Unix())
randomNumberChannel := make(chan int)
finishedChannel := make(chan bool)
if len(os.Args) != 3 {
fmt.Printf("usage: %s count max\n", filepath.Base(os.Args[0]))
os.Exit(1)
}
n1, _ := strconv.ParseInt(os.Args[1], 10, 64)
count := int(n1)
n2, _ := strconv.ParseInt(os.Args[2], 10, 64)
max := int(n2)
fmt.Printf("Going to create %d random numbers.\n", count)
go createNumber(max, randomNumberChannel, finishedChannel)
for i := 0; i < count; i++ {
fmt.Printf("%d ", <-randomNumberChannel)
}
finishedChannel <- false
fmt.Println()
_, ok := <-randomNumberChannel
if ok {
fmt.Println("Channel is open!")
} else {
fmt.Println("Channel is closed!")
}
finishedChannel <- true
_, ok = <-randomNumberChannel
if ok {
fmt.Println("Channel is open!")
} else {
fmt.Println("Channel is closed!")
}
}
该程序的执行步骤如下:
1.
main
函数读取命令行参数,创建
randomNumberChannel
和
finishedChannel
通道。
2. 启动
createNumber
协程,该协程通过
select
语句监听两个通道:
- 当
randomNumberChannel
可写时,生成一个随机数并发送到该通道。
- 当
finishedChannel
可读且接收到
true
值时,关闭两个通道并退出协程。
3.
main
函数中的
for
循环从
randomNumberChannel
读取指定数量的随机数并输出。
4. 向
finishedChannel
发送
false
值,检查
randomNumberChannel
是否仍然打开。
5. 向
finishedChannel
发送
true
值,再次检查
randomNumberChannel
是否关闭。
运行
useSelect.go
的示例输出如下:
$ go run useSelect.go 2 100
Going to create 2 random numbers.
19 74
Channel is open!
Channel is closed!
11. 总结
通过本文的学习,我们深入了解了Go语言中管道、协程和通道的高级应用。我们学习了如何使用管道连接协程,实现数据的流动和处理;掌握了不同类型通道的使用,如信号通道、缓冲通道等;了解了Go调度器的工作原理,以及如何使用
sync
包中的互斥锁保护共享数据;还学会了使用
select
关键字处理多个通道的通信操作。
在实际开发中,合理运用这些高级特性可以提高程序的并发性能和可维护性。例如,在处理大量数据时,使用管道和协程可以实现高效的数据处理;在多协程访问共享数据时,使用互斥锁可以保证数据的一致性。
同时,我们也通过练习题进一步巩固了所学知识。希望大家能够通过不断实践,熟练掌握这些高级特性,编写出更加高效、稳定的Go程序。
下面是一个简单的mermaid流程图,展示
useSelect.go
的执行流程:
graph LR
A[main函数] --> B[读取命令行参数]
B --> C[创建通道randomNumberChannel和finishedChannel]
C --> D[启动createNumber协程]
D --> E{select语句}
E --> F[生成随机数发送到randomNumberChannel]
E --> G{接收到true值?}
G -->|是| H[关闭通道并退出协程]
A --> I[读取随机数并输出]
A --> J[发送false值到finishedChannel]
A --> K[检查randomNumberChannel是否打开]
A --> L[发送true值到finishedChannel]
A --> M[检查randomNumberChannel是否关闭]
总之,Go语言的并发编程特性为我们提供了强大的工具,通过合理运用这些特性,我们可以充分发挥多核处理器的性能,实现高效的并发程序。
超级会员免费看
1344

被折叠的 条评论
为什么被折叠?



