go语言中的main函数也是运行在一个单独的goroutine中的,通常称为 main goroutine,main函数结束时,会打断其它 goroutine 的执行,可是其它 goroutine 不会打断其它的 goroutine 的执行,除非是经过通讯让对方自行停止。缓存
先来看一个最简单的并发例子,一个时间服务器:(偏题了,不该该使用这个例子,应该突出重点,这个例子能够放到tcp那节)服务器
func main() { listener, err := net.Listen("tcp", "localhost:8000") if err != nil { log.Fatal(err) } for { conn, err := listener.Accept() if err != nil { log.Print(err) continue } go handleConn(conn) } } func handleConn(c net.Conn) { defer c.Close() for { _, err := io.WriteString(c, time.Now().Format("2006/01/02 15:04:05\n")) if err != nil { return } time.Sleep(1 * time.Second) } }
从这个例子能够看出,使用go开启一个tcp服务很是简洁,此外从该例子中,咱们能够顺便了解一下go中格式化日期和时间的方式是很是奇葩的,格式化模板限定为Mon Jan 2 03:04:05PM 2006 UTC-0700,能够这样记:1月2日下午3点4分5秒(200)6年UTC-0700。数据结构
能够用 nc 或 telnet 来链接这个tcp服务进行测试, 也可使用go实现一个简单的客户端:并发
func main() { conn, err := net.Dial("tcp", "localhost:8000") if err != nil { log.Fatal(err) } defer conn.Close() mustCopy(os.Stdout, conn) } func mustCopy(dst io.Writer, src io.Reader) { if _, err := io.Copy(dst, src); err != nil { log.Fatal(err) } }
goroutine是经过 channel 来通讯的, 包括发送和接收两种操做。能够经过 make 来声明一个channel 如 ch = make(chan int),和 map 相似,channel 也只是对应底层数据结构的引用,因此发送方和接收方都将引用同一份对象,channel是引用类型,因此它的零值是 nil。channel 使用的操做符是 <- ,发送操做是指将数据发送到 channel,接收是指从 channel中接收。channel相似于通常的io系统,能够经过 close(ch) 来关闭一个 channel,关闭后,将不能进行发送操做,但能够接收以前未接收完的数据,若是没有数据可接收,则接收到一个nil。tcp
经过 make(chan int) 这种方式建立的 channel 称之为无缓存channel,表示 channel的容量是0,若是要声明一个有缓存的channel,可使用 make(chan int, 100) ,第二个参数表示初始化时的channel容量大小。函数
一个基于无缓存Channels的发送操做将致使发送者goroutine阻塞,直到另外一个goroutine在相同的Channels上执行接收操做,当发送的值经过Channels成功传输以后,两个goroutine能够继续执行后面的语句。反之,若是接收操做先发生,那么接收者goroutine也将阻塞,直到有另外一个goroutine在相同的Channels上执行发送操做。测试
简单的说,就是使用无缓存channel时,一旦通讯发起,必需要等到通讯完成才能够继续执行,不然将阻塞等待。这也就意味着,无缓存channel在通讯时,会强制进行一次同步操做,因此无缓存channel也称之为同步channelspa
下面是一个串联channel的例子:code
func main() { ch1 := make(chan int) ch2 := make(chan int) go func() { for x := 1; x <= 10; x++ { ch1 <- x } close(ch1) }() go func() { for x := range ch1 { ch2 <- x * x } close(ch2) }() for x := range ch2 { fmt.Println(x) } }
无论一个channel是否被关闭,当它没有被引用时将会被Go语言的垃圾自动回收器回收。(不要将关闭一个打开文件的操做和关闭一个channel操做混淆。对于每一个打开的文件,都须要在不使用的使用调用对应的Close方法来关闭文件。)试图重复关闭一个channel或者关闭一个nil值的channel都将致使panic异常,此外关闭一个channels还会触发一个广播机制。orm
由于关闭操做只用于断言再也不向channel发送新的数据,因此只有在发送者所在的goroutine才会调用close函数,所以对一个只接收的channel调用close将是一个编译错误。
单方向channel,只发送或只接收,功能明确。类型chan<- int
表示一个只发送int的channel,只能发送不能接收。相反,类型<-chan int
表示一个只接收int的channel,只能接收不能发送。(箭头<-
和关键字chan的相对位置代表了channel的方向。)这种限制将在编译期检测。
func counter(out chan<- int) { for x := 0; x < 100; x++ { out <- x } close(out) } func squarer(out chan<- int, in <-chan int) { for v := range in { out <- v * v } close(out) } func printer(in <-chan int) { for v := range in { fmt.Println(v) } } func main() { naturals := make(chan int) squares := make(chan int) go counter(naturals) go squarer(squares, naturals) printer(squares) }
带缓存的 channel,内部持有一个元素队列。队列的最大容量是在调用make函数建立channel时经过第二个参数指定的。向缓存Channel的发送操做就是向内部缓存队列的尾部插入缘由,接收操做则是从队列的头部删除元素。若是内部缓存队列是满的,那么发送操做将阻塞直到因另外一个goroutine执行接收操做而释放了新的队列空间。相反,若是channel是空的,接收操做将阻塞直到有另外一个goroutine执行发送操做而向队列插入元素。
内置的 cap 函数能够查看某个 channel 的容量,len 函数能够查看某个 channel 的当前缓存队列中有效元素的个数。
若是咱们使用了无缓存的channel,那么两个慢的goroutines将会由于没有人接收而被永远卡住。这种状况,称为goroutines泄漏,这将是一个BUG。和垃圾变量不一样,泄漏的goroutines并不会被自动回收,所以确保每一个再也不须要的goroutine能正常退出是重要的。
关于无缓存或带缓存channels之间的选择,或者是带缓存channels的容量大小的选择,均可能影响程序的正确性。无缓存channel更强地保证了每一个发送操做与相应的同步接收操做;可是对于带缓存channel,这些操做是解耦的。一样,即便咱们知道将要发送到一个channel的信息的数量上限,建立一个对应容量大小带缓存channel也是不现实的,由于这要求在执行任何接收操做以前缓存全部已经发送的值。若是未能分配足够的缓冲将致使程序死锁。
若是生产线的前期阶段一直快于后续阶段,那么它们之间的缓存在大部分时间都将是满的。相反,若是后续阶段比前期阶段更快,那么它们之间的缓存在大部分时间都将是空的。对于这类场景,额外的缓存并无带来任何好处。
个人理解是,当通讯双方的工做效率接近时,缓存才有意义,若是一方处理过快,而另外一方过慢,则效率由短板决定,缓存并不能起到提高效率的做用。