课程主页: 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,它支持多种语言,提供了更复杂的特性,例如双向流和集成认证。