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