MIT6.824 Lecture 2 RPC & Threads

2023-04-23

一、为什么使用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过程

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系统是最多一次。

评论