Prerequisite for Lecture2
课程要求通过该网站熟悉GO语言,有其它语言基础熟悉一下还是很快的。恕我见识短浅,第一次见到Go的设计颠覆三观orz。
在课程之外,我也整理了一下Go的GPM模型,更有利于理解Go的从语言层面支持的多线程。
西瓜学习:Go的GPM多线程调度zhuanlan.zhihu.com使用Go的理由
- Go有垃圾回收且类型安全(自动确定最后一个线程何时结束一个对象的使用)
- Go优秀的多线程模型(goroutine, 从语言层面实现并发),和很好的RPC包用于分布式系统
- 简单,就是简单
Easy to understand.
Easy to use.
Easy to reason about.
为什么要多线程
- I/O 并发,当一个线程在I/O等待时,其它线程任可执行
- 并行运行,计算是多核心设计的
- 方便在后台实现监视进程(在之后的Lab中会多次使用)
多线程编程的挑战
- 共享数据。线程间资源的竞争
加锁
- 线程协作
同步。即Go中可以使用信道chan, sync.Cond, wairGroup实现同步。chan即信道就像是linux中的pipe。
- 死锁
死锁是很常见的。RPC,Go channels都会存在。
爬虫
以更加“Go”的方式写一个网络爬虫,即通过Go中的信道Channel实现。
func worker(url string, ch chan []string, fetcher Fetcher) {
urls, err := fetcher.Fetch(url)
if err != nil { // 爬取失败,返回错误
ch <- []string{}
} else {
ch <- urls // fetch的url传入ch信道
}
}
func master(ch chan []string, fetcher Fetcher) { // 创建worker从而fetch网页
n := 1
fetched := make(map[string]bool) // map映射,记录某一个url是否被爬取过,此处并需要给map加锁,因为它并没有被共享,是master私有的
for urls := range ch { // 信道中非空则不断循环,*fetch到的url也换进入信道ch中*
for _, u := range urls {
if fetched[u] == false {
fetched[u] = true
n += 1
go worker(u, ch, fetcher) // gorountine调用worker
}
}
n -= 1
if n == 0 { // 爬虫完成
break
}
}
}
func ConcurrentChannel(url string, fetcher Fetcher) {
ch := make(chan []string) // 创建信道
go func() { // 闭包函数
ch <- []string{url} // 发送url到master中,开启循环
}()
master(ch, fetcher)
}
- channel同时完成了通信和同步,不同的线程可以通过一个channel进行发送和接受。同时,channel的代价是很小的。
RPC(Remote Procedure Call, 远程过程调用)
PS:为什么这部分没讲就下课了orz
课堂笔记
RPC是实现分布式系统的关键机制,会被反复的使用。RPC的目标是更方便于C/S通信的编程,允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。RPC隐藏了底层的网络协议,将数据转变为“wire format”,即编码解码。
总的来说,RPC在客户端需要完成序列化,按格式编码,数据传输。服务器端刚好相反。
// 为Client和Server定义相应的Args和return结构体
const ( // 定义常量
OK = "OK"
ErrNoKey = "ErrNoKey"
)
type Err string // 错误信息
type PutArgs struct { // put操作传入参数
Key string
Value string
}
type PutReply struct { // put操作返回
Err Err
}
type GetArgs struct { // get操作传入参数
Key string
}
type GetReply struct { // get操作返回
Err Err
Value string
}
// Client端
func connect() *rpc.Client {
client, err := rpc.Dial("tcp", ":1234") // 创建TCP连接,server的1234端口
if err != nil { // 创建失败
log.Fatal("dialing:", err)
}
return client
}
func get(key string) string {
client := connect() // 获得连接
args := GetArgs{key} // 设置传入参数结构体
reply := GetReply{} // 设置返回参数结构体
err := client.Call("KV.Get", &args, &reply) // 远程调用
if err != nil {
log.Fatal("error:", err)
}
client.Close() // 关闭
return reply.Value
}
func put(key string, val string) {
client := connect()
args := PutArgs{key, val}
reply := PutReply{}
err := client.Call("KV.Put", &args, &reply) // 远程调用
if err != nil {
log.Fatal("error:", err)
}
client.Close()
}
// Server端
type KV struct {
mu sync.Mutex // 互斥锁
data map[string]string // 简单的映射表,模拟数据库
}
func server() {
kv := new(KV) // 分配内存,初始化为0
kv.data = map[string]string{}
rpcs := rpc.NewServer() // 注册RPC服务
rpcs.Register(kv) // 注册kv为调用对象
l, e := net.Listen("tcp", ":1234") // 监听1234端口
if e != nil {
log.Fatal("listen error:", e)
}
go func() { // goroutine执行
for { // 循环,并发处理client请求
conn, err := l.Accept()
if err == nil {
go rpcs.ServeConn(conn) // 另开线程,避免该线程阻塞。调用ServeConn实现解码,利用反射机制找到对应的函数执行后编码返回
} else {
break
}
}
l.Close()
}()
}
func (kv *KV) Get(args *GetArgs, reply *GetReply) error { // map为共享,所以必须传入指针
kv.mu.Lock()
defer kv.mu.Unlock() // 离开函数后释放,保证锁会被释放
val, ok := kv.data[args.Key] // 常规处理,查找key
if ok {
reply.Err = OK
reply.Value = val
} else {
reply.Err = ErrNoKey
reply.Value = ""
}
return nil
}
func (kv *KV) Put(args *PutArgs, reply *PutReply) error {
kv.mu.Lock()
defer kv.mu.Unlock()
kv.data[args.Key] = args.Value // 插入键值对
reply.Err = OK
return nil
}
func main() {
server()
put("subject", "6.824")
fmt.Printf("Put(subject, 6.824) donen")
fmt.Printf("get(subject) -> %sn", get("subject"))
}
- 注意到的是该demo调用了Go中的net/rpc包,该包无法跨语言传输,只支持Go对应的编解码方式。
- Go有两种内存分配函数,new和make。new(T)为每个类型分配内存并初始化为0,返回* T,即指针,适用于数组,结构体。new(T)返回一个T的初始化,只适用于内建引用类型切片,map和channel。
- ServeConn是实现远程调用服务端的核心。其得到一个连接后,内部调用ServeCodec解码,再调用call调用客户端请求的函数。
“at most once” RPC应有的行为
服务器中的RPC代码会检查处重复的请求,对于重复的请求,直接返回之前执行的结果,而不是重复执行。
最naive的想法是对每个请求设置一个独一无二的ID,如果是相同的请求,ID相同。但存在几个问题:
- 不同的用户拥有了相同的ID值。可以给ID值绑定client相关的独一无二的身份,如IP值。
- 能保存的ID是有限的,过去的ID对应的返回值迟早被丢弃。一是保证每个RPC请求之前的相应以回复。二是只保留一个时间段内的数据。
- 第一个请求还在执行,而重复请求达到。此时还没有旧数据。