30、Go 并发编程实战:文件处理与练习实践

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 语言的并发编程提供了强大而灵活的工具,通过不断练习和实践,可以更好地掌握并发编程的技巧,编写出高效、稳定的并发程序。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值