Lecture 2: RPC and Threads #
Why Go #
-
多线程,线程间的锁和同步
-
RPC
-
类型安全和内存安全
-
垃圾回收,c++需要手动释放
-
语言简单
Thread #
每个线程都有自己的程序计数器,一组寄存器和一个栈
- I/O Concurrency
- Parallelism: Multi core
- Convenience
除了多线程,还能哪些并发执行的方式?事件驱动模型的异步编程(epoll,比如nginx)
进程和线程的区别?进程是操作系统提供的一种包含有独立地址空间的一种抽象,一个 Go 程序启动时作为一个进程,可以启动很多线程
Challenges #
- 如何处理共享数据?share memory
- race,lock
- coordination
- channel
- Sync.cond() Java Condition
- waitgorup()
- dealock
Crawler #
课程中 crawler 源码下载: cralwer.go
串行抓取 #
-
使用 DFS 获取所有网页,用一个 Map(变量fetcher)标记 url 是否被访问过
-
Serial(u, fetcher, fetched)
如果被改为go Serial(u, fetcher, fetched)
,将会启动一个新的 goroutine,但是主程序并不会等待该 goroutine 完成执行,因此会直接 return,新的 goroutine 输出可能会被打印在其他地方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) //go Serial(u, fetcher, fetched) } return }
用Mutex并发抓取 #
-
启动多个 goroutine 进行抓取,用一个 Map(变量fetcher)标记 url 是否被访问过
-
为了避免多个线程竞争(race),导致多次抓取同一个网页。在获取 fetched[url],并更新 fetched[url] 时,必须使用Mutex加锁
-
每个 goroutine 必须等待所有的子 url 的 goroutine 完成抓取之后,才可以return。因此使用了WaitGroup(类似Java的CountdownLatch),WaitGroup内部维护了一个计数器,在创建新的go线程时调用
Add(1)
加一,在线程完成退出后调用Done()
减一,调用Wait()
方法阻塞当前线程直到计数器变为0 -
如果 goroutine 异常退出没有调用
Done()
怎么办?可以使用defer
将其写在 goroutine 的开始 -
goroutine的匿名函数
func(u string)
方法定义里为什么要传入参数 u,直接使用 for 循环定义的 u 不行吗?- 如果匿名函数里直接使用 for 循环定义的 u 的话,所有的 goroutine 的 u 都指向同一个内存地址。当 for 遍历执行时,u 的内容就变化了,goroutine 里的值也就变化了,因此不能直接使用 for 循环定义的 u
- 可以在 for 循环里,定义一个新的变量
u2 := u
,然后 goroutine 里可以直接使用新的变量 u2,每个 goroutine 拿到的 u2 是指向不同内存地址的
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) u2 := u go func() { defer done.Done() ConcurrentMutex(u2, fetcher, f) }() //go func(u string) { // defer done.Done() // ConcurrentMutex(u, fetcher, f) //}(u) } done.Wait() return }
用Channel并发抓取 #
-
定义一个 channel 用来存储待爬取的 urls;
-
master 不断从 channel 中取出 urls,判断是否抓取过,然后启动新的 worker goroutine 去抓取
-
worker goroutine 抓取到给定的任务 url,并将解析出的结果 urls 塞回 channel
-
master 使用一个变量 n 来记录待处理的任务数;创建一个新的 worker goroutine 时加一;从 channel 中获取并处理完一份结果(即将其再安排给 worker 或者是空数组)减一;当所有任务都处理完时,退出程序
-
channel 是线程安全的,故当 master 消费,worker 写入时不会产生竞争问题
-
master 在 for 循环读取 channel 里的内容时,即使 channel 里没有数据,也会一直阻塞等待,直到 channel 里有新的数据产生,不会主动退出。因此在 for 循环读取 channel 之前,将初始 url 写入 channel 中,并将 n 置为 1,当
n == 0
时,退出循环。在退出循环后,由于 master/worker 都不会对 channel 存在引用,稍后 gc collector 会将其回收,因此 channel 不需要最后 closefunc 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) }