Golang系列之Synchronization (四)

如何实现多线程之间的通信,是并发模型里面最需要被考虑到的问题。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
     }
}

原文转自谢培阳的博客

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270

推荐阅读更多精彩内容

  • 能力模型 选择题 [primary] 下面属于关键字的是()A. funcB. defC. structD. cl...
    _张晓龙_阅读 24,641评论 14 224
  • 控制并发有三种种经典的方式,一种是通过channel通知实现并发控制 一种是WaitGroup,另外一种就是Con...
    wiseAaron阅读 10,593评论 4 34
  • Goroutines 模型:和其他goroutine在共享的地址空间中并发执行的函数 资源消耗: 初始时非常小的栈...
    大漠狼道阅读 1,239评论 0 8
  • 今天介绍一下 go语言的并发机制以及它所使用的CSP并发模型 CSP并发模型 CSP模型是上个世纪七十年代提出的,...
    falm阅读 68,157评论 10 80
  • go语言的并发机制以及它所使用的CSP并发模型 CSP并发模型CSP模型是上个世纪七十年代提出的,用于描述两个独立...
    seven_son阅读 2,594评论 0 5