MIT6.824 lec2 RPC&Thread


2.1 Go语言

  • 为什么要使用Go?
    • 多线程协同,以及垃圾回收都很重要。Go在多线程,??,线程同步做的很好,
    • Go有非常方便的RPC package,在C++中的RPC包非常的大且难用
    • Go是类型安全且内存安全的语言,在C++中要编写一个完美的程序,总是存在内存安全问题(比如double free,使用研已经释放的内存,使用完的vector未被释放),Go的GC能够很好的处理内存问题
    • C++可能因为一个拼写错误,编译器就会报出非常复杂的错误

2.2 多线程

  • 多线程是现实并发的重要工具,在分布式程序中,一个程序需要与多个计算机通信,需要一种简单的方式去现实它能够同时做多件不同的事情
  • 每个线程有自己的程序计数器,栈,一套寄存器,Go中,即便只有main函数在程序中,当首次启动程序的时候,它会在main函数中运行,它就是一个goroutine,然后做完所有事情。多线程运行程序的不同部分独立的执行不同的动作

I/O concurrency

  • 一个程序已经启动并且通过RPC请求网络上不同服务器,然后同时等待多个回复。这个问题的具体做法是为每一个RPC调用创建一个线程,每个线程都会通过RPC发送request消息,对应线程阻塞等待,当响应回复时,对应线程继续执行,使用多线程可以让我们同时发起多个网络请求,所有线程都会等待回复,不用同一时间去发送请求。不同的IO并发活动可能会有互相重叠的部分,其中一个线程正在等待,那么另一个线程可以继续执行

Parallelism

  • 多线程可以实现并行化,计算机可以使用多核的方式实现真正的并行计算。服务器能够采用多线程,很重要,因为一般有多个客户端去请求服务器

易用性

  • 存在这样的需求,你想要去在后台周期性的执行一些任务,但是却并不想在主线程中插入判断什么时候去执行这种任务。比如MapReduce想要周期性的给worker发送心跳包,检测这些worker是否宕机,所以可以创建一种线程定期检查,再睡眠1s。同时这些后台线程的消耗非常小,

异步编程(asynchronous)/事件驱动编程(event-drive)

  • 事件驱动编程一般是,存在一个线程,该线程中有一个循环,该循环等待输入或其他事件,这些事件可以触发程序继续执行,比如在服务器编程中,这个事件可以是来自客户端的请求,再比如,定时器到期,windows系统程序很多都是事件驱动的风格。所以在事件驱动程序中,这个线程一直循环,一直等待输入,比如当收到客户端的报文,主线程存在一个表格,它能够通过查询表格查看哪个客户端发送的这个报文,表格中记录了这个客户端到底处于什么样的活动状态,比如该客户端的状态是要去"读"某个文件的状态
  • 事件驱动编程相较于多线程来说,一般更快,(为什么更快?),但是难以获得多核性能,相较于多线程模型,比如在I/O并发中,对于C10K问题,建立1万个进程/线程,操作系统都是承受不住的。多线程还存在需要多个栈,多组寄存器,线程间切换需要消耗CPU时间,尤其是C10K场景下
  • 使用事件驱动编程,可以实现一个简单高性能的服务器

线程与进程

  • 对于goroutine而言,多个goroutine可以复用一个线程,也就是goroutine是用户态线程,那么对于goroutine的调度存在两种调度,也就是os先调度一个内核态线程,之后在这个线程中调度goroutine

2.3 多线程编程

  • 线程之间可以共享内存,存在race,所以需要实现线程同步与互斥
  • 线程间的协作(Coordination),go的channel就可以实现进程间的协作,或者sync.Cond,或者WaitGroup(用于启动已知数据的goroutine,再等待它们结束)

Crawler in the tour of go

  • 对于serial的爬虫,如果再Serial的dfs调用时,开启goroutine,那么在子goroutine没有执行完的时候,主goroutine就会结束
//
// Serial crawler
//

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

  • concurrent with mutex为每一个实现Fetcher接口的fetch创建一个goroutine,一通过互斥锁保护共享map(也就是f.fetc),来保证每个爬取过的页面不会被重复爬取,二通过WaitgGroup来保证每个子goroutine能够执行完成
  • 注意启动子goroutine的函数是个闭包,那么u需要靠形参传入,如果不传参,那么在启动了多个子goroutine,它们使用的一般都是for循环中最后一个url
  • 为了保证done.Done()一定会执行,那么需要defer,否则比如在除零错误,解引用空指针等情况,子goroutine崩溃,那么done.Done()不执行,则主goroutine就永远阻塞了
  • 为什么多个goroutine执行done.Done()不会有race,因为done.Done()内部存在加??释放??
  • 对于函数中的局部变量,比如函数返回了一个指向局部变量的指针,那么这个变量就会从栈区逃逸到堆区
//
// Concurrent crawler with shared state and Mutex
//

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
}

func makeState() *fetchState {
	f := &fetchState{}
	f.fetched = make(map[string]bool)
	return f
}
  • go语言内置了一个race探测器其基本原理是检测到一个线程写了一个地址,一段时间后,另一个线程又去读这个地址,这其中没有加??
  • CrawlerwithChannel,没有共享的map,通过channel去传递url数组,channel内部存在?? 。这里没有关闭channel,最后GC会回收channel。注意这里把初始的url放入channel的语句需要放在一个goroutine中,不然会因为channel的数据没人读,而一直阻塞
//
// Concurrent crawler with channels
//

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

func coordinator(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}
	}()
	coordinator(ch, fetcher)
}