Go 语言之旅_Web 爬虫_并发思维方式
代码来自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默认会阻塞,需要考虑退出/超时机制
两种实现更多的是思维方式上的不同,需要用心体会。