一、为什么使用Go
理论上,有很多编程语言可以用来做分布式编程,但是在6.824中选择Go有一些原因。
- Go对线程和RPC有很好的支持,这两点对于分布式编程非常重要
- Go有GC,垃圾收集器。如果做共享内存式的并发,多个线程共享一个结构体或变量,那么有垃圾回收器是很好的。
- Go是类型安全的(Type Safe)
- 比较简单,好学
- 是编译型语言,运行时开销不是很大
二、Threads
在Go中,线程称为Goroutine,使用go run 创建一个线程。
这些线程会共享内存,因为所有线程都在相同的地址空间中运行。
Go线程通常退出时隐性的,当函数执行结束时,就会退出。
Go会停止线程,当读取一个空channel或者写一个满channel时,会阻塞。
2.1 为什么要有线程?
并发:
- IO并发 当一个线程发起一个网络调用,在它等待回复的这段时间里,它会被阻塞,这个时候需要让其他线程来运行。
- 允许多核并行
- 方便
2.2 你可以创建多少线程?
你应该可以根据需要创建尽可能多的线程
2.3 挑战(使用线程进行编程)
- 竞争 两个线程对于共享变量的访问,可能会导致结果出现错误。
– 对于竞争的解决方案,一是避免共享,即通过channel来通信值,而不是直接共享内存;二是加锁,让一系列指令变为原子操作。
– Go有一个竞态检测器,虽然不能捕获所有可能的竞争,但它在识别竞争方面做的非常好。所以默认情况下,你应该在启用竞争检测器的情况下运行Go。 - 协调,即一个线程必须等待另一个线程,在完成某件事之前。
– 使用channel或者条件变量解决 - 死锁
2.4 Go如何解决这些挑战
channel & (locks & condition variables)
当没有线程持有引用时,垃圾回收器才会删除该对象
条件变量与锁相关联,例如:
var mu sync.Mutex
cond := sync.NewCond(&mu)
所以,你可以把条件变量看成两个不同线程之间的协调原语。
channel在Go中是线程安全的。
三、爬虫
爬虫是并发编程的一个更现实的例子。
实现一个IO并发的、对一个url只爬取一次、多核并行工作的爬虫
3.1 锁版本
#原语,用于跟踪你有多少活动线程,什么时候可以终止
vare done sync.WaitGroup
#每生成一个线程,加一
done.Add(1)
#而当线程结束时,Done()
done.Done()
#主线程等待所有线程结束
done.Wait()
四、RPC
即远程过程调用,RPC系统的目标是客户端可以远程调用服务端上的函数,将结果返回给客户端。
4.1 RPC过程
- 客户端调用函数并传入参数
- 它实际上是在调用一个称为stub的东西
- stub是本地函数,stub过程所做的是构建一个消息,表示哪个函数需要被调用,并给出函数的参数,参数类型,参数值等等
- 随后stub通过网络发送到服务器上对应的stub
- 服务器接收到消息,并将它反序列化(它用来将值从字节数组转换回值)
- 然后在服务器上的stub调用这个函数
- 函数返回到stub,stub序列化响应值,返回给客户端的stub
- 客户端stub反序列化返回值,将值返回给客户端
这两个stub使远程调用看起来像是常规过程调用
4.2 RPC失败的语义
如果服务器发生故障,客户端所做的事情
at-least-once至少一次意味着客户端将自动重试并继续,缺点是同一个操作可能会被多次执行,所以这并不适用于许多应用程序;at-most-once这个是常出现的,相应的服务器请求执行零次或一次,但不会超过一次,它实现的方式是通过过滤重复;exactly-once恰好一次,这是理想情况下,但是这很难。
Go的RPC系统是最多一次。