美文网首页程序员
Golang系列之Synchronization (四)

Golang系列之Synchronization (四)

作者: 谢培阳 | 来源:发表于2016-09-20 22:58 被阅读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
         }
    }
    

    原文转自谢培阳的博客

    相关文章

      网友评论

        本文标题:Golang系列之Synchronization (四)

        本文链接:https://www.haomeiwen.com/subject/mlnoettx.html