课程主页: https://pdos.csail.mit.edu/6.824/schedule.html
这节课主要是对分布式整体的概念和Go的一些相关介绍, 包括线程、RPC、同步原语、管道等,相对简单, 简单记录一下, 顺带附上我进行Lab1实验时相关知识的补充
1 同步原语
课程举了一个投票的例子:
1 | package main |
1.1 锁
整体而言go中锁的语法和C/C++区别不大, 但这里介绍一下defer:
defer语句后面跟着的是一个函数调用,这个调用不会立即执行。而是延迟到包含它的函数执行完毕时才执行- 如果有多个
defer语句,它们的调用顺序是后进先出的,即最后一个defer的函数调用将会第一个被执行 - 重点:
defer的调用不会在每次循环结束时执行,而是会在包围 defer 的函数返回时才执行 => 如果有锁需要释放的话, 需要在循环体内手动释放?
1.2 条件变量
还是相同的例子:
1 | package main |
没啥好讲的…
条件变量的三个主要方法是:
Wait:调用这个方法会阻塞调用协程,直到其他协程在相同的条件变量上调用 Signal 或 Broadcast。Signal:唤醒等待该条件变量的一个协程(如果存在的话)。Broadcast:唤醒等待该条件变量的所有协程。
1.3 WaitGroup协程同步
sync.WaitGroup 用于等待一组协程的完成。一个 WaitGroup 等待一系列的事件,主要的用法包括三个方法:
Add方法: 在启动协程之前,使用Add方法来设置要等待的事件数量。通常这个值设置为即将启动的协程的数量。Done方法: 当协程的工作完成时,调用Done方法。Done方法减少WaitGroup的内部计数器,通常等价于Add(-1)。Wait方法: 使用 Wait 方法来阻塞,直到所有的事件都已通过调用Done方法来报告完成。
1 | // 假设我们有三个并行的任务需要执行 |
注意:
- 不要复制
WaitGroup。如果需要将 WaitGroup 传递给函数,应使用指针。 - 避免在协程内部调用
Add方法,因为这可能会导致计数器不准确。最好在启动协程之前添加所需的计数。 - 使用
Done方法是减少WaitGroup计数器的推荐方式,它等价于Add(-1)。
2 通道
2.1 案例
还是投票…go的通道可以实现无锁的并发访问, 核心在于其保证通道写入在不同协程间不会冲突
1 | package main |
2.2 带缓冲的通道
另外, 通道还支持带缓冲:
1 | bufferedCh := make(chan int, 2) // 缓冲大小为2 |
两种通道区别如下:
- 不带缓冲的通道在发送操作和接收操作之间进行同步:发送会阻塞,直到有另一个协程来接收数据;接收会阻塞,直到有另一个协程发送数据。
- 带缓冲的通道有一个固定大小的缓冲区。发送操作只在缓冲区满时阻塞,接收操作只在缓冲区空时阻塞。
2.3 SELECT和通道
select允许协程在多个通道操作上等待。select 会阻塞,直到其中一个通道操作可以执行:
1 | select { |
3 Context控制上下文
3.1 Context 接口
Context 类型用于创建和操纵上下文的函数,用于定义截止日期、取消信号以及其他请求范围的值的接口。它设计用来传递给请求范围的数据、取消信号和截止时间到不同的协程中,以便于管理它们的生命周期。先来看Context 接口:Context 接口定义了四个方法:
Deadline:返回Context被取消的时间,也就是完成工作的截止时间(如果有的话)。Done:返回一个Channel,这个Channel会在当前工作应当被取消时关闭。Err:返回Context结束的原因,它只会在Done返回的Channel被关闭后返回非空值。Value:从Context中检索键对应的值。
3.2 操纵上下文的函数
context 包提供了几个用于创建和操纵上下文的函数:
context.Background:返回一个空的Context。这个Context通常被用作整个程序或请求的顶层Context。context.TODO:不确定应该使用哪个Context或者还没有可用的Context时,使用这个函数。这在编写初始化代码或者不确定要使用什么上下文时特别有用。context.WithCancel:创建一个新的Context,这个Context会包含一个取消函数,可用于取消这个Context及其子树。context.WithDeadline:创建一个新的Context,这个Context会在指定的时间到达时自动取消。context.WithTimeout:创建一个新的Context,这个Context会在指定的时间段后自动取消。
3.3 案例
3.3.1 简单案例
1 | package main |
这个案例中, 将ctx显式传递给在子协程, 使其可以受外部的协程控制。
3.3.2 复杂案例socks5代理
这里给出一个字节跳动后端青训营实现的socks5代理中对context 的使用, 完整代码看这里:
1 | ctx, cancel := context.WithCancel(context.Background()) |
任务需求:
有两个 goroutine 分别用于从客户端读取数据并写入目的端,以及从目的端读取数据并写入客户端, 要求一旦有一个方向的拷贝操作出现错误, 将另一个操作也取消
- 方案:使用
WithCancel和<-ctx.Done(): - 问题?我第一次看到这个代码时, 有这样的问题
ctx并没有被显式地传递给2个goroutine, 2个goroutine调用cancel取消的是WithCancel返回的ctx, 而不是自己, 所以这为什么能工作? - 答案:
cancel函数与ctx相关联,而cancel被闭包捕获并在多个goroutine中使用。这就是为什么调用cancel()会影响所有这些 goroutine 的原因,不管ctx是否被显式传递。这种行为是context包设计的一部分,允许协调不同goroutine之间的取消事件。cancel() 被调用时会取消ctx上下文,而与这个ctx相关联的所有操作(在这个例子中是两个io.Copy调用)都会接收到取消通知,即使它们在不同的goroutine中执行,且ctx没有显式地传递给它们。
3.4 注意事项
- 不应该把
Context存储在结构体中,它应该通过参数传递。 Context是协程安全的,你可以把一个Context传递给多个协程,每个协程都可以安全地读取和监听它。- 一旦一个
Context被取消,它的所有子Context都会被取消。 Context的Value方法应该被用于传递请求范围的数据,而不是函数的可选参数。
Context 在处理跨越多个协程的取消信号、超时以及传递请求范围数据时起到了关键作用,是 Go 并发编程中的重要组件。
4 RPC
这本来是在
Lab 1过程中补的知识, 但介于这里写了这么多Go, 就放在一起了
Go 标准库中的 net/rpc 包提供了创建 RPC 客户端和服务器的机制。
RPC 允许客户端程序调用在服务器上运行的程序的函数或方法,就好像它是本地可用的一样。客户端和服务器之间的通信通常是透明的,客户端程序仅需知道需要调用的远程方法和必须提供的参数。
net/rpc 包使用了 Go 的编码和解码接口,允许使用 encoding/gob 包来序列化和反序列化数据(尽管也可以使用其他编码,如 JSON)。RPC 调用默认是通过 TCP 或者 UNIX 套接字传输的,但是你可以实现自己的传输方式。
4.1 服务器端
要创建一个 Go RPC 服务器,你需要创建一些方法,这些方法必须满足以下条件:
- 方法是导出的(首字母大写)。
Lab1 被坑惨了 - 方法有两个参数,都是导出类型或内建类型。
- 方法的第二个参数是指针, 相当于写出。
- 方法返回一个
error类型。
然后,将这个类型的实例注册为 RPC 服务:
1 | type Arith int |
4.2 客户端
客户端需要使用 rpc.Dial 函数连接到 RPC 服务器,然后可以通过 Call 方法进行同步调用或 Go 方法进行异步调用:
1 | client, err := rpc.Dial("tcp", "server address") |
4.3 缺点
net/rpc 包的文档提到,该包已经被标记为“冻结”(frozen)并不推荐使用。这意味着该包不会有新的发展,尽管它仍然是可用的。因此,应该考虑使用更现代的解决方案,如 gRPC,它支持多种语言,提供了更复杂的特性,例如双向流和集成认证。