golang并发编程 - 例子解析

February 26, 2013

最近在看《Programming in Go》, 其中关于并发编程写得很不错, 受益非浅, 其中有一些例子是需要多思考才能想明白的, 所以我打算记录下来, 强化一下思路

《Programming in Go》在 Chapter 7. Concurrent Programming 里面一共用3个例子来讲述并发编程的3个模式, 第一个是 filter , 筛选出后缀名和文件大小文件列表, 还算简单就不说, 然后第二个是升级版, 正则版 filter , 不同的是他是根据正则搜索出文件的文本并且列出来. 这个例子我起初看是有点蒙的, 这样写是没错, 但是为什么要这样写, 他的设计思路是什么, 和其他方法相比他有什么优势, 这些都不清楚, 于是决定好好分析一下. 实际上这个例子实现的功能并不复杂, 所以我的文章实际上是在讨论怎么产生出和作者相似的思路.

如果不考虑用 goroutine 的话, 思路其实很简单:

1. 列出文件列表, 编译正则.
2. 遍历文件, 打开并遍历每行, 如果正则能匹配, 记录下来.
3. 列出来.

如果用 goroutine , 就会有以下思路:

1. 在得到文件路径数组之后, 分发任务给N个核.
2. 每个核负责打开文件, 将符合条件的那行文本写入到 `channel`
3. 主线程等待并接收`channel`的结果. 显示出来, 完毕

** 然后下文才是重点 **

1. channel关闭的时机

在go中, channel 是不会自动关闭的, 所以需要在我们使用完之后手动去关闭, 而且如果使用for语法来遍历channel每次得到的数据, 如果channel没有关闭的话会陷入死循环. 在 goroutine 中会造成 deadlock

for job := range jobs {
fmt.Println(job)
}

如果没close, 会触发dead lock. 因为for...range...会自动阻塞直到读取到数据或者channel关闭, 没close的话就会导致整个channel处于睡眠状态. channel关闭后, 就不允许写入(缓冲的数据还在, 还可以读取), 所以, channel 关闭的时机很重要.

2. 分发任务

我所知道任务分发方法有两种:

第一种是固定分配, 如果说我想计算1+2+3+...+100, 然后分成4份, 也就是 1+2+..+25......86+87+...+100, 然后再将结果累加起来.

还有一种是抢占式的, 这里需要使用一个队列, 将所有任务写入队列, 然后开N个goroutine, 每个goroutine从队列读取任务(要确保线程安全), 处理, 完成后再继续读取任务. 不再是固定分配, 自己那份做完了就休息了, 所以看来第二种要好一点.

采用第二种方式的话, 对应go的做法, 那就是使用一个channel, 命名为 jobs, 将所有的任务写入进去, 写入完毕之后关闭这个 channel, 当然, 因为是N核, 系统能同时处理的任务我们设置为N个(也就是我们使用了N个goroutine), 那么声明 jobs 是缓冲区长度为N的 channel.

Buffered channel 和普通的 channel 的差别是他可以同时容纳多个单位数据, 当缓存的数据单位数量等于 channel 容量的时候, 再执行写入将会阻塞, 否则都是及时处理的.

3. 结果集

当我们将数据处理后, 就需要将结果收集起来. 需要注意的是, 这些操作不是在主 goroutine 执行, 所以我们需要通过 channel 传递给主 goroutine . 所以只需要在外部声明一个名为 results 的 channel . 然后在主 goroutine 通过 for 来显示, 这时候就会发现一个问题, 这个 results 关闭的时机问题. 正确的关闭时机是写入所有的 Result 之后. 但是别忘了我们同时开了多个 goroutine , 所以 results 应该在 执行任务的 goroutine 完成信号累计到N个 这个时机关闭. 所以我们再引入一个名叫 done 的 channel 来解决. 每个 goroutine 发送完 result 后会写入一次done, 然后我们就可以遍历 done , 遍历之后说明全部完成了, 再执行显示.

Result 的数据结构

type Result struct {
filename string
lino int
line string
}

书中的 cgrep1 就是这样的

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

但是这样有可能造成死锁, 因为书中 results 缓冲区长度限定为最大1000个, 也就是超过1000个 result 的时候再打算写入 result 会等待取出 result 后才执行, done 也不会写入, 而 awaitCompletion 是等到所有 goroutine 都完成了才会取出 results, 而且当 result 非常大的时候因为内存的缘故也是不可能一次性取出的. 所以就需要在读取 results 的同时读取 done, 当读取 done 次数大于 N 后关闭 results, 所以, 因为要在多个 channel 中同时读取, 所以需要使用 select.

下面是书中的 cgrep3 , 改进版:

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
}
}
}

看到这里, 我就有个疑问, 为什么在全部完成之后(done都接收到N个了), 还要再遍历出 results, 直到读取不到才算读取完成呢(我反应一向比较慢^_^)? 于是我做了个实验, 去掉了后面再次循环的部分, 发现有时会遗漏掉数据(我用4个测试文件...), 证明这段代码是有用的!!!

我的想法是, 他是在处理完 result, 然后写入 results, 写完了才发送 done, 也就是在收到所有的 done 之后, 所有的数据应该是已经处理完成的. 为了验证这个想法, 我写了一下代码:

for working := workers; working > 0; {
select { // Blocking
case result := <-results:
// received result
case <-done:
working--
if working <= 0 {
println(len(results))
}
}
}

然后看到输出的数是大于0的, 也就是说在接收到全部 done 之后, results 还有数据在缓冲区中, 然后在看看发送result 的代码, 突然就明白了

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

我把写入和读取想当然认为一起发生了, 因为有缓冲区的缘故, doJobs在发送进 results 的缓冲区之后就立刻发送 done 了, 但是写入的数据有没有被处理, 是不知道的, 所以在接收到所有 done 之后, results 缓冲区还有数据, 需要再循环一遍.


附我的代码一份:

package main

import (
"bufio"
"fmt"
"log"
"os"
"regexp"
"runtime"
) type Job struct {
filename string
results chan<- Result
} type Result struct {
filename string
line string
lino int
} var worker = runtime.NumCPU() func main() {
// config cpu number
runtime.GOMAXPROCS(worker)
files := os.Args[2:]
regex, err := regexp.Compile(os.Args[1])
if err != nil {
log.Fatal(err)
return
} // 任务列表, 并发数目为CPU个数
jobs := make(chan Job, worker)
// 结果
results := make(chan Result, minimum(1000, len(files)))
defer close(results)
// 标记完成
dones := make(chan int, worker)
defer close(dones) go addJob(files, jobs, results)
for i := 0; i < worker; i++ {
go doJob(jobs, regex, dones)
}
awaitForCloseResult(dones, results)
} func addJob(files []string, jobs chan<- Job, results chan<- Result) {
for _, filename := range files {
jobs <- Job{filename, results}
}
close(jobs)
} func doJob(jobs <-chan Job, regex *regexp.Regexp, dones chan int) {
for job := range jobs {
job.Do(regex)
} dones <- 1
} func awaitForCloseResult(dones <-chan int, results chan Result) {
working := 0
MyForLable:
for {
select {
case result := <-results:
println(result)
case <-dones:
working++
if working >= worker {
if rlen := len(results); rlen > 0 {
println("----------------------------------")
println("left:", rlen)
println("----------------------------------")
for i := 1; i <= rlen; i++ {
println(<-results)
}
}
break MyForLable
}
}
}
} func (j *Job) Do(re *regexp.Regexp) {
f, err := os.Open(j.filename)
if err != nil {
println(err)
return
}
defer f.Close() b := bufio.NewReader(f)
lino := 0
for {
line, _, err := b.ReadLine()
if re.Match(line) {
j.results <- Result{j.filename, string(line), lino}
} if err != nil {
break
}
lino += 1
}
} func minimum(a, b int) int {
if a > b {
return b
}
return a
} func println(o ...interface{}) {
fmt.Println(o...)
}

转自:http://chenye.org/goroutine-note.html

goroutine 并发之搜索文件内容的更多相关文章

  1. win7中搜索文件内容的方法

    打开“控制面板”,选择“大类别”或“小类别”,然后打开 “索引选项”.点击“高级”按钮,在弹出的对话框中打开“文件类型”标签,在下方的输入框中“将新扩展名添加到列表中”,添加要搜索的未知文本文件的扩展 ...

  2. windows10的文件浏览器中无法搜索文件内容

    系统:更新到最新的win10(2018年8月23日 23:54:31) 重现步骤:git clone一个项目,然后切换到它的另一个分支:打开文件夹浏览器(explorer),在右上角里输入想要查找的字 ...

  3. lucene7.1.0实现搜索文件内容

    Lucene的使用主要体现在两个步骤: 1 创建索引,通过IndexWriter对不同的文件进行索引的创建,并将其保存在索引相关文件存储的位置中. 2 通过索引查寻关键字相关文档. 首先,我们需要定义 ...

  4. linux 搜索文件内容并输出命令 grep、-i、-v ^#

    grep /bin/grepgrep -iv [指定字条串] [文件]在文件中搜索字符串匹配的行并输出-i 不区分大小写 -v 排除指定字符串 grep -i java /etc/profile gr ...

  5. Intellij IDEA 搜索文件内容

    位置:Edit-Find-Find in Path 把KeyMap改成Eclipse的就可以用Ctrl+H查找了.

  6. FileSeek文件内容搜索工具下载

    Windows 内建的搜索功能十分简单,往往不能满足用户的需要.很多的第三方搜索工具因此诞生,比如 Everything,Locate32等. 而FileSeek也是一款不错的搜索工具,其不同于其他搜 ...

  7. Linux搜索文件或内容

    1.递归搜索文件内容,如果查找"hello,world!"字符串,可以这样: grep -rn "hello,world!" * * : 表示当前目录所有文件, ...

  8. Linux基础知识第四讲,文件内容命令

    目录 一丶常用命令 1.cat命令演示以及常用选项 2.grep 搜索命令的使用 3.echo 以及 重定向的使用 4.管道概念 一丶常用命令 序号 命令 对应英文 作用 01 cat 文件名 con ...

  9. windows7搜索python java go php等其他文件内容

    1.添加文件内容搜索配置 2.将需要搜索的文件索引,添加至windows索引 控制面板->索引选项->高级->文件类型 把需要搜索的文件添加一下索引 3.如果不行的话,那么还是在索引 ...

随机推荐

  1. C++11之 Move semantics(移动语义)(转)

    转https://blog.csdn.net/wangshubo1989/article/details/49748703 按值传递的意义是什么? 当一个函数的参数按值传递时,这就会进行拷贝.当然,编 ...

  2. ExtJS4 ajax请求同步异步问题

    今天在写代码过程中遇到一个奇怪的问题.事情是这种,我写了一个简单的页面用来删除选中的用户,请看以下: 由于后台的servlet的代码实现了依据用户名来删除一条记录.所以我在前台的ExtJS代码里面用了 ...

  3. 【洛谷5286】[HNOI2019] 鱼(计算几何)

    点此看题面 大致题意: 给你\(n\)个点,让你求鱼形图的数量. 核心思路 首先,考虑到\(n\)这么小,我们可以枚举线段\(AD\),再去找符合条件的\(BC,EF\). 然后,不难发现\(BC\) ...

  4. 动态规划(DP),最大矩阵和

    题目链接:http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemId=74 http://poj.org/problem?id=1050 解题 ...

  5. luogu P1710 地铁涨价

    嘟嘟嘟 一道最短路好题. 首先明确一点,把一条边的边权变成2,等于删去这条边.因为变成2后最短路肯定不会经过这条边,就相当于删去这条边了. 所以题目变成了依次删去Q条边,求每一次删完边后有几个点的最短 ...

  6. 使用ToDateTime方法转换日期显示格式

    实现效果: 知识运用: Convert类的ToDateTime方法:(将字符串转化为DateTime对象) public static DateTime ToDateTime(string value ...

  7. ASP.NET SignalR 与LayIM配合,轻松实现网站客服聊天室(三) 激动人心的时刻到啦,实现1v1聊天

    看起来挺简单,细节还是很多的,好,接上一篇,我们已经成功连接singalR服务器了,那么剩下的内容呢,就是一步一步实现聊天功能. 我们先看看缺什么东西 点击好友弹框之后,要给服务器发消息,进入组Gro ...

  8. 教你用SingalR实现后台开发程序运行时时检测

    在调试js的时候都喜欢用console.log输出相应的运行结果或者查看变量值,以便于调试程序bug,可是程序后台代码运行的时候,本地没有问题,线上代码有问题怎么办呢,写日志是一个办法,可是有没有更直 ...

  9. Google TensorFlow 学习笔记一 —— TensorFlow简介

    "TensorFlow is an Open Source Software Library for Machine INtenlligence" 本笔记参考tensorflow. ...

  10. 【洛谷P3119】[USACO15JAN]草鉴定Grass Cownoisseur

    草鉴定Grass Cownoisseur 题目链接 约翰有n块草场,编号1到n,这些草场由若干条单行道相连.奶牛贝西是美味牧草的鉴赏家,她想到达尽可能多的草场去品尝牧草. 贝西总是从1号草场出发,最后 ...