程序员

Golang系列之Synchronization (四)

2016-09-20  本文已影响377人  谢培阳

如何实现多线程之间的通信,是并发模型里面最需要被考虑到的问题。golang为此引进了channel,channel可作为goroutine之间交流的通道。每一个channel都可读可写,且都是阻塞的,也即,当一个goroutine读一个channel的时候,就被阻塞住了,直到另一个goroutine向这个channel写入信息。这个特性也常被用于synchronization。

声明一个channel ,类型为int。
c := make(chan int)
向channel写入值
c <- 1
读取channel的值并用于初始化a
a := <- c
另外有带缓冲的channel:
buff := make(chan int, 10)
向buff写入数据不会阻塞,但当写满10个时,就会被阻塞住,直到buff的数据被读取,可视为一个带长度限制的队列。

channel的用处简单明了,任何需要线程之间交换传递数据的地方都可以用到channel。下面一个简单的例子,最能说明channel的简单和强大。

想象一个场景,若干只老鼠依次排开,最右边的老鼠向它左边的老鼠说一句话,左边的老鼠听到后,又传给它左边的老鼠,直到最左边的老鼠知道了这句话。这个过程可以用下面的代码描述。函数gopher代表一只老鼠,负责将右边听到的信息传给左边。在main函数的循环里面,定了次数1000000,也就是有一百万只老鼠参加了这个游戏,每次循环都make出新的channel,最后,向最右边的channel写入’i am hungry’(也就是left,因为最后left = right),并打印出最左边(mostleft)收到的信息。那么,完成整个过程需要多久呢?在我的虚拟机里面(4g内存,单核),一百万只老鼠花费的时间是12秒,下面的代码编译后直接可运行的,读者可以试着调下循环的次数并观察goroutine的增加对运行时间和机器的影响。

package main
import (
     "time"
     "fmt"
)

func main() {
     tbeg := time.Now()
     mostleft := make(chan string)
     left := mostleft
     for i := 0;i < 1000000;i++ {
         right := make(chan string)
         go gopher(left, right)
          left = right
     }
     left <- "i am hungry"
     fmt.Println(<- mostleft)
     cost := time.Now().Sub(tbeg)
     fmt.Println(“cost: “, cost)
}

func gopher(left, right chan string) {
     left <- <- right   //所有的gopher均会被阻塞住直到最右边收到消息
}

由上面的例子可以看出,channel非常适合消息传递的场合,然而,golang被广为流传的有一句话说到:Do not communicate by sharing memory; instead, share memory by communicating.所以,channel应当替代Mutex???

我认为,这句话最多只能算做golang宣传的口号,并不能当成实践真理。并发模型的通信机制无非两种,共享内存和消息传递。channel只是作为消息传递的一种实现,并不能说它就比共享内存的做法更先进或者简洁。channel更合适数据传递收发的场景,mutex则适合共享数据读取的场景。

func (t *Worker)loop(c chan string) {
     for {
         select {
             case s := <- c:
                   t.doSomething(s)
             case <- t.stop:
                   break
         }
     }
}

上面的例子,守护函数从channel s 或者channel t.stop里面获取消息,select语句用于从多个channel里面选取其一,当某个channel准备好的时候,就跳到那个对应的case。loop函数倾听着两个数据来源,收到数据后进行处理,当t.stop收到消息时,则退出。这种数据传递收发的场景,用起channel来就简洁明了,如果是Mutex的话,则要不断加锁->判断数据是否准备好->解锁->sleep等待,很是麻烦。

而在另外的场景中,则用Mutex最好不过了

// goroutine 1
func update() {
     lock.Lock()
     html = XXX
     lock.Unlock()
}
// goroutine 2
func handler(r Request, w writer) {
     lock.Lock()
     w.write(html)
     lock.Unlock()
}

handler是一个网页访问的处理接口,当收到一个请求的时候,负责返回html,而html的内容会时而更新,由update函数进行处理。这种场景下,用锁是再好不过了,这时候非要share memory by communicating的话也不是不可以,只是会平添复杂的同步逻辑,读者不妨尝试一下,嘻嘻。

讲道理,golang对channel和mutex都支持得很好,所以无谓去争论哪个更好,哪种用起来简洁就用哪种,没必要拘束于其中之一。毕竟白帽黑猫,能最快抓到老鼠的就是更好的猫。

前些日子,一个没注意在项目里弄了一个bug,自己检查检查不出来,最后才知道该加锁的地方忘记加锁了。Don’t be clever,是The Go Memory Model里给的忠告。下面这段代码就是引发bug的地方,在这里列出代码逻辑,既说明下golang语法上一些特性,毕竟talk is cheap,show me the code : )。也借此寻求下读者的意见,是否有更好的方法来重构这段代码,交流交流

// 需求:有规则集合R,数据库D,每一条规则r需到D拿数据,并判断此条规则是否已经符合。
// 要求:因为R量比较大,且经常变化,需要程序作为daemon循环的跑,实时性要求比较高,所以每跑一次耗费的时间不能太长。
// 最简单的处理办法就是,从R一条条取出规则,然后一条条到D拿数据比对,逻辑非常简单,
// 但是,这样,每一万条规则耗费的时间 > 10 min。不符合要求。
// 所以需要将规则聚合,将相似规则聚合成一条查询sql到D取数据,然后返回。可是聚合怎么聚合呢?
// 每一条规则都有很多属性,如果在主逻辑里面进行聚合,将会使代码不清晰,且若以后增加规则属性,
// 整个规则分类逻辑都要改。所以最好主逻辑还是一条条拿规则,一条条取数据进行判定,这样代码会清晰很多。
// 在这种方法下,每一万条处理时间 < 4s

//被循环调用的函数,主逻辑
func Work() {
     result := []Result{}
     wait := sync.WaitGroup{}
     for r := range R {
          wait.Add(1)
          //异步IO,输入一条规则,返回对应的数据,然后在callback里面进行判断是否规则已符合
          //NodeJS借鉴来的其实,想一条进,一条出,而又想按规则聚合到数据库拿数据,只想到这种方法了。
          Select(r,func(r Rule, d Data){
              result = append(result, r.Judge(d)) //这里没加锁会导致race conditions
              wait.Done()
          })
     }
     Do()
     wait.Wait()  //等待所有callback都被执行了
     doSomething(result)
}

//Select的实现封装,这里可以一条条处理,也可以聚合后再处理,已经对外隐藏了。
func Select(r Rule, f Callback) {
     count++
     Class.add(r)    //按规则属性组合哈希值进行聚合。这里再怎么复杂都没关系了。
     go func() {
         <- done     //必须等待Do()函数被执行,这样getData()才能拿到数据。
         f(r, getData(r))
     }
}

func Do() {
     //do dirty work
     //按聚合的规则到D拿数据
     ...
     ...
     //通知所有Select调用执行callback
     for i := 0;i < count;i++ {
          done <- true
     }
}

原文转自谢培阳的博客

上一篇下一篇

猜你喜欢

热点阅读