本节内容:Lect 2   RPC and Threads


线程:Threads allow one program to (logically) execute many things at once.
The threads share memory. However, each thread includes some per-thread state: program counter, registers, stack.

下面以go语言写一个爬虫作为例子来介绍线程:

Go example: crawler.go

package main
import (
"fmt"
"sync"
)
// Several solutions to the crawler exercise from the Go tutorial: https://tour.golang.org/concurrency/10 type fakeResult struct {
body string
urls []string
} // fakeFetcher is Fetcher that returns canned results.
type fakeFetcher map[string]*fakeResult // fetcher is a populated fakeFetcher.
var fetcher = fakeFetcher{
"http://golang.org/": &fakeResult{
"Title: The Go Programming Language",
[]string{
"http://golang.org/pkg/",
"http://golang.org/cmd/",
},
},
"http://golang.org/pkg/": &fakeResult{
"Title: Packages",
[]string{
"http://golang.org/",
"http://golang.org/cmd/",
"http://golang.org/pkg/fmt/",
"http://golang.org/pkg/os/",
},
},
"http://golang.org/pkg/fmt/": &fakeResult{
"Title: Package fmt",
[]string{
"http://golang.org/",
"http://golang.org/pkg/",
},
},
"http://golang.org/pkg/os/": &fakeResult{
"Title: Package os",
[]string{
"http://golang.org/",
"http://golang.org/pkg/",
},
},
} type Fetcher interface {
Fetch(urlstring string) (urllist []string, err error)
// Fetch(urlstring) method returns a slice of URLs found on the page.
} func (f fakeFetcher) Fetch(urlstring string) ([]string, error) {
if res, ok := f[urlstring]; ok {
//https://tour.golang.org/flowcontrol/6
fmt.Printf("found: %s\n", urlstring)
return res.urls, nil
}
fmt.Printf("missing: %s\n", urlstring)
return nil, fmt.Errorf("not found: %s", urlstring)
} // ###### Serial crawler ###### func Serial(url string, fetcher Fetcher, fetched map[string]bool) {
if fetched[url] {
return
}
fetched[url] = true
urls, err := fetcher.Fetch(url)
if err != nil {
return
}
for _, u := range urls {
Serial(u, fetcher, fetched)
}
return
} // ###### Concurrent crawler with shared state and Mutex ###### func makeState() *fetchState {
f := &fetchState{}
f.fetched = make(map[string]bool)
return f
} type fetchState struct {
mu sync.Mutex
fetched map[string]bool
} func ConcurrentMutex(url string, fetcher Fetcher, f *fetchState) {
f.mu.Lock()
if f.fetched[url] {
f.mu.Unlock()
return
}
f.fetched[url] = true
f.mu.Unlock() urls, err := fetcher.Fetch(url)
if err != nil {
return
}
var done sync.WaitGroup
for _, u := range urls {
done.Add(1)
go func(u string) {
defer done.Done()
ConcurrentMutex(u, fetcher, f)
}(u)
}
done.Wait()
return
} // ###### Concurrent crawler with channels ###### func worker(url string, ch chan []string, fetcher Fetcher) {
urls, err := fetcher.Fetch(url)
if err != nil {
ch <- []string{}
} else {
ch <- urls
}
} func master(ch chan []string, fetcher Fetcher) {
n := 1
fetched := make(map[string]bool)
for urls := range ch {
for _, u := range urls {
if fetched[u] == false {
fetched[u] = true
n += 1
go worker(u, ch, fetcher)
}
}
n -= 1
if n == 0 {
break
}
}
} func ConcurrentChannel(url string, fetcher Fetcher) {
ch := make(chan []string)
go func() {
ch <- []string{url}
}()
master(ch, fetcher)
} // ###### main ###### func main() {
fmt.Printf("=== Serial===\n")
Serial("http://golang.org/", fetcher, make(map[string]bool)) //Serial version of crawler fmt.Printf("=== ConcurrentMutex ===\n")
ConcurrentMutex("http://golang.org/", fetcher, makeState()) fmt.Printf("=== ConcurrentChannel ===\n")
ConcurrentChannel("http://golang.org/", fetcher)
}

为了简便起见,这其实只是一个假的爬虫......并没有涉及网络访问,它的作用就是在fetcher中建立一个string->fakeResult类型的hash table,表示每个网页上的链接列表,并通过爬虫函数读取它们。为了演示go语言的并发,代码中实现了三种函数:Serial,ConcurrentMutex,ConcurrentChannel

在这段代码中,首先定义了一个接口Fetcher(go中接口的概念和java相似),其中有一个方法Fetch,用于在fetcher中返回urlstring所对应的链接列表。和java不一样,go语言中方法和函数不是一个概念:方法是面向对象中的概念。go中方法和函数最大的区别就是方法带有一个接收器(Fetch()中的f fakeFetcher参数),表示调用f对象的Fetch()方法(用法即some_obj_f.Fetch(url),这样就可以自动适配不同对象的同名方法;而函数是面向过程中的概念,函数只有输入参数和输出参数,和对象无关。

在58行这里的if有个神奇的用法,参考 https://tour.golang.org/flowcontrol/6

接下来我们先来看serial的版本。它的输入参数包括根域名url,fetcher(前面提到过的hash table),和一个bool数组fetched(用来记录哪些网站被访问过了)。注意163行这里有个神奇的用法make(),参考https://www.jianshu.com/p/f01841004810。  serial函数本身比较简单,就不赘述了,基本思路就是对fetcher中的每个域名,递归抓取它下面的链接(在fakeResult里面)。

第二个版本是ConcurrentMutex

  • 注意它的输入参数fetchState,里面除了bool数组之外还多了一个互斥锁mu。它的原理就是用来给共享变量fetched加锁,保证在多线程爬虫时,每次只有一个线程能访问fetched变量。当mu已经被锁上时,任何试图访问它的线程都会阻塞在mu.Lock()处,直到mu被释放掉才能往下进行(可以理解为二元信号量的wait操作)。而对于每个域名下面的链接,会再启动一个ConcurrentMutex线程来抓取,而不是单纯的递归,这样就实现了多线程。  
  • 另外110行有一个var done sync.WaitGroup,这个是用来确定何时结束爬虫的,WaitGroup 对象内部有一个计数器,最初从0开始,它有三个方法:Add(), Done(), Wait() 用来控制计数器的数量。Add(n) 把计数器设置为n ,Done() 每次把计数器-1 ,wait() 会阻塞代码的运行,直到计数器地值减为0 (可以理解为counting semaphore)[参考https://studygolang.com/articles/12972?fr=sidebar]。  
  • 注意113-116行有一个匿名函go func,这个是go中的协程(go routine),作用有点像c里面的fork(),可以理解为新开了一个线程。函数里面的语句会在一个新建的go routine里执行。这样就实现了并发访问多个url
  • 114行有个defer done.Done(),defer关键字的含义是:defer后面的函数在defer语句所在的函数执行结束的时候会被调用。这里也就是func函数运行结束后(ConcurrentMutex之后)把计数器-1。       另外用defer和不用defer的一大不同点就是,defer后面紧跟的函数值和函数参数会立即被求值(函数体会立即执行),但函数不会立即调用(不会立即被return),本例中还看不出来这一点,可以参考https://www.cnblogs.com/racaljk/p/8150726.htmlhttps://www.cnblogs.com/racaljk/p/8150726.html
  • 这个版本虽然实现了一定程度上的并发,但是对fetched的访问仍然是serial的。如果其中发生了很多的race,那么整体速度就被拖慢了。

第三个版本是ConcurrentChannel,这个例子中用了Go channel。这部分可以参考https://www.cnblogs.com/pdev/p/11286349.html

When to use sharing and locks, versus channels?

  • Most problems can be solved in either style
  • What makes the most sense depends on how the programmer thinks
    • state -- sharing and locks
    • communication -- channels
    • waiting for events -- channels
  • Use Go's race detector:
    • https://golang.org/doc/articles/race_detector.html
    • go test -race

RPC

基本概念5105都学过了.....这里来看看用go语言如何实现吧。

在5105课上讲过Reliable RPC的概念,讲的是如果在server-client之间如果传输出了故障该怎么办。

17_reliable_comm
. Reliable RPC: client-server

. Server failure( client 不知道 server 啥时候挂的,是 operation 执行前还是执行后)
Sol: 分三种 operation semantics:
Exactly once(保证操作恰好执行一次): impossible to achieve
At least once(至少执行过一次): retry
At most once(执行过 次或 次): send request only once . Client failure( client 已经挂了。 server 没必要再执行了,浪费资源)
Sol: Extermination: log at client stub and explicitly kill orphans
/ Reincarnation: Divide time into epochs between failures and delete computations from old epochs.
/ Expiration: give each RPC a fixed quantum T. Explicitly request extensions.

At least once适用于以下场景:If it's OK to repeat operations (e.g. read-only op), or if application has its own plan for coping w/ duplicates (which you will need for Lab 1)

at most once的思路是,server RPC code could detect duplicate requests, and returns previous reply instead of re-running the handler(RPC function).  在Lab2中就会用到这个方法。

Q: how to detect a duplicate request?
  A: client includes unique ID (XID) when sending each request, and uses the same XID for re-send
      server:
      if seen[xid]:
          r = old[xid]
      else
          r = handler()
          old[xid] = r
          seen[xid] = true
但是at most once也有个问题:如果server挂了,导致seen[]丢失了,那么server就不知道哪个xid曾经接收过了。

exactly once需要在at most once的基础上增加容错协议。这个会在Lab3中用到。

Go RPC is "at-most-once"
  STEP1  open TCP connection
  STEP2  write request to TCP connection
  STEP3  TCP may retransmit, but server's TCP will filter out duplicates

There is no retry in Go code (i.e. will NOT create 2nd TCP connection)
    Go RPC code returns an error if it doesn't get a reply, when
          perhaps after a timeout (from TCP)
          perhaps server didn't see request
          perhaps server processed request but server/net failed before reply came back

下面以go语言写的简易key-value storage为例:

Go example: kv.go

package main
import (
"fmt"
"log"
"net"
"net/rpc"
"sync"
) // RPC request/reply definitions const (
OK = "OK"
ErrNoKey = "ErrNoKey"
) type Err string type PutArgs struct {
Key string
Value string
} type PutReply struct {
Err Err
} type GetArgs struct {
Key string
} type GetReply struct {
Err Err
Value string
} // Client ------------------------------------------------------- func connect() *rpc.Client { //建立与server的连接
client, err := rpc.Dial("tcp", "127.0.0.1:1234")
if err != nil {
log.Fatal("dialing:", err)
}
return client
} func get(key string) string {
client := connect()
args := GetArgs{"subject"}
reply := GetReply{}
err := client.Call("KV.Get", &args, &reply) //rpc调用server上的函数
if err != nil {
log.Fatal("error:", err)
}
client.Close() //关闭连接
return reply.Value
} func put(key string, val string) {
client := connect()
args := PutArgs{"subject", "6.824"}
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() { //建立server
kv := new(KV)
kv.data = map[string]string{}
rpcs := rpc.NewServer()
rpcs.Register(kv)
l, e := net.Listen("tcp", ":1234")
if e != nil {
log.Fatal("listen error:", e)
}
go func() {
for {
conn, err := l.Accept()
if err == nil {
go rpcs.ServeConn(conn)
} else {
break
}
}
l.Close()
}()
} func (kv *KV) Get(args *GetArgs, reply *GetReply) error {
kv.mu.Lock()
defer kv.mu.Unlock() val, ok := kv.data[args.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
} // main ------------------------------------------------------- func main() {
server() put("subject", "6.824")
fmt.Printf("Put(subject, 6.824) done\n")
fmt.Printf("get(subject) -> %s\n", get("subject"))
}

逻辑还是比较简单的...比java thrift简洁多了。


Ref:

https://golang.org/doc/effective_go.html

https://golang.org/pkg/net/rpc/

https://tour.golang.org/concurrency/10

https://www.cnblogs.com/pdev/p/10936485.html

MIT 6.824学习笔记2 RPC/Thread的更多相关文章

  1. MIT 6.824学习笔记4 Lab1

    现在我们准备做第一个作业Lab1啦 wjk大神也在做6.824,可以参考大神的笔记https://github.com/zzzyyyxxxmmm/MIT6824_Distribute_System P ...

  2. MIT 6.824学习笔记3 Go语言并发解析

    之前看过一个go语言并发的介绍:https://www.cnblogs.com/pdev/p/10936485.html   但这个太简略啦.下面看点深入的 还记得https://www.cnblog ...

  3. MIT 6.824学习笔记1 MapReduce

    本节内容:Lect 1 MapReduce框架的执行过程: master分发任务,把map任务和reduce任务分发下去 map worker读取输入,进行map计算写入本地临时文件 map任务完成通 ...

  4. RESTful源码学习笔记之RPC和 RESTful 什么区别

    REST,即Representational State Transfer的缩写.翻译过来是表现层状态转换.如果一个架构符合REST原则,就称它为RESTful架构.啥叫json-rpc?接口调用通常 ...

  5. RESTful源码学习笔记之RPC和Restful深入理解

    以下资料搜集自网络 0x00 RPC RPC 即远程过程调用(Remote Procedure Call Protocol,简称RPC),像调用本地服务(方法)一样调用服务器的服务(方法).通常的实现 ...

  6. rabbitMQ学习笔记(七) RPC 远程过程调用

    关于RPC的介绍请参考百度百科里的关于RPC的介绍:http://baike.baidu.com/view/32726.htm#sub32726 现在来看看Rabbitmq中RPC吧!RPC的工作示意 ...

  7. Swoft2.x 小白学习笔记 (四) --- RPC

    介绍 swoft 中 RPC使用:搭建访问服务端和客户端 RPC服务端: 一.配置,在文件 /app/bean.php中添加 return [ 'rpcServer' => [ 'class' ...

  8. RabbitMQ学习笔记(六) RPC

    什么RPC? 这一段是从度娘摘抄的. RPC(Remote Procedure Call Protocol)——远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的 ...

  9. Hadoop源码学习笔记(4) ——Socket到RPC调用

    Hadoop源码学习笔记(4) ——Socket到RPC调用 Hadoop是一个分布式程序,分布在多台机器上运行,事必会涉及到网络编程.那这里如何让网络编程变得简单.透明的呢? 网络编程中,首先我们要 ...

随机推荐

  1. Tensorflow学习笔记3:卷积神经网络实现手写字符识别

    # -*- coding:utf-8 -*- import tensorflow as tf from tensorflow.examples.tutorials.mnist import input ...

  2. 【转】SIP协议 会话发起协议

    转自:https://www.cnblogs.com/gardenofhu/p/7299963.html 会话发起协议(SIP)是VoIP技术中最常用的协议之一.它是一种应用层协议,与其他应用层协议协 ...

  3. [NOI2015]程序自动分析(并查集,离散化)

    [NOI2015]程序自动分析 Description 在实现程序自动分析的过程中,常常需要判定一些约束条件是否能被同时满足. 考虑一个约束满足问题的简化版本:假设x1,x2,x3,-代表程序中出现的 ...

  4. nginx http正向代理简单配置及systemd 配置

    #user nobody; worker_processes 1; #error_log logs/error.log; #error_log logs/error.log notice; #erro ...

  5. Java 静态方法、私有方法、常量的使用

    1.静态方法 2.私有方法 3.常量 4.接口小结

  6. Solr从数据库导入数据(DIH)

    一. 数据导入(DataImportHandler-DIH) DIH 是solr 提供的一种针对数据库.xml/HTTP.富文本对象导入到solr 索引库的工具包.这里只针对数据库做介绍. A.准备以 ...

  7. 前端小姐姐学PHP之(一)

    作为一个前端不懂后台那是不对的,嘻嘻,来走一波... 一.安装 **我这里用的是phpStudy和phpStrom** 1.安装phpStudy 链接:https://pan.baidu.com/s/ ...

  8. pycharm的一个bug,pycharm 在debug时,会运行项目下的所有文件,而不是当前文件

    pycharm的一个bug,pycharm 在debug时,会运行项目下的所有文件,而不是当前文件

  9. 方法返回前面有if - else if - else ,最终返回值是?

    -(NSString *)testA{ int a = ?; ) { return @"a大于5"; } ) { return @"a不大于5"; } retu ...

  10. AndroidStudio3.4+Unity2018.3,导出JAR包给UNITY使用

    环境 Android studio 3.4 + unity2018.3 1,android studio 新建空工程,一切默认,完成.这个空工程只是个壳,它的所有参数都没什么用,它存在的意义是为了后面 ...