一、整体思路
这种方法几乎同步的地方都是使用channel来控制的(channel 的整个发送过程和接收过程都使用 runtime.mutex 进行加锁。runtime.mutex 是 runtime 相关源码中常用到的一个轻量级锁。整个过程并不是最高效的 lockfree 的做法。)。
Coordinator是master,负责任务的分配,开始创建Coordinator时,新建两个channel,代表map和reduce的任务队列,worker通过RPC任务从channel中取任务。另外用两个Fin channel,代表已完成的任务计数,通过统计Fin channel长度,来修改Coordinator状态,决定是发送map任务还是reduce任务。
本方法7个测试用例通过了5个,失败分析在另一篇文章中。
(注:channel是一种进程内的通信方式,不支持跨进程通信,所以我们下面的coordinator和worker之间访问channel都是通过RPC调用)
二、具体实现
1. Coordinator的实现
构造Coordinator和task结构体
初始化Coordinator的同时,初始化四个channel
在Coordinator中实现给worker分发task的RPC方法GetTask
和worker发送完成任务信号的RPC方法TaskFin,属性State表示Coordinator的状态,根据任务的完成情况修改State
2. worker的实现
worker的主要执行逻辑在下面方法中:通过获取任务返回的State判断当前任务是map还是reduce,分别进行处理
map任务:读取输入文件,根据插件中提供的mapf方法,解析成对应内容,根据哈希值分桶,每一个map任务(如id为i)生成nReduce个中间文件mr-i-[0~nReduce-1],一共numMap个任务(numMap=输入文件个数),共生成numMap*nReduce个中间文件。
reduce任务:每个reduce任务读取自己任务id(如j)对应的中间文件,读取mr-[0~numMap-1]-j共numMap个。根据插件中的reducef方法,执行后生成输出文件mr-out-j,最后将输出文件整合到同一个文件mr-wc-all中,与mr-correct-wc.txt对比,一样则通过。
(注:以上是根据第一个测试用例wc.so分析的)
func Worker(mapf func(string, string) []KeyValue,
reducef func(string, []string) string) {
// Your worker implementation here.
// 每一个worker循环执行以下逻辑
for {
// 先取任务
args := TaskRequest{}
reply := TaskResponse{}
CallGetTask(&args, &reply)
state := reply.State
//根据返回任务的不同状态,分别处理
if state == 0 {
id := strconv.Itoa(reply.XTask.IdMap)
fileName := reply.XTask.FileName
file, err := os.Open(fileName)
if err != nil {
log.Fatalf("cannot open mapTask %s", fileName)
}
content, err := ioutil.ReadAll(file)
if err != nil {
log.Fatalf("cannot read %s", fileName)
}
file.Close()
// 解析mrapp/wc.go中的map方法,将content字符串切分成key/value键值对数组,key为拆分出的每个关键字,value为1(这里fileName没什么用)
kva := mapf(fileName, string(content)) // 接下来要把kva写到中间文件中去
// 生成与reduce任务个数相同的中间文件
numReduce := reply.NumReduceTask
bucket := make([][]KeyValue, numReduce)
//接下来是论文Figure1的local write阶段, 即将读入的文件内容,按照hash值分类保存到bucket中
for _, kv := range kva {
num := ihash(kv.Key) % numReduce
bucket[num] = append(bucket[num], kv) // key的hash值为num的kv,分到了bucket[num]
}
//分完桶后根据reduce的个数写入临时文件tmpFile
for i := 0; i < numReduce; i++ {
tmpFile, error := ioutil.TempFile("", "mr-map-*")
if error != nil {
fmt.Printf("error is : %+v\n", error)
log.Fatal("cannot open map tmpFile")
}
//参考课程提示,用json向tmpFile中写bucket
//enc为 *json.Encoder格式,代表以json格式编码往tmpFile文件中写
enc := json.NewEncoder(tmpFile)
// 把bucket[i]的内容传递给enc
err := enc.Encode(bucket[i])
if err != nil {
log.Fatal("encod bucket error")
}
tmpFile.Close()
//根据课程提示,把中间文件rename
outFileName := `mr-` + id + `-` + strconv.Itoa(i)
os.Rename(tmpFile.Name(), outFileName)
}
//map任务完成后向MapTaskFin中发送一个true
CallTaskFin()
} else if state == 1 {
//否则就是reduce
//每一个reduce的任务取numMap个中间文件,取对应的最后一个数字为自己task id的中间文件,因为中间文件的名字最后一个数字是取哈希值得到的
numMap := reply.NumMapTask
id := strconv.Itoa(reply.XTask.IdReduce)
//说明所有的map任务完成,开始reduce
//kva保存从中间文件中读出的key value对
intermediate := []KeyValue{}
//reduce开始读中间文件
for i := 0; i < numMap; i++ {
mapFileName := "mr-" + strconv.Itoa(i) + "-" + id
// inputFile为 *os.File格式,读中间文件
inputFile, err := os.OpenFile(mapFileName, os.O_RDONLY, 0777)
if err != nil {
log.Fatalf("cannot open reduceTask %s\n", mapFileName)
}
// 将inputFile按照json格式解析
dec := json.NewDecoder(inputFile)
for {
var kv []KeyValue
if err := dec.Decode(&kv); err != nil {
break
}
//以上把中间文件中读出的键值对保存到了intermediate中
intermediate = append(intermediate, kv...)
}
//将intermediate强转为ByKey类型,排序
sort.Sort(ByKey(intermediate))
//准备整合,去重
//创建一个tmpFile,存放reduce输出结果
outFileName := "mr-out-" + id
tmpFile, err := ioutil.TempFile("", "mr-reduce-*")
if err != nil {
log.Fatalf("cannot open reduce tmpFile")
}
i := 0
for i < len(intermediate) {
j := i + 1
for j < len(intermediate) && intermediate[j].Key == intermediate[i].Key {
j++
}
//values累积了所有key相同的kv对的value值,单词例子中为很多个1
values := []string{}
for k := i; k < j; k++ {
values = append(values, intermediate[k].Value)
}
// reducef为插件解析的mrapp/wc.go中的reduce方法,返回values的数组长度,即values有多少个1(这里intermediate[i].Key没有用)
output := reducef(intermediate[i].Key, values)
//将每个key和对应的有多少个数按格式拼好,写入到tmpFile
fmt.Fprintf(tmpFile, "%v %v\n", intermediate[i].Key, output)
//再遍历下一批key相等的键值对
i = j
}
tmpFile.Close()
//改名字
os.Rename(tmpFile.Name(), outFileName)
}
CallTaskFin()
} else {
// state == 2的情况
break
}
}
}
3.RPC的实现
同时还要实现worker访问Coordinator的RPC方法,有两个,分别用来获取task和向Fintask channel中发送完成信号。
//参考下面的CallExample方法实现获取任务的方法
func CallGetTask(args *TaskRequest, reply *TaskResponse) {
ok := call("Coordinator.GetTask", &args, &reply)
if ok {
fmt.Printf("CallGetTask method - reply.FileName %s\n", reply.XTask.FileName)
} else {
fmt.Printf("CallGetTask method failed!\n")
}
}
func CallTaskFin() {
// 这两个参数没有用,但是call方法必须要给参数
args := ExampleArgs{}
reply := ExampleReply{}
ok := call("Coordinator.TaskFin", &args, &reply)
if ok {
fmt.Printf("CallTaskFin method ok\n")
} else {
fmt.Printf("CallTaskFin method failed\n")
}
}
RPC就是将worker请求任务需要的内容封装在response结构体中返回即可
type TaskRequest struct {
}
type TaskResponse struct {
XTask Task
NumMapTask int
NumReduceTask int
State int // 0 map 1 reduce 2 finish
}
这里的State对应的是Coordinator中的State,表示当前的阶段。
三、结果
运行结果是7个测试案例可以通过5个,reduce parallelism test失败, crush test失败。
分析失败原因见另一博客lab1(1)问题调试
(参考资料:go中channel的实现https://www.cyhone.com/articles/analysis-of-golang-channel/)