22、Go语言中的并发编程:管道、协程与通道的高级应用

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语言的并发编程特性为我们提供了强大的工具,通过合理运用这些特性,我们可以充分发挥多核处理器的性能,实现高效的并发程序。

纸张塑料实例分割数据集 一、基础信息 • 数据集名称:纸张塑料实例分割数据集 • 图片数量: 训练集:5304张图片 验证集:440张图片 总计:5744张图片 • 训练集:5304张图片 • 验证集:440张图片 • 总计:5744张图片 • 分类类别: 纸张(paper):常见的可回收材料,广泛用于包装和日常用品。 塑料(plastic):合成聚合物材料,在垃圾处理和回收中需准确识别。 • 纸张(paper):常见的可回收材料,广泛用于包装和日常用品。 • 塑料(plastic):合成聚合物材料,在垃圾处理和回收中需准确识别。 • 标注格式:YOLO格式,包含实例分割多边形标注,适用于实例分割任务。 • 数据格式:图片数据来源于相关领域,标注精确,支持模型训练。 二、适用场景 • 垃圾自动分类系统开发:数据集支持实例分割任务,帮助构建能够精确分割纸张和塑料物体的AI模型,用于智能垃圾桶、回收设施或环境监测系统。 • 环境监测保护应用:集成至环保监控平台,实时检测和分类垃圾,促进垃圾分类、回收和可持续发展。 • 学术研究创新:支持计算机视觉环保领域的交叉研究,为垃圾识别和材料分类提供数据基础,推动AI在环境科学中的应用。 • 工业自动化物流:在制造业或物流环节中,用于自动化检测和分类材料,提升生产效率和资源管理。 三、数据集优势 • 精准标注实用性:每张图片均经过仔细标注,实例分割边界精确,确保模型能够学习纸张和塑料的细粒度特征。 • 数据多样性:涵盖多种场景和条件,提升模型在不同环境下的泛化能力和鲁棒性。 • 任务适配性强:标注兼容主流深度学习框架(如YOLO等),可直接用于实例分割模型训练,并支持扩展至其他视觉任务。 • 应用价值突出:专注于可回收材料检测,为垃圾管理、环保政策和自动化系统提供可靠数据支撑,助力绿色科技发展。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值