年后回来第一篇!老规矩,先上Github
学习Go语言也很有一段时间了。这个东西从年前就开始构思,这两天终于研究着搞出来了。算是对于goroutine相关的一个练习吧。
###框架概述
框架的入口为MapReduce容器 MRContainer。使用流程如下:
- 初始化一个MRContainer,指定map与reduce线程数参数。
- 指定map与reduce执行方法。
- 输入数据。
- 调用Start函数启动服务。
- 通过GetResult函数获取结果集。
框架会根据配置开启指定数目的map与reduce协程,然后轮询这些协程以分配任务。最终将结果放在一个list存储的结果集中。
###实现细节
####容器内容 既然要做MapReduce,首先要分析程序中包含哪些东西。
首先map与reduce方法自然是必不可少的。由于协程有框架来进行调度,两个方法声明如下:
Mapper func(data interface{})MapperedDataEntry
Reducer func(in MapperedDataSet) interface{}
如此声明在实现上限定了mapreduce的操作方式。若是一个map方法需要输出多个映射结果,可以选择将输出channel传入Mapper方法以供使用。
容器中包括输入,中转和输出三个数据缓存区。用来保存输入,map结果与reduce结果。
用于goroutine之间通讯的channel也需要保存起来,由于一个goroutine有多个channel需要管理,声明了一个结构体用来持有这些channel。保存于一个list用于轮询。
####工作流程
map与reduce的协程启动起来基本都是一样的。所以就直接放在一起说了。
根据go的推荐用法,在goroutine之间使用channel进行通信。又由于channel操作是阻塞的,若是某个任务执行时间很长,会影响轮询,或者说轮询也就基本失去了意义。(必须等待任务执行完成才能继续)
为了解决这个问题,每个工作协程附带着启动了一个心跳协程,在轮询时通过select同时监听输出和心跳协程。心跳协程会每隔一段时间返回当前工作协程的状态。若收到输出或者工作协程状态为空闲,则会给工作协程一个新的任务。否则跳过继续轮询。
启动时,首先启动指定数目的工作协程,并将持有的channel保存到容器中,下面以map协程为例:
//map协程方法
func (container *MRContainer) mapWrapper (in <-chan interface{}, out chan<- MapperedDataEntry, system chan int,cb chan int){
mapper := container.Mapper
workState := SIGN_BEEP_FREE // 当前工作状态,若为0,则可以输入,否则正忙
beepChan := make(chan int)
go beep(&workState,cb,beepChan) // 启动心跳
for {
shutdownFlag := false
select {
case src,ok := <-in:
if !ok {
fmt.Print("not ok !")
}
workState = SIGN_BEEP_WORKING
entry := mapper(src)
workState = SIGN_BEEP_FREE
out<-entry
case command,ok := <-system:
if !ok {
fmt.Print("not ok !")
}
fmt.Println("recive shutdown sign :", strconv.Itoa(command))
if(command == SIGN_CTRL_SHUTDOWN){
shutdownFlag = true
}
}
if(shutdownFlag){
break;
}
}
fmt.Println("goroutines has been finish")
}
此时工作协程处于空闲状态,等待接收数据。然后就要在容器协程启动轮询开始分发数据,同样以map操作为例:
func (container *MRContainer) startMap (){
fmt.Println("container doing map work")
holder := container.holdMapChans.Front();
inbuffer := container.inBuffer
for {
fail2Stop := false // 判断是否可以停止map工作,当缓存清空且所有工作均已经完成即可
if chans,ok := holder.Value.(mapChanSet);ok{
select {
case entry,ok :=<-chans.out:
if !ok {
fmt.Print("not ok !")
}
container.midBuffer.put(entry.key,entry.value)
e := inbuffer.pull()
if e!=nil {
chans.in <- e
fail2Stop = true //还有人没完成工作呢,哼哼
}
case state,ok := <-chans.beep:
if !ok {
fmt.Print("not ok !")
}
if state==0 {
e := inbuffer.pull()
if e!=nil {
chans.in <- e
fail2Stop = true //还有人没完成工作呢,哼哼
}else{
chans.system <- SIGN_CTRL_SHUTDOWN // 当前已经没有更多数据,且此协程已经完成工作,发出关闭信号
}
}else{
fail2Stop = true // 尝试停止服务失败,还有协程没有完成任务
}
}
}
if(!fail2Stop){
break;
}
//维持轮询
ele := holder.Next()
if ele==nil{
holder = container.holdMapChans.Front()
}
}
}
###问题总结
在开发过程当中当然会遇到很多问题,从中也吸取了很多教训。
#####Go中的类型校验:
与java不同,在Java中,不明真相的类型之间可以互相进行转换,转换失败才会报错。在Go中未经过类型校验直接转换是会报错的。应当也可以算是某种强制的编程规范吧。做法如下:
if ele,ok:=e.Value.(MapperedDataSet);ok{
chans.in <- ele
fail2Stop = true //还有人没完成工作呢,哼哼
}
#####channel的阻塞
在编码中最初遇到的问题之一就是由于channel阻塞导致各种死锁崩溃。go能够自动判断死锁并结束程序还是非常好的。但是出问题就很纠结了。
很多种情况会导致崩溃,不过我遇到的问题基本就是由于一个channel进行了多次读取。在多线程的程序中这种问题真的很隐蔽,找了很久才找明白原因。换用单项channel可以很有效的避免这种问题。
#####指针和值的使用
一开始的时候在容器中直接使用变量来保存数据,后来发现有些数据的修改一直都不管用,直到最后换用指针方式才成功执行。
这是一个很大的话题,还需要仔细研究,估计之后还可以再单发一篇简书出来。
###总结
不得不说用Go来编写多线程的程序真的是太方便了,比起java来说不知道高明到哪里去了。线程之间的通信等也十分简洁。不需要过多啰嗦的代码就可以很流畅的实现通信。
通过这个框架的实现,算是比较全面的练习了一下go语言的核心功能:goroutine,以及相关的技术。同时也算装了一下MapReduce的B。原本神秘的技术其实最简单的实现起来原理并不困难。
新的一年,从此开始!