本文转载于http://hustcat.github.io/channel/

“网络,并发”是Go语言的两大feature。Go语言号称“互联网的C语言”,与使用传统的C语言相比,写一个Server所使用的代码更少,也更简单。写一个Server除了网络,另外就是并发,相对python等其它语言,Go对并发支持使得它有更好的性能。

Goroutine和channel是Go在“并发”方面两个核心feature。

Channel是goroutine之间进行通信的一种方式,它与Unix中的管道类似。

Channel声明:

ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .

例如:

var ch chan int
var ch1 chan<- int  //ch1只能写
var ch2 <-chan int  //ch2只能读

channel是类型相关的,也就是一个channel只能传递一种类型。例如,上面的ch只能传递int。

在go语言中,有4种引用类型:slice,map,channel,interface。

Slice,map,channel一般都通过make进行初始化:

ci := make(chan int)            // unbuffered channel of integers
cj := make(chan int, 0)         // unbuffered channel of integers
cs := make(chan *os.File, 100)  // buffered channel of pointers to Files

创建channel时可以提供一个可选的整型参数,用于设置该channel的缓冲区大小。该值缺省为0,用来构建默认的“无缓冲channel”,也称为“同步channel”。

Channel作为goroutine间的一种通信机制,与操作系统的其它通信机制类似,一般有两个目的:同步,或者传递消息。

同步

c := make(chan int)  // Allocate a channel.
// Start the sort in a goroutine; when it completes, signal on the channel.
go func() {list.Sort()c <- 1  // Send a signal; value does not matter.
}()
doSomethingForAWhile()
<-c   // Wait for sort to finigo; discard sent value.

上面的示例中,在子goroutine中进行排序操作,主goroutine可以做一些别的事情,然后等待子goroutine完成排序。

接收方会一直阻塞直到有数据到来。如果channel是无缓冲的,发送方会一直阻塞直到接收方将数据取出。如果channel带有缓冲区,发送方会一直阻塞直到数据被拷贝到缓冲区;如果缓冲区已满,则发送方只能在接收方取走数据后才能从阻塞状态恢复。

消息传递

我们来模拟一下经典的生产者-消费者模型。

func Producer (queue chan<- int){for i:= 0; i < 10; i++ {queue <- i}
}func Consumer( queue <-chan int){for i :=0; i < 10; i++{v := <- queuefmt.Println("receive:", v)}
}func main(){queue := make(chan int, 1)go Producer(queue)go Consumer(queue)time.Sleep(1e9) //让Producer与Consumer完成
}

上面的示例在Producer中生成数据,在Consumer中处理数据。

Server编程模型

在server编程,一种常用的模型:主线程接收请求,然后将请求分发给工作线程,工作线程完成请求处理。用go来实现,如下:

func handle(r *Request) {process(r)  // May take a long time.
}func Serve(queue chan *Request) {for {req := <-queuego handle(req)  // Don't wait for handle to finigo.}
}

一般来说,server的处理能力不是无限的,所以,有必要限制线程(或者goroutine)的数量。在C/C++编程中,我们一般通过信号量来实现,在go中,我们可以通过channel达到同样的效果:

var sem = make(chan int, MaxOutstanding)func handle(r *Request) {sem <- 1    // Wait for active queue to drain.process(r)  // May take a long time.<-sem       // Done; enable next request to run.
}func Serve(queue chan *Request) {for {req := <-queuego handle(req)  // Don't wait for handle to finigo.}
}

我们通过引入sem channel,限制了同时最多只有MaxOutstanding个goroutine运行。但是,上面的做法,只是限制了运行的goroutine的数量,并没有限制goroutine的生成数量。如果请求到来的速度过快,会导致产生大量的goroutine,这会导致系统资源消耗完全。

为此,我们有必要限制goroutine的创建数量:

func Serve(queue chan *Request) {for req := range queue {sem <- 1go func() {process(req) // Buggy; see explanation below.<-sem}()}
}

上面的代码看似简单清晰,但在go中,却有一个问题。Go语言中的循环变量每次迭代中是重用的,更直接的说就是req在所有的子goroutine中是共享的,从变量的作用域角度来说,变量req对于所有的goroutine,是全局的。

这个问题属于语言实现的范畴,在C语言中,你不应该将一个局部变量传递给另外一个线程去处理。有很多解决方法,这里有一个讨论。从个人角度来说,我更倾向下面这种方式:

func Serve(queue chan *Request) {for req := range queue {sem <- 1go func(r *Request) {process(r)<-sem}(req)}
}

至少,这样的代码不会让一个go的初学者不会迷糊,另外,从变量的作用域角度,也更符合常理一些。

在实际的C/C++编程中,我们倾向于工作线程在一开始就创建好,而且线程的数量也是固定的。在go中,我们也可以这样做:

func handle(queue chan *Request) {for r := range queue {process(r)}
}func Serve(clientRequests chan *Request, quit chan bool) {// Start handlersfor i := 0; i < MaxOutstanding; i++ {go handle(clientRequests)}<-quit  // Wait to be told to exit.
}

开始就启动固定数量的handle goroutine,每个goroutine都直接从channel中读取请求。这种写法比较简单,但是不知道有没有“惊群”问题?有待后续分析goroutine的实现。

传递channel的channel

channel作为go语言的一种原生类型,自然可以通过channel进行传递。通过channel传递channel,可以非常简单优美的解决一些实际中的问题。

在上一节中,我们主goroutine通过channel将请求传递给工作goroutine。同样,我们也可以通过channel将处理结果返回给主goroutine。

主goroutine:

type Request struct {args        []intresultChan  chan int
}request := &Request{[]int{3, 4, 5}, make(chan int)}
// Send request
clientRequests <- request
// Wait for response.
fmt.Printf("answer: %d\n", <-request.resultChan)

主goroutine将请求发给request channel,然后等待result channel。子goroutine完成处理后,将结果写到result channel。

func handle(queue chan *Request) {for req := range queue {result := do_something()req.resultChan <- result}
}

多个channel

在实际编程中,经常会遇到在一个goroutine中处理多个channel的情况。我们不可能阻塞在两个channel,这时就该select场了。与C语言中的select可以监控多个fd一样,go语言中select可以等待多个channel。

    c1 := make(chan string)c2 := make(chan string)go func() {time.Sleep(time.Second * 1)c1 <- "one"}()go func() {time.Sleep(time.Second * 2)c2 <- "two"}()for i := 0; i < 2; i++ {select {case msg1 := <-c1:fmt.Println("received", msg1)case msg2 := <-c2:fmt.Println("received", msg2)}}

在C中,我们一般都会传一个超时时间给select函数,go语言中的select没有该参数。

select语句

select语句可以用于多个channel的读或者写。它与switch语句比较类似,只不过select只用于channel。 如果有多个channel可以处理,那么select随机选择一个channel处理:

for {  // send random sequence of bits to cselect {case c <- 0:  // note: no statement, no fallthrough, no folding of casescase c <- 1:}
}

如果所有channel都不能处理,如果有default语句,则执行default,如果没有default,则会阻塞,直到有channel可以处理。一个处理nil channel,没有default的select会永远阻塞。这常用于daemon程序。

select {}  // block forever

考虑如下代码:

package main
import "fmt"
func main(){fmt.Println("start")select{}
}

上面的代码会返回下面的错误:

$ go run select1_ex.go
start
fatal error: all goroutines are asleep - deadlock!

需要改成下面这种方式:

package main
import "time"
import "fmt"
func main(){fmt.Println("start")go func(){for {time.Sleep(time.Second * 1)fmt.Println("do some work")}}()select{}
}

参考golang spec: Select statements。

超时

由于select本身并不支持超时,我们需要额外的手段来模拟超时:

timeout := make(chan bool, 1)
go func() {time.Sleep(1 * time.Second)timeout <- true
}()select {case <-ch:// a read from ch has occurred
case <-timeout:// the read from ch has timed out
}

我们可以通过一个单独的timeout channel和goroutine来实现超时机制。

相关资料

  • Go Concurrency Patterns: Timing out, moving on
  • effective go
  • 深入讨论channel timeout
  • Golang channels tutorial
  • golang的select典型用法

Go语言学习:Channel相关推荐

  1. Go语言学习资料整理

    整理网上找到的Golang语言学习资料 基础 基础教程 书籍在线版 Go 指南-A Tour of Go Go语言圣经(中文版) Effective Go中文版 Go Web编程 build-web- ...

  2. Go 语言学习总结(4)—— 为什么说 Golang 是面向未来的语言?

    前言 Golang 是最年轻的编程语言之一,于 2007 年设计,由 Ken Thompson(UNIX 和 C 的设计者和创建者).Rob Pike(UTF 8 和 UNIX 格式的共同创建者)和 ...

  3. go get 拉取指定版本_go语言学习笔记-基础知识-3

    相关文档 go语言学习笔记-目录 1.简介 1.1 什么是GO Go 是一个开源的编程语言,它能让构造简单.可靠且高效的软件变得容易.Go是从2007年末由Robert Griesemer, Rob ...

  4. Go语言学习之路(二)

    Go语言学习之路(二) 面对对象编程思想 抽象 封装 继承 接口 文件 命令行参数 Json 序列化 反序列化(unmarshal) 单元测试 Redis Redis简介 Redis基本使用 Go连接 ...

  5. 【Go语言 · 学习笔记】

    文章目录 Go语言 · 学习笔记 一.Go包管理 1. 什么是Go语言中的包 2. 包的命名 3. main包 4. 导入包 5. 远程包导入 6. 命名导入 7. 包的init函数 二.Go开发工具 ...

  6. Go语言学习1-基础入门

    1. Go语言环境搭建及基础知识 Go语言官方网站(http://golang.org) 代码包文档网站(http://godoc.org) Go语言中文网(http://studygolang.co ...

  7. (一)Go语言学习笔记

    Go语言学习笔记 1 前言 2 写Go语言需要注意的地方 2.1 Go语言编译执行和直接run的区别 2.2 Go语言的特点 2.3 Linux下配置Go环境变量 2.4 随记 3 go_code 3 ...

  8. Go语言学习之map

    Go语言学习之map 1.map的基本介绍 map是 key-value数据结构,又称为字段或者关联数组,类似JAVA的集合 在编程中经常使用到 2.map的声明 1.语法: var map 变量名 ...

  9. 深度解密Go语言之channel

    大家好啊!"深度解密 Go 语言"系列好久未见,我们今天讲 channel,预祝阅读愉快!在开始正文之前,我们先说些题外话. 上一篇关于 Go 语言的文章讲 Go 程序的整个编码. ...

  10. golang-阅读雨痕大神的Go语言学习笔记的心得

    golang-阅读雨痕大神的Go语言学习笔记的心得 第一章 概述 1.1 go与java中的局部变量初始化问题 1.2 golang中实现生产者消费者模型,利用管道进行数据通信 第二章 类型 2.1 ...

最新文章

  1. 高性能千万级定时任务管理服务forsun使用详解
  2. php跳转app,小程序支持跳转app么
  3. 第二章 知识图谱——机器大脑中的知识库
  4. OpenCV中图像窗口的鼠标事件
  5. 数据机房气流组织的常见类型及应用
  6. Java面向对象抽象类案例分析
  7. form、document.all[].value的数字处理
  8. oracle停止trace日志,关闭ORACLE客户端trace日志
  9. LVS_DR实现过程...
  10. python版代码整洁之道
  11. 程序员面试HR常问问题(含答案)
  12. C++QT开发——Xml、Json解析
  13. 人不成熟的几大特征-----海尔集团CEO张瑞敏演讲稿
  14. 基于照片标记的广州市旅游流特征简单分析(上)
  15. synchdem matlab,数字高程模型(DEM)移动插值算法
  16. javascript继承的6种方法
  17. 台式计算机外观组成部分,台式电脑由哪些部分组成?
  18. JavaScrapt朝花夕拾
  19. 26个节省时间的Vue提示
  20. 组合预测模型 | PSO-ELM、ELM极限学习机数据回归预测对比(Matlab程序)

热门文章

  1. 服装制图软件测试初学者,服装行业版软件测试方案.ppt
  2. 数字逻辑——七段数码管
  3. 文件无法删除 你需要计算机管理员 提供的权限才能对此文件进行更改解决办法
  4. AR和VR,有哪些知名的开源平台
  5. Win10 AMD610显卡驱动安装出现错误206安装失败
  6. Hausdorff 距离
  7. WRF4.2安装过程全记录
  8. Github黑暗模式正式发布,Reddit直接飙至4k高赞
  9. 2016最新精彩而又幽默的搞笑段子精选
  10. 【BP数据预测】基于matlab天牛须算法优化BP神经网络数据预测【含Matlab源码 1318期】