Go中的goroutine和channel使用

u012761711 · · 1657 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

作为一个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原则,并且更加安全了。

完整代码如下:



有疑问加站长微信联系(非本文作者)

本文来自:CSDN博客

感谢作者:u012761711

查看原文:Go中的goroutine和channel使用

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1657 次点击  
加入收藏 微博
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传