28、Go 并发编程示例解析

Go 并发编程示例解析

1. 并发编程模式概述

Go 语言使用较少的语法(如 <- chan go select )就能以丰富多样的方式实现并发。常见的并发编程模式有三种:管道模式、多个独立并发任务模式(有或无同步结果)以及多个相互依赖的并发任务模式。下面将通过具体示例来详细介绍这些模式。

2. 过滤器(Filter)示例
2.1 管道概念与 Unix 管道类比

对于有 Unix 背景的人来说,Go 的通道类似于 Unix 管道,不同的是通道是双向的,而管道是单向的。Unix 管道可用于创建流水线,即一个程序的输出作为另一个程序的输入,以此类推。例如,使用 find $GOROOT/src -name "*.go" | grep -v test.go 命令可以获取 Go 源代码树中除测试文件外的所有 Go 文件列表。而且这种方式易于扩展,如添加 | xargs wc -l 可统计每个文件的行数,再添加 | sort -n 可按行数对文件进行排序。除了使用标准库的 io.Pipe() 函数创建 Unix 风格的管道外,还可以使用通道创建管道,下面将详细介绍。

2.2 过滤器程序实现

过滤器示例程序( filter/filter.go )接受一些命令行参数(如指定最小和最大文件大小以及可接受的文件后缀)和文件列表,并输出列表中符合命令行指定标准的文件。以下是该程序 main() 函数的主体:

minSize, maxSize, suffixes, files := handleCommandLine()
sink(filterSize(minSize, maxSize, filterSuffixes(suffixes, source(files))))

handleCommandLine() 函数使用标准库的 flag 包处理命令行参数。管道从最内层的函数调用( source(files) )到最外层的( sink() )依次执行。为了更易于理解,将管道布局如下:

channel1 := source(files)
channel2 := filterSuffixes(suffixes, channel1)
channel3 := filterSize(minSize, maxSize, channel2)
sink(channel3)
  • source() 函数
func source(files []string) <-chan string {
    out := make(chan string, 1000)
    go func() {
        for _, filename := range files {
            out <- filename
        }
        close(out)
    }()
    return out
}

此函数创建一个用于传递文件名的通道,使用缓冲通道以提高吞吐量。创建一个 goroutine 遍历文件并将每个文件名发送到通道,所有文件发送完毕后关闭通道。最后将双向通道作为单向接收通道返回,以更明确表达意图。

  • filterSuffixes() 函数
func filterSuffixes(suffixes []string, in <-chan string) <-chan string {
    out := make(chan string, cap(in))
    go func() {
        for filename := range in {
            if len(suffixes) == 0 {
                out <- filename
                continue
            }
            ext := strings.ToLower(filepath.Ext(filename))
            for _, suffix := range suffixes {
                if ext == suffix {
                    out <- filename
                    break
                }
            }
        }
        close(out)
    }()
    return out
}

该函数是两个过滤器函数之一,首先创建一个与输入通道缓冲区大小相同的输出通道。创建一个 goroutine 遍历输入通道中的文件名,如果未指定后缀,则将所有文件名发送到输出通道;如果指定了后缀,则仅将后缀匹配的文件名发送到输出通道。处理完成后关闭输出通道,并将其作为单向接收通道返回。

  • sink() 函数
func sink(in <-chan string) {
    for filename := range in {
        fmt.Println(filename)
    }
}

该函数在主 goroutine 中操作,遍历最后一个通道中的文件名并输出。其 range 语句会一直阻塞,直到通道关闭,确保主 goroutine 在其他 goroutine 处理完成后才终止。

2.3 过滤器程序并发结构

在过滤器程序中, sink() 函数在主 goroutine 中执行,每个管道函数(如 source() filterSuffixes() filterSize() )在各自的 goroutine 中执行。这意味着每个管道函数调用会立即返回,执行迅速到达 sink() 函数。此时所有 goroutine 并发执行,直到所有文件处理完毕。

以下是该程序的并发结构示意图:

graph LR
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    classDef channel fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
    A(Main goroutine: sink()):::process --> C(Channel #3):::channel
    B1(Goroutine #1: source()):::process --> C1(Channel #1):::channel
    B2(Goroutine #2: filterSuffixes()):::process --> C2(Channel #2):::channel
    B3(Goroutine #3: filterSize()):::process --> C
    C1 --> B2
    C2 --> B3
    C --> A
3. 并发 grep(Concurrent Grep)示例
3.1 并发 grep 模式概述

常见的并发编程模式之一是有多个独立任务,每个任务可以独立完成。例如,Go 标准库的 net/http 包中的 HTTP 服务器就遵循这种模式,每个请求在独立的 goroutine 中并发处理,且 goroutine 之间无通信。下面将以 cgrep “并发 grep” 程序的变体为例,介绍如何实现这种模式。

3.2 cgrep1 程序实现

cgrep1 程序( cgrep1/cgrep.go )使用三个通道,其中两个用于发送和接收结构体。

type Job struct {
    filename string
    results chan<- Result
}

type Result struct {
    filename string
    lino     int
    line     string
}

Job 结构体用于指定每个任务,包含要处理的文件名和结果发送的通道; Result 结构体封装每个结果,包含文件名、行号和匹配的行。

以下是 main() 函数:

func main() {
    runtime.GOMAXPROCS(runtime.NumCPU()) // Use all the machine's cores
    if len(os.Args) < 3 || os.Args[1] == "-h" || os.Args[1] == "--help" {
        fmt.Printf("usage: %s <regexp> <files>\n",
            filepath.Base(os.Args[0]))
        os.Exit(1)
    }
    if lineRx, err := regexp.Compile(os.Args[1]); err != nil {
        log.Fatalf("invalid regexp: %s\n", err)
    } else {
        grep(lineRx, commandLineFiles(os.Args[2:]))
    }
}

main() 函数的第一行告诉 Go 运行时系统使用机器可用的所有处理器核心。处理命令行参数(正则表达式和文件列表)后,调用 grep() 函数进行工作协调。

var workers = runtime.NumCPU()
func grep(lineRx *regexp.Regexp, filenames []string) {
    jobs := make(chan Job, workers)
    results := make(chan Result, minimum(1000, len(filenames)))
    done := make(chan struct{}, workers)
    go addJobs(jobs, filenames, results) // Executes in its own goroutine
    for i := 0; i < workers; i++ {
        go doJobs(done, lineRx, jobs) // Each executes in its own goroutine
    }
    go awaitCompletion(done, results) // Executes in its own goroutine
    processResults(results)
    // Blocks until the work is done
}

grep() 函数创建三个双向通道: jobs 通道用于分配任务, results 通道用于收集结果, done 通道用于标记任务完成。调用 addJobs() 函数添加任务,启动多个 doJobs() 函数处理任务,调用 awaitCompletion() 函数等待所有任务完成并关闭 results 通道,最后调用 processResults() 函数处理结果。

以下是各函数的具体实现:
- addJobs() 函数

func addJobs(jobs chan<- Job, filenames []string, results chan<- Result) {
    for _, filename := range filenames {
        jobs <- Job{filename, results}
    }
    close(jobs)
}

该函数将每个文件名作为 Job 值发送到 jobs 通道,发送完成后关闭通道。

  • doJobs() 函数
func doJobs(done chan<- struct{}, lineRx *regexp.Regexp, jobs <-chan Job) {
    for job := range jobs {
        job.Do(lineRx)
    }
    done <- struct{}{}
}

该函数在多个 goroutine 中调用,遍历 jobs 通道,处理每个任务,任务完成后向 done 通道发送空结构体表示完成。

  • awaitCompletion() 函数
func awaitCompletion(done <-chan struct{}, results chan Result) {
    for i := 0; i < workers; i++ {
        <-done
    }
    close(results)
}

该函数等待所有任务完成,关闭 results 通道。

  • processResults() 函数
func processResults(results <-chan Result) {
    for result := range results {
        fmt.Printf("%s:%d:%s\n", result.filename, result.lino, result.line)
    }
}

该函数在主 goroutine 中遍历 results 通道,输出结果。

3.3 cgrep 程序并发结构

以下是 cgrep 程序的并发结构示意图:

graph LR
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    classDef channel fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
    A(Main goroutine: processResults()):::process --> C(results channel):::channel
    B1(Goroutine #1: addJobs()):::process --> C1(jobs channel):::channel
    B2(Goroutine #2: doJobs()):::process --> C1
    B3(Goroutine #3: doJobs()):::process --> C1
    B4(Goroutine #4: doJobs()):::process --> C1
    B5(Goroutine #5: doJobs()):::process --> C1
    B6(Goroutine #6: awaitCompletion()):::process --> C
    B2 --> C
    B3 --> C
    B4 --> C
    B5 --> C
    B6 --> D(done channel):::channel
    B2 --> D
    B3 --> D
    B4 --> D
    B5 --> D
3.4 cgrep 程序变体

cgrep2 程序( cgrep2/cgrep.go )是 cgrep1 的变体,没有 awaitCompletion() processResults() 函数,而是使用单个 waitAndProcessResults() 函数:

func waitAndProcessResults(done <-chan struct{}, results <-chan Result) {
    for working := workers; working > 0; {
        select { // Blocking
        case result := <-results:
            fmt.Printf("%s:%d:%s\n", result.filename, result.lino,
                result.line)
        case <-done:
            working--
        }
    }
    DONE:
    for {
        select { // Nonblocking
        case result := <-results:
            fmt.Printf("%s:%d:%s\n", result.filename, result.lino,
                result.line)
        default:
            break DONE
        }
    }
}

cgrep3 程序( cgrep3/cgrep.go )是 cgrep2 的变体,增加了超时处理:

func waitAndProcessResults(timeout int64, done <-chan struct{},
    results <-chan Result) {
    finish := time.After(time.Duration(timeout))
    for working := workers; working > 0; {
        select { // Blocking
        case result := <-results:
            fmt.Printf("%s:%d:%s\n", result.filename, result.lino,
                result.line)
        case <-finish:
            fmt.Println("timed out")
            return // Time's up so finish with what results there were
        case <-done:
            working--
        }
    }
    for {
        select { // Nonblocking
        case result := <-results:
            fmt.Printf("%s:%d:%s\n", result.filename, result.lino,
                result.line)
        case <-finish:
            fmt.Println("timed out")
            return // Time's up so finish with what results there were
        default:
            return
        }
    }
}
3.5 任务处理方法

每个任务的处理通过 Job 结构体的 Do() 方法实现:

func (job Job) Do(lineRx *regexp.Regexp) {
    file, err := os.Open(job.filename)
    if err != nil {
        log.Printf("error: %s\n", err)
        return
    }
    defer file.Close()
    reader := bufio.NewReader(file)
    for lino := 1; ; lino++ {
        line, err := reader.ReadBytes('\n')
        line = bytes.TrimRight(line, "\n\r")
        if lineRx.Match(line) {
            job.results <- Result{job.filename, lino, string(line)}
        }
        if err != nil {
            if err != io.EOF {
                log.Printf("error:%d: %s\n", lino, err)
            }
            break
        }
    }
}

该方法打开文件,逐行读取,使用正则表达式匹配行,将匹配的行作为 Result 发送到 results 通道。

通过以上示例,我们详细了解了 Go 语言中管道模式和多个独立并发任务模式的实现,以及如何使用通道和 goroutine 进行并发编程。这些示例展示了 Go 语言并发编程的灵活性和强大功能。

Go 并发编程示例解析

4. 并发编程模式总结与对比

在前面我们详细介绍了过滤器(Filter)和并发 grep(Concurrent Grep)两个示例,它们分别代表了不同的并发编程模式。下面我们对这两种模式进行总结与对比。

模式 特点 适用场景 示例程序
管道模式 通过通道将多个函数连接成一个流水线,每个函数在独立的 goroutine 中执行,数据依次经过各个函数处理 当需要对数据进行一系列的处理步骤,且每个步骤可以独立完成时 过滤器(Filter)示例
多个独立并发任务模式 有多个独立的任务,每个任务可以独立完成,通过通道进行任务分配和结果收集 当有多个独立的任务需要同时处理,且任务之间不需要通信时 并发 grep(Concurrent Grep)示例
5. 并发编程的注意事项

在进行 Go 并发编程时,有一些注意事项需要我们牢记:
1. 通道的使用
- 通道的缓冲区大小需要根据实际情况进行设置,过大的缓冲区会占用过多的内存,过小的缓冲区可能会导致阻塞。
- 确保在不需要使用通道时及时关闭通道,避免出现死锁或资源泄漏。
2. 共享资源的线程安全
- 对于共享的指针类型的值,需要确保其是线程安全的,否则需要使用互斥锁等机制来保证线程安全。
- 例如,在并发 grep 示例中, *regexp.Regexp 是线程安全的,因此可以在多个 goroutine 中共享使用。
3. 避免 CPU 浪费
- 在使用 select 语句时,要避免使用非阻塞的 select 语句导致 CPU 浪费,除非有特殊需求。
- 例如,在并发 grep 示例中,使用阻塞的 select 语句等待结果或任务完成。

6. 并发编程的扩展与优化

并发编程可以根据实际需求进行扩展和优化,以下是一些常见的方法:
1. 增加处理步骤
- 在管道模式中,可以增加更多的处理函数,以实现更复杂的数据处理流程。
- 例如,在过滤器示例中,可以添加更多的过滤器函数来进一步筛选文件。
2. 动态调整 goroutine 数量
- 在多个独立并发任务模式中,可以根据任务的数量和复杂度动态调整 goroutine 的数量。
- 例如,可以根据文件数量动态创建相应数量的 goroutine 来处理任务。
3. 使用更复杂的通道类型
- 可以使用更复杂的通道类型,如带缓冲的通道、单向通道等,以提高程序的性能和可读性。
- 例如,在并发 grep 示例中,使用带缓冲的通道来减少阻塞。

7. 总结

通过本文的介绍,我们深入了解了 Go 语言中的并发编程模式,包括管道模式和多个独立并发任务模式。我们学习了如何使用通道和 goroutine 来实现并发编程,以及在并发编程中需要注意的事项和优化方法。

并发编程是 Go 语言的一大特色,它可以提高程序的性能和响应速度。在实际开发中,我们可以根据具体的需求选择合适的并发编程模式,并结合通道和 goroutine 的使用,编写出高效、稳定的并发程序。

以下是一个简单的 mermaid 流程图,展示了并发编程的一般流程:

graph LR
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    classDef channel fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
    A(任务初始化):::process --> B(创建 goroutine):::process
    B --> C(任务分配):::process
    C --> D(任务处理):::process
    D --> E(结果收集):::process
    E --> F(结果处理):::process
    F --> G(任务结束):::process
    B -.-> H(通道通信):::channel
    D -.-> H
    E -.-> H

总之,Go 语言的并发编程为我们提供了强大的工具和灵活的编程模式,通过合理运用这些特性,我们可以更好地应对各种复杂的并发场景。希望本文对你理解和掌握 Go 并发编程有所帮助。

【最优潮流】直流最优潮流(OPF)课设(Matlab代码实现)内容概要:本文档主要围绕“直流最优潮流(OPF)课设”的Matlab代码实现展开,属于电力系统优化领域的教学与科研实践内容。文档介绍了通过Matlab进行电力系统最优潮流计算的基本原理与编程实现方法,重点聚焦于直流最优潮流模型的构建与求解过程,适用于课程设计或科研入门实践。文中提及使用YALMIP等优化工具包进行建模,并提供了相关资源下载链接,便于读者复现与学习。此外,文档还列举了大量与电力系统、智能优化算法、机器学习、路径规划等相关的Matlab仿真案例,体现出其服务于科研仿真辅导的综合性平台性质。; 适合人群:电气工程、自动化、电力系统及相关专业的本科生、研究生,以及从事电力系统优化、智能算法应用研究的科研人员。; 使用场景及目标:①掌握直流最优潮流的基本原理与Matlab实现方法;②完成课程设计或科研项目中的电力系统优化任务;③借助提供的丰富案例资源,拓展在智能优化、状态估计、微电网调度等方向的研究思路与技术手段。; 阅读建议:建议读者结合文档中提供的网盘资源,下载完整代码与工具包,边学习理论边动手实践。重点关注YALMIP工具的使用方法,并通过复现文中提到的多个案例,加深对电力系统优化问题建模与求解的理解。
本程序为针对江苏省中医院挂号系统设计的自动化预约工具,采用Python语言编写。项目压缩包内包含核心配置文件与主执行文件。 配置文件conf.ini中,用户需根据自身情况调整身份验证参数:可填写用户名与密码,或直接使用有效的身份令牌(若提供令牌则无需填写前两项)。其余配置项通常无需更改。 主文件main.py包含两项核心功能: 1. 预约测试模块:用于验证程序运行状态及预约流程的完整性。执行后将逐步引导用户选择院区、科室类别、具体科室、医师、就诊日期、时段及具体时间,最后确认就诊卡信息。成功预约后将返回包含预约编号及提示信息的结构化结果。 2. 监控预约模块:可持续监测指定医师在设定日期范围内的可预约时段。一旦检测到空闲号源,将自动完成预约操作。该模块默认以10秒为间隔循环检测,成功预约后仍会持续运行直至手动终止。用户需注意在预约成功后及时完成费用支付以确认挂号。 程序运行时会显示相关技术支持信息,包括采用的验证码识别组件及训练数据来源。操作界面采用分步交互方式,通过输入序号完成各环节选择。所有网络请求均经过结构化处理,返回结果包含明确的状态码与执行耗时。 资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值