Go 语言之旅_Web 爬虫_并发思维方式

https://tour.go-zh.org/concurrency/10

代码来自MIT 6.824。学习这三种实现方式,理解Go下的并发思维方式

串行

func Serial(url string, fetcher Fetcher, fetched map[string]bool) {
	if fetched[url] {
		return
	}
	fetched[url] = true
	urls, err := fetcher.Fetch(url)
	if err != nil {
		return
	}
	for _, u := range urls {
		Serial(u, fetcher, fetched)
	}
}

清晰的dfs实现,fetched中记录访问过的url,遍历所有url

并发加锁

type fetchState struct {
	mu      sync.Mutex
	fetched map[string]bool
}

func ConcurrentMutex(url string, fetcher Fetcher, f *fetchState) {
	f.mu.Lock()
	already := f.fetched[url]
	f.fetched[url] = true
	f.mu.Unlock()

	if already {
		return
	}

	urls, err := fetcher.Fetch(url)
	if err != nil {
		return
	}
	var done sync.WaitGroup
	for _, u := range urls {
		done.Add(1)
		go func(u string) {
			ConcurrentMutex(u, fetcher, f)
			done.Done()
		}(u)
	}
	done.Wait()
	return
}

记录已访问的url这一步操作,线程之间存在竞争。多线程共享内存的并发编程模型,多个线程操作同一内存存在冲突,这里用互斥锁来保证并发安全
这里还有两个有意思的点,sync.WaitGroup用来等待任务执行完成;func(u string)这里参数u需要传入(值拷贝),而不可以引用一个上文中不断变化的u变量(引用拷贝)

并发通信

func worker(url string, ch chan []string, fetcher Fetcher) {
	urls, err := fetcher.Fetch(url)
	if err != nil {
		ch <- []string{}
	} else {
		ch <- urls
	}
}

func master(ch chan []string, fetcher Fetcher) {
	n := 1
	fetched := make(map[string]bool)
	for urls := range ch {
		for _, u := range urls {
			if fetched[u] == false {
				fetched[u] = true
				n += 1
				go worker(u, ch, fetcher)
			}
		}
		n -= 1
		if n == 0 {
			break
		}
	}
}

func ConcurrentChannel(url string, fetcher Fetcher) {
	ch := make(chan []string)
	go func() {
		ch <- []string{url}
	}()
	master(ch, fetcher)
}

通过channel将master和worker解耦,master从channel取出url进行判断,而worker负责遍历url并将结果写回channel。master和worker之间存在的交互只是对channel的读写
尽管channel内部也是通过加锁实现的,不过这是一种新的并发编程思维方式。由于语言本身提供了线程安全的channel,只需要考虑怎么和channel沟通
需要注意的是,读写channel默认会阻塞,需要考虑退出/超时机制

两种实现更多的是思维方式上的不同,需要用心体会。

更多,关于Go并发原理

posted @ 2021-12-05 17:09  柠檬水请加冰  阅读(71)  评论(0编辑  收藏  举报