Go 并发编程实战:文件处理与练习实践
1. 并发编程模型与示例概述
在 Go 语言的并发编程中,有多种并发模型可供选择。例如,
apachereport3
程序采用的并发模型是每个 goroutine 填充自己无竞争的数据结构,最后合并结果,这种模型在很多场景下都很有用。Go 文档强烈推荐使用 goroutines 和 channels,遵循“不要通过共享内存来通信,而是通过通信来共享内存”的原则,并且 Go 编译器也在不断优化以支持这种并发模型。
2. 查找重复文件程序
findduplicates
findduplicates
程序用于查找重复文件,它不依赖文件名,而是通过文件大小和 SHA - 1 值来判断文件是否重复。以下是该程序的详细介绍:
2.1 程序结构与流程
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始]):::startend --> B(创建 infoChan 通道):::process
B --> C(启动 findDuplicates 函数):::process
C --> D(执行 mergeResults 函数):::process
D --> E(执行 outputResults 函数):::process
E --> F([结束]):::startend
2.2 代码实现
const maxGoroutines = 100
func main() {
runtime.GOMAXPROCS(runtime.NumCPU()) // 使用所有机器核心
if len(os.Args) == 1 || os.Args[1] == "-h" || os.Args[1] == "--help" {
fmt.Printf("usage: %s <path>\n", filepath.Base(os.Args[0]))
os.Exit(1)
}
infoChan := make(chan fileInfo, maxGoroutines*2)
go findDuplicates(infoChan, os.Args[1])
pathData := mergeResults(infoChan)
outputResults(pathData)
}
type fileInfo struct {
sha1 []byte
size int64
path string
}
func findDuplicates(infoChan chan fileInfo, dirname string) {
waiter := &sync.WaitGroup{}
filepath.Walk(dirname, makeWalkFunc(infoChan, waiter))
waiter.Wait() // 阻塞直到所有工作完成
close(infoChan)
}
const maxSizeOfSmallFile = 1024 * 32
func makeWalkFunc(infoChan chan fileInfo,
waiter *sync.WaitGroup) func(string, os.FileInfo, error) error {
return func(path string, info os.FileInfo, err error) error {
if err == nil && info.Size() > 0 &&
(info.Mode()&os.ModeType == 0) {
if info.Size() < maxSizeOfSmallFile ||
runtime.NumGoroutine() > maxGoroutines {
processFile(path, info, infoChan, nil)
} else {
waiter.Add(1)
go processFile(path, info, infoChan,
func() { waiter.Done() })
}
}
return nil // 忽略所有错误
}
}
func processFile(filename string, info os.FileInfo,
infoChan chan fileInfo, done func()) {
if done != nil {
defer done()
}
file, err := os.Open(filename)
if err != nil {
log.Println("error:", err)
return
}
defer file.Close()
hash := sha1.New()
if size, err := io.Copy(hash, file);
size != info.Size() || err != nil {
if err != nil {
log.Println("error:", err)
} else {
log.Println("error: failed to read the whole file:", filename)
}
return
}
infoChan <- fileInfo{hash.Sum(nil), info.Size(), filename}
}
type pathsInfo struct {
size int64
paths []string
}
func mergeResults(infoChan <-chan fileInfo) map[string]*pathsInfo {
pathData := make(map[string]*pathsInfo)
format := fmt.Sprintf("%%016X:%%%dX", sha1.Size*2) // == "%016X:%40X"
for info := range infoChan {
key := fmt.Sprintf(format, info.size, info.sha1)
value, found := pathData[key]
if !found {
value = &pathsInfo{size: info.size}
pathData[key] = value
}
value.paths = append(value.paths, info.path)
}
return pathData
}
func outputResults(pathData map[string]*pathsInfo) {
keys := make([]string, 0, len(pathData))
for key := range pathData {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
value := pathData[key]
if len(value.paths) > 1 {
fmt.Printf("%d duplicate files (%s bytes):\n",
len(value.paths), commas(value.size))
sort.Strings(value.paths)
for _, name := range value.paths {
fmt.Printf("\t%s\n", name)
}
}
}
}
func commas(x int64) string {
value := fmt.Sprint(x)
for i := len(value) - 3; i > 0; i -= 3 {
value = value[:i] + "," + value[i:]
}
return value
}
2.3 代码解释
-
main函数 :程序的入口点,负责创建通道、启动findDuplicates函数、合并结果并输出。 -
fileInfo结构体 :用于存储每个文件的 SHA - 1 值、大小和路径。 -
findDuplicates函数 :调用filepath.Walk遍历目录树,并使用sync.WaitGroup等待所有工作完成。 -
makeWalkFunc函数 :创建一个filepath.WalkFunc函数,根据文件大小和当前 goroutine 数量决定是否创建新的 goroutine 处理文件。 -
processFile函数 :计算文件的 SHA - 1 值,并将文件信息发送到infoChan通道。 -
mergeResults函数 :从infoChan通道读取文件信息,合并重复文件。 -
outputResults函数 :输出重复文件的信息。 -
commas函数 :为文件大小添加逗号分隔符,方便阅读。
3. 避免“打开文件过多”问题的策略
为了避免“打开文件过多”的问题,程序采用了以下两种策略:
-
处理小文件
:对于小于 32KiB 的小文件,在当前 goroutine 中直接计算其 SHA - 1 值,确保在处理大量小文件时不会同时打开过多文件。
-
控制 goroutine 数量
:使用
runtime.NumGoroutine()
函数监控当前 goroutine 数量,如果数量过多,则停止创建新的 goroutine 处理大文件,而是在当前 goroutine 中处理所有文件。
4. 并发编程练习
以下是几个并发编程的练习,用于巩固所学知识:
4.1 创建线程安全的切片
创建一个线程安全的切片类型
safeSlice
和
SafeSlice
接口:
type SafeSlice interface {
Append(interface{})
At(int) interface{}
Close() []interface{}
Delete(int)
Len() int
Update(int, UpdateFunc)
}
需要实现
safeSlice.run()
方法和
New()
函数,具体实现可以参考
safeslice/safeslice.go
文件。
4.2 处理图像文件
创建一个程序,接受一个或多个图像文件名作为命令行参数,并发处理这些图像,并输出 HTML 标签。
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始]):::startend --> B(读取命令行参数):::process
B --> C{是否为图像文件}:::decision
C -->|是| D(启动 goroutine 处理):::process
C -->|否| E(忽略该文件):::process
D --> F(输出 HTML 标签):::process
F --> G([结束]):::startend
E --> G
具体步骤如下:
1. 读取命令行参数。
2. 过滤非图像文件。
3. 使用固定数量的 worker goroutine 并发处理图像。
4. 使用
image.DecodeConfig()
函数获取图像的宽度和高度。
5. 输出 HTML 标签。
4.3 处理 HTML 文件中的图像标签
创建一个并发程序,接受一个或多个 HTML 文件作为命令行参数,检查 HTML 文件中的
<img>
标签是否有宽度和高度属性,如果没有则添加。
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始]):::startend --> B(读取命令行参数):::process
B --> C(启动固定数量的 worker goroutine):::process
C --> D(读取 HTML 文件):::process
D --> E{是否找到 <img> 标签}:::decision
E -->|是| F{是否有宽度和高度属性}:::decision
E -->|否| D
F -->|否| G(获取图像大小):::process
F -->|是| D
G --> H(更新 <img> 标签):::process
H --> D
D --> I([结束]):::startend
具体步骤如下:
1. 读取命令行参数。
2. 启动固定数量的 worker goroutine。
3. 读取 HTML 文件,使用正则表达式查找
<img>
标签。
4. 检查
<img>
标签是否有宽度和高度属性,如果没有则使用
image.DecodeConfig()
函数获取图像大小并更新标签。
通过这些练习,可以更好地掌握 Go 语言的并发编程技巧,提高编程能力。
Go 并发编程实战:文件处理与练习实践
5. 练习实现的详细分析
5.1 创建线程安全的切片
在创建线程安全的切片时,我们定义了
SafeSlice
接口,它包含了一系列操作切片的方法,如
Append
、
At
、
Close
、
Delete
、
Len
和
Update
。以下是对这些方法的详细解释:
| 方法名 | 功能 |
| ---- | ---- |
|
Append(interface{})
| 将给定的项追加到切片中 |
|
At(int)
| 返回指定索引位置的项 |
|
Close()
| 关闭通道并返回切片 |
|
Delete(int)
| 删除指定索引位置的项 |
|
Len()
| 返回切片中的项数 |
|
Update(int, UpdateFunc)
| 更新指定索引位置的项 |
为了实现线程安全,我们需要在
safeSlice.run()
方法中创建底层切片,并在一个“无限”循环中迭代通信通道。
New()
函数负责创建安全切片并在 goroutine 中执行
safeSlice.run()
方法。具体的实现细节可以参考
safeslice/safeslice.go
文件。
5.2 处理图像文件
在处理图像文件的练习中,我们的目标是接受一个或多个图像文件名作为命令行参数,并发处理这些图像,并输出 HTML 标签。以下是具体的操作步骤:
1.
读取命令行参数
:程序启动后,首先读取用户输入的命令行参数,获取图像文件的路径。
2.
过滤非图像文件
:使用
os.FileInfo
检查文件是否为常规文件,并使用
image.DecodeConfig()
函数的特性,结合导入相应的图像格式包(如
_ "image/jpeg"
、
_ "image/png"
)来判断文件是否为图像文件。忽略非图像文件和所有错误。
3.
使用固定数量的 worker goroutine 并发处理图像
:创建一个固定数量的 worker goroutine 池,将图像文件分配给这些 goroutine 进行处理。
4.
使用
image.DecodeConfig()
函数获取图像的宽度和高度
:打开图像文件,将其作为
io.Reader
传递给
image.DecodeConfig()
函数,获取图像的宽度和高度,而无需读取整个图像。
5.
输出 HTML 标签
:根据获取的图像信息,输出 HTML 标签,格式为
<img src="filename" width="width" height="height" />
。
以下是一个简化的代码示例,展示了如何实现这些步骤:
package main
import (
"fmt"
"image"
_ "image/jpeg"
_ "image/png"
"os"
)
func processImage(filename string) {
file, err := os.Open(filename)
if err != nil {
return
}
defer file.Close()
config, _, err := image.DecodeConfig(file)
if err != nil {
return
}
fmt.Printf("<img src=\"%s\" width=\"%d\" height=\"%d\" />\n", filename, config.Width, config.Height)
}
func main() {
if len(os.Args) < 2 {
fmt.Println("usage: program <image_file1> <image_file2> ...")
return
}
for _, filename := range os.Args[1:] {
go processImage(filename)
}
// 等待所有 goroutine 完成
// 这里可以使用 sync.WaitGroup 来实现更精确的同步
// 为了简化示例,省略了这部分代码
}
5.3 处理 HTML 文件中的图像标签
在处理 HTML 文件中的图像标签的练习中,我们需要接受一个或多个 HTML 文件作为命令行参数,检查 HTML 文件中的
<img>
标签是否有宽度和高度属性,如果没有则添加。以下是具体的操作步骤:
1.
读取命令行参数
:程序启动后,读取用户输入的命令行参数,获取 HTML 文件的路径。
2.
启动固定数量的 worker goroutine
:创建一个固定数量的 worker goroutine 池,每个 goroutine 负责处理一个或多个 HTML 文件。
3.
读取 HTML 文件,使用正则表达式查找
<img>
标签
:使用
os.Open()
打开 HTML 文件,读取文件内容,使用正则表达式
<[iI][mM][gG][^>]+>
查找所有
<img>
标签。
4.
检查
<img>
标签是否有宽度和高度属性,如果没有则使用
image.DecodeConfig()
函数获取图像大小并更新标签
:对于每个找到的
<img>
标签,使用正则表达式
src=["']([^"']+)["']
提取图像文件名,打开图像文件,使用
image.DecodeConfig()
函数获取图像的宽度和高度,然后更新
<img>
标签。
以下是一个简化的代码示例,展示了如何实现这些步骤:
package main
import (
"bufio"
"fmt"
"image"
_ "image/jpeg"
_ "image/png"
"os"
"regexp"
"strings"
)
func processHTMLFile(filename string) {
file, err := os.Open(filename)
if err != nil {
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
var content string
for scanner.Scan() {
content += scanner.Text() + "\n"
}
imgRegex := regexp.MustCompile(`<[iI][mM][gG][^>]+>`)
srcRegex := regexp.MustCompile(`src=["']([^"']+)["']`)
imgTags := imgRegex.FindAllString(content, -1)
for _, imgTag := range imgTags {
if !strings.Contains(imgTag, "width") || !strings.Contains(imgTag, "height") {
srcMatch := srcRegex.FindStringSubmatch(imgTag)
if len(srcMatch) > 1 {
imgFilename := srcMatch[1]
imgFile, err := os.Open(imgFilename)
if err != nil {
continue
}
defer imgFile.Close()
config, _, err := image.DecodeConfig(imgFile)
if err != nil {
continue
}
newTag := fmt.Sprintf(`<img src="%s" width="%d" height="%d" />`, imgFilename, config.Width, config.Height)
content = strings.ReplaceAll(content, imgTag, newTag)
}
}
}
fmt.Println(content)
}
func main() {
if len(os.Args) < 2 {
fmt.Println("usage: program <html_file1> <html_file2> ...")
return
}
for _, filename := range os.Args[1:] {
go processHTMLFile(filename)
}
// 等待所有 goroutine 完成
// 这里可以使用 sync.WaitGroup 来实现更精确的同步
// 为了简化示例,省略了这部分代码
}
6. 并发编程的总结与建议
通过对
findduplicates
程序的分析和并发编程练习的实践,我们可以总结出以下几点关于 Go 并发编程的建议:
-
合理使用 goroutine 和 channel
:Go 语言的并发编程模型强调使用 goroutine 和 channel 进行通信和同步。合理使用它们可以提高程序的性能和可维护性。例如,在
findduplicates
程序中,使用 channel 传递文件信息,使用 goroutine 并发处理文件。
-
控制 goroutine 数量
:过多的 goroutine 可能会导致系统资源耗尽,尤其是在处理大量文件或网络请求时。使用
sync.WaitGroup
和
runtime.NumGoroutine()
函数来控制 goroutine 的数量,避免“打开文件过多”等问题。
-
使用线程安全的数据结构
:在并发编程中,需要确保数据结构的线程安全。可以使用互斥锁、读写锁或其他同步机制来实现线程安全。例如,在练习中创建线程安全的切片。
-
测试和性能优化
:并发编程的性能受到多种因素的影响,如机器性能、goroutine 数量、处理逻辑等。使用性能测试工具和分析工具来找出性能瓶颈,并进行优化。例如,通过测试不同的并发策略和 goroutine 数量,选择最适合的方案。
总之,Go 语言的并发编程提供了强大而灵活的工具,通过不断练习和实践,可以更好地掌握并发编程的技巧,编写出高效、稳定的并发程序。
超级会员免费看
9

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



