go http put请求_MIT 6.824 Lecture2和Go的GPM模型 - Go语言中文社区

go http put请求_MIT 6.824 Lecture2和Go的GPM模型


32af2a496561cffbfb33ab45bedaed66.png

Prerequisite for Lecture2

课程要求通过该网站熟悉GO语言,有其它语言基础熟悉一下还是很快的。恕我见识短浅,第一次见到Go的设计颠覆三观orz。

在课程之外,我也整理了一下Go的GPM模型,更有利于理解Go的从语言层面支持的多线程。

西瓜学习:Go的GPM多线程调度​zhuanlan.zhihu.com
fc0d30999ad77c444394c50ed7b07a75.png

使用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有两种内存分配函数,newmake。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请求之前的相应以回复。二是只保留一个时间段内的数据。
  • 第一个请求还在执行,而重复请求达到。此时还没有数据。
版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/weixin_30135925/article/details/112099487
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2021-05-16 04:42:59
  • 阅读 ( 782 )
  • 分类:Go

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢