Hi, there! 这是一份根据 MIT 6.824(2021) 课程的第 2 课的课堂示例代码改编的 2 个 go 语言编程练习。像其他的编程作业一样,我去除了核心部分,保留了代码框架,并编写了每一步的提示
练习代码在本文的最后面
爬虫
在第一部分,你需要实现 3 个版本的网络爬虫。
1 单线程爬虫
首先,请为 fakeFetcher
类型实现 Fetcher
接口中的 Fetch()
方法。然后实现串行爬虫 Serial()
函数(递归),并在 main()
中调用它,预期的输出如下:
bash
=== Serial===
found: http://golang.org/
found: http://golang.org/pkg/
missing: http://golang.org/cmd/
found: http://golang.org/pkg/fmt/
found: http://golang.org/pkg/os/
2 多线程爬虫(使用锁同步)
我们定义了 fetchState
类型,用于对 fetched
加锁保护,但是还未实现它的"构造函数"。请先实现它的"构造函数",名为 makeState()
。注意对于结构体,一般返回其指针
然后实现 ConcurrentMutex
,实现一个通过锁控制的并发爬虫。提示:
sync.WaitGroup
(等待组)是Go语言标准库中的一个并发原语,用于等待一组 goroutine 完成执行,提供了三个主要方法:
- Add(delta int):增加等待组的计数器。delta 参数表示要添加到计数器的值,通常为 1
- Done():减少等待组的计数器。相当于 Add(-1)
- Wait():阻塞调用它的 goroutine,直到等待组的计数器归零
等待组的计数器是一个非负整数,初始值为 0。当计数器的值变为 0 时,意味着所有的 goroutine 都已经完成,Wait() 方法会解除阻塞并继续执行后续代码
最后,在 main
中调用 ConcurrentMutex
,预期的输出如下:
bash
=== Serial===
found: http://golang.org/
found: http://golang.org/pkg/
missing: http://golang.org/cmd/
found: http://golang.org/pkg/fmt/
found: http://golang.org/pkg/os/
=== ConcurrentMutex ===
found: http://golang.org/
missing: http://golang.org/cmd/
found: http://golang.org/pkg/
found: http://golang.org/pkg/os/
found: http://golang.org/pkg/fmt/
3 多线程爬虫(使用channel同步)
练习代码中已经提供了 ConcurrentChannel
函数,你无需改动它,只需要实现 worker
和 master
函数即可
go
// Concurrent crawler with channels
func ConcurrentChannel(url string, fetcher Fetcher) {
ch := make(chan []string)
go func() {
ch <- []string{url}
}()
master(ch, fetcher)
}
最后,在 main
中调用 ConcurrentChannel
函数,预期的输出如下:
bash
=== Serial===
found: http://golang.org/
found: http://golang.org/pkg/
missing: http://golang.org/cmd/
found: http://golang.org/pkg/fmt/
found: http://golang.org/pkg/os/
=== ConcurrentMutex ===
found: http://golang.org/
missing: http://golang.org/cmd/
found: http://golang.org/pkg/
found: http://golang.org/pkg/os/
found: http://golang.org/pkg/fmt/
=== ConcurrentChannel ===
found: http://golang.org/
missing: http://golang.org/cmd/
found: http://golang.org/pkg/
found: http://golang.org/pkg/os/
found: http://golang.org/pkg/fmt/
kv 存储
在第二部分,你需要实现一个基于 RPC 的 KV 存储服务
首先你需要为 *KV
实现 Get
和 Put
方法,它们都返回 error
类型。然后补全 get
函数和 put
函数,使得它们能够在 main
中正常工作
提示:你可以通过下面 3 行代码调用 server 中已经注册的 KV.Get
服务:
go
client := connect()
err := client.Call("KV.Get", &args, &reply)
client.Close()
完成后,预期的输出如下:
bash
Put(subject, 6.824) done
get(subject) -> 6.824
练习代码
go
// crawler-exercise.go
package main
import (
"sync"
)
// Serial crawler
func Serial(url string, fetcher Fetcher, fetched map[string]bool) {
// TODO 1
}
// Concurrent crawler with shared state and Mutex
type fetchState struct {
mu sync.Mutex
fetched map[string]bool
}
// TODO 2: implement fetchState's constructor
func ConcurrentMutex(url string, fetcher Fetcher, f *fetchState) {
// TODO 2
}
func worker(url string, ch chan []string, fetcher Fetcher) {
// TODO 3
}
func master(ch chan []string, fetcher Fetcher) {
// TODO 3
}
// Concurrent crawler with channels
func ConcurrentChannel(url string, fetcher Fetcher) {
ch := make(chan []string)
go func() {
ch <- []string{url}
}()
master(ch, fetcher)
}
func main() {
// uncomment them step by step
/*
fmt.Printf("=== Serial===\n")
Serial("http://golang.org/", fetcher, make(map[string]bool))
fmt.Printf("=== ConcurrentMutex ===\n")
ConcurrentMutex("http://golang.org/", fetcher, makeState())
fmt.Printf("=== ConcurrentChannel ===\n")
ConcurrentChannel("http://golang.org/", fetcher)
*/
}
// Fetcher
type Fetcher interface {
// Fetch returns a slice of URLs found on the page.
Fetch(url string) (urls []string, err error)
}
// fakeFetcher is Fetcher that returns canned results.
type fakeFetcher map[string]*fakeResult
type fakeResult struct {
body string
urls []string
}
// TODO 1: implement Fetch for fakeFetch
// fetcher is a populated fakeFetcher.
var fetcher = fakeFetcher{
"http://golang.org/": &fakeResult{
"The Go Programming Language",
[]string{
"http://golang.org/pkg/",
"http://golang.org/cmd/",
},
},
"http://golang.org/pkg/": &fakeResult{
"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{
"Package fmt",
[]string{
"http://golang.org/",
"http://golang.org/pkg/",
},
},
"http://golang.org/pkg/os/": &fakeResult{
"Package os",
[]string{
"http://golang.org/",
"http://golang.org/pkg/",
},
},
}
go
// kv-exercise.go
package main
import (
"fmt"
"log"
"net"
"net/rpc"
"sync"
)
//
// Common 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
}
func connect() *rpc.Client {
client, err := rpc.Dial("tcp", ":1234")
if err != nil {
log.Fatal("dialing:", err)
}
return client
}
func get(key string) string {
// TODO 2
return ""
}
func put(key string, val string) {
// TODO 2
}
type KV struct {
mu sync.Mutex
data map[string]string
}
// TODO 1: implement `Get` method for *KV
// TODO 1: implement `Put` method for *KV
func 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 main() {
server()
put("subject", "6.824")
fmt.Printf("Put(subject, 6.824) done\n")
fmt.Printf("get(subject) -> %s\n", get("subject"))
}