Lecture 2

Lecture 2: RPC and Threads #

Why Go #

  1. 多线程,线程间的锁和同步

  2. RPC

  3. 类型安全和内存安全

  4. 垃圾回收,c++需要手动释放

  5. 语言简单

Thread #

每个线程都有自己的程序计数器,一组寄存器和一个栈

  1. I/O Concurrency
  2. Parallelism: Multi core
  3. Convenience

除了多线程,还能哪些并发执行的方式?事件驱动模型的异步编程(epoll,比如nginx)

进程和线程的区别?进程是操作系统提供的一种包含有独立地址空间的一种抽象,一个 Go 程序启动时作为一个进程,可以启动很多线程

Challenges #

  1. 如何处理共享数据?share memory
  2. race,lock
  3. coordination
  4. channel
  5. Sync.cond() Java Condition
  6. waitgorup()
  7. 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 不需要最后 close

    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)
    }