C++程序员的go学习之路(3)——goroutine、channel

goroutine

go中的并发主要靠协程(goroutine)。不同于C和C++中常用的多线程,协程并不与操作系统中的线程一一对应,操作系统是不知道有协程的存在的,协程间的调度由用户程序自己控制。go在runtime、系统调用等多方面对goroutine调度进行了封装处理,从语言层面支持了协程,这也是go的一大特色。只需要在函数前加一个go关键字就可以创建一个协程。

协程就是在应用层模拟的线程,降低了线程间切换的耗费。协程也是基于线程实现的,程序在内部维护一组数据结构和线程,真正执行的还是线程,而协程执行的代码被放进一个队列中,由工作线程从队列中拉出来执行。

go的协程是目前各类有协程概念的语言中实现得最完整和成熟的,十万个协程同时运行也毫无压力,虽然我们不会这么写代码。go对各种io函数进行了封装,当这些异步函数阻塞时go就会利用这个时机将现有的执行序列压栈,切换到另外一个协程。但是由于协程是非抢占式的调度,无法实现公平的任务调用,也无法直接利用多核优势,所以我们也不能直接说协程是比线程更高级的技术。

虽然在任务调度上,协程弱于线程,但资源消耗方面协程远远小于线程。一个线程的内存在MB级别,而协程只需要KB级别。我们可以把协程的基本特点归纳为:

  1. 协程调度机制无法实现公平调度
  2. 协程的资源开销非常低,一台普通的服务器就可以支持百万协程

在一个函数调用前加上go关键字,这次调用就会在一个新的goroutine中并发执行,当函数返回时该goroutine也自动结束,且返回值会被丢弃:

func Add(x, y int) {
  z := x+y
  fmt.Println(z)
}

func main() {
  Add(1, 1)
  go Add(1, 1)
}

上面的代码创建了一个协程,并在主函数与协程中打印值。但是运行一下我们会发现只有一个输出。因为在协程开始工作之前,主函数已经退出了。
为了阻止主函数过早退出,显然我们可以Sleep一下,但是这并不是一个优雅的解决方案。想想C++中的thread库为我们提供了join函数用来等待线程执行完毕,这里我们也需要一个东西来让主函数阻塞住,在go里可以用channel来达到这个目的。

channel

channel用于goroutine之间的通信,类似于管道。可以看做一个FIFO队列。它的操作符是箭头,<-
它的类型定义如下:

ChannelType = ("chan" | "chan" "<-" | "<-" "chan") ElementType
chan T  // 可以接收或发送类型为T的数据
chan<- float64  // 只能发送类型为float64的数据
<-chan int   // 只能接收类型为int的数据

如果没有指定方向就是双向,既可以接收,也可以发送。
应该在生产者处关闭channel,如果在消费者处关闭容易引起panic:往一个已经关闭的channel发送数据会panic,接收会返回0值,可以用一个额外的参数检查channel是否已经关闭:

x, ok := <- ch

使用make初始化channel,并且可以设置缓冲区容量。如果channel没有缓存,那么只有通信两端都准备好后才会开始通信,否则保持阻塞。如果设置了缓存,则缓存满时发送方才会阻塞,缓存空时接收方才会阻塞。
往一个nil channel发送或接收数据会一直阻塞。
可以在多个goroutine中读写同一个channel,不必加锁。

// 使用make建立一个channel
var channel chan int = make(chan int)
// 或者这样写也行
channel := make(chan int)
// 初始化并设置缓存容量
channel := make(chan int, 1024)

// 写channel,在有其他goroutine来读之前会保持阻塞
ch <- value
// 读channel,如果channel之前没有写入数据,也会一直阻塞直至有人写入数据
value := <-ch
value, ok := <-ch

可以通过类型转换将一个channel转换为单向的,从而限制对其的操作(类似于C++中对const的使用):

ch4 := make(chan, int)
ch5 := <-chan int(ch4)
ch6 := chan<- int(ch4)

channel与slice类似,也对应一个由make创建的底层数据结构的引用。当我们复制一个channel或者将channel作为函数参数时,我们只是拷贝了一个引用,它们指向的底层数据结构是一致的。
所以使用channel改写一下上面的那个例子,使协程可以正常执行:

var complete chan int = make(chan int)
func Add(x, y int) {
  z := x+y
  fmt.Println(z)
  complete <- 0 // 发个消息通知已经执行完了
}

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

推荐阅读更多精彩内容