您的位置:首页 > 编程语言 > Go语言

Go中的goroutine和channel使用

2015-11-29 17:22 519 查看
作为一个go的新手,一开始跟着Go指南进行Go的学习,在完成指南上的Web爬虫练习时遇到了一些goroutine与channel相关的问题。
指南上一开始给出了原始代码其中最重要的就是Crawl函数,代码如下:

// Crawl 使用 fetcher 从某个 URL 开始递归的爬取页面,直到达到最大深度。
func Crawl(url string, depth int, fetcher Fetcher) {
// TODO: 并行的抓取 URL。
// TODO: 不重复抓取页面。
// 下面并没有实现上面两种情况:
if depth <= 0 {
return
}
body, urls, err := fetcher.Fetch(url)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("found: %s %q\n", url, body)
for _, u := range urls {
Crawl(u, depth-1, fetcher)
}
return
}
这段代码很简单,就是递归地Crawl所获得的所有url。题目要求修改Crawl函数使用并发并且不重复抓取页面。

完成这个功能的难点在于如果依旧使用递归的方式来进行页面抓取,那么子goroutine如何知晓某一url是否已被抓取,一开始,我是用这样的方式实现的:

// Crawl 使用 fetcher 从某个 URL 开始递归的爬取页面,直到达到最大深度。
func Crawl(url string, depth int, fetcher Fetcher) {
// TODO: 并行的抓取 URL。
// TODO: 不重复抓取页面。
ch:=make(chan int)
count:=1
urlsFetched:=make(map[string]string)
go CrawlWithConcurrency(url,depth,fetcher,ch,urlsFetched)
for count>0 {
count+=<-ch
}
return
}

func CrawlWithConcurrency(url string, depth int, fetcher Fetcher, ch chan int,urlsFetched map[string]string) {
if depth<=0 {
ch <- -1
return
}
body, urls, err := fetcher.Fetch(url)
urlsFetched[url]="true"
if err != nil {
fmt.Println(err)
ch <- -1
return
}
fmt.Printf("found: %s %q\n", url, body)
for _,u:=range urls {
_,exists:=urlsFetched[u]
if exists {
continue
}
go CrawlWithConcurrency(u,depth-1,fetcher,ch,urlsFetched)
ch <- 1
}
ch <- -1
return
}
简单地说就是:

1、执行Crawl函数的主goroutine通过count来确保在所有的子goroutine都结束运行后再结束运行。
2、所有goroutine通过共享urlsFetched内存来确保抓取的页面不会重复。
go是按照CSP来实现并发的,提倡“通过通信来共享内存,而非通过共享内存来通信”的原则。
上面的代码通过共享内存urlsFetched来进行通信是违背CSP原则的,各goroutine都可以读取并修改urlFetched,这样的设计存在隐患,仍然有可能会重复抓取页面,所以共享内存的方式很危险。
为了遵守CSP原则,我修改了代码,修改后代码如下:

type Result struct {
depth int
urls []string
}

//主goroutine用于控制程序的结束
func Crawl(url string, depth int, fetcher Fetcher) {
ch:=make(chan *Result)
urlsFetched:=make(map[string]string)
count:=1
urlsFetched[url]="true"
go CrawlWorker(url,depth,fetcher,ch)
for count>0 {
result:=<-ch
//depth<=1则不crawlresult中的urls
if result.depth>1 {
for _,u:=range result.urls{
_,exists:=urlsFetched[u]
if exists {
continue
} else {
count++
urlsFetched[u]="true"
go CrawlWorker(u,result.depth-1,fetcher,ch)
}
}
}
count--
}
return
}

func CrawlWorker(url string, depth int, fetcher Fetcher,ch chan *Result) {
body, urls, err := fetcher.Fetch(url)
if err != nil {
fmt.Println(err)
ch<-&Result{depth,urls}
return
}
fmt.Printf("found: %s %q\n", url, body)
ch<-&Result{depth,urls}
}


修改后的代码主要的内容就是:

1、不再进行递归
2、只让主goroutine管理urlFetched内存
3、其他子goroutine传回运行结果给主goroutine,主goroutine再启动新的子goroutine
这样便遵守了CSP原则,并且更加安全了。
完整代码如下:

[code]package main
import (
"fmt"
)
type Fetcher interface {
// Fetch 返回 URL 的 body 内容,并且将在这个页面上找到的 URL 放到一个 slice 中。
Fetch(url string) (body string, urls []string, err error)
}
/*
// Crawl 使用 fetcher 从某个 URL 开始递归的爬取页面,直到达到最大深度。
func Crawl(url string, depth int, fetcher Fetcher) {
// TODO: 并行的抓取 URL。
// TODO: 不重复抓取页面。
ch:=make(chan int)
count:=1
urlsFetched:=make(map[string]string)
go CrawlWithConcurrency(url,depth,fetcher,ch,urlsFetched)
for count>0 {
count+=
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  go goroutine channel CSP