内容简介:学习之前先看一下下面这句话:Don’t communicate by sharing memory; share memory by communicating.不要通过共享数据来通讯,要以通讯的方式共享数据。
go语句及其执行规则
学习之前先看一下下面这句话:
Don’t communicate by sharing memory; share memory by communicating.
不要通过共享数据来通讯,要以通讯的方式共享数据。
通道(也就是 channel)类型的值可以被用来以通讯的方式共享数据。更具体地说,它一般被用来在不同的goroutine之间传递数据。
这篇主要讲goroutine是什么。简单来说,goroutine代表着并发编程模型中的用户级线程。
调度器
Go语言不但有着独特的并发编程模型,以及用户级线程goroutine,还拥有强大的用于调度goroutine、对接系统级线程的调度器。
这个调度器是 Go 语言运行时系统的重要组成部分,它主要负责统筹调配Go并发编程模型中的三个主要元素:
- G(goroutine 的缩写),用户级线程
- P(processor 的缩写),一种可以承载若干个G,且能够使用这些G适时的与M进行对接,并得到真正运行的中介
- M(machine 的缩写),系统级线程
主goroutine
这里需要知道一个与主goroutine有关的重要特性,一旦主goroutine中的代码(也就是main函数中的那些代码)执行完毕,当前的 Go 程序就会结束运行。
先看下面这个例子:
package main import "fmt" func main() { for i := 0; i < 10; i++ { go func() { fmt.Println(i) }() } }
上面的程序运行之后,不会有打印任何内容。
只要go语句本身执行完毕,Go程序完全不会等待go函数的执行,它会立刻去执行后面的语句。这就是所谓的异步并发地执行。
在上面的例子中,在for语句执行完毕后,里面包装的10个goroutine还没有获得运行的机会,主goroutine中的代码执行完了,Go程序就会立即结束运行。
使用Sleep等待
上面的例子中,如果要让程序在其他goroutine运行完之后再退出。最简单粗暴的办法是Sleep一段时间:
package main import ( "fmt" "time" ) func main() { for i := 0; i < 10; i++ { go func() { fmt.Println(i) }() } time.Sleep(time.Second) }
这个办法可行,但是Sleep的时间需要预估。太长会浪费时间,太短则不能保证所有goroutine都运行完毕。不容易预估时间,最好是让其他的goroutine在运行完毕后发送通知。
让主goroutine等待其他goroutine
使用通道,通道的长度与启用的goroutine的数量一致。每个goroutine运行完毕前,都向通道发送一个值。在主goroutine则是从这个通道接收值,接收了足够数量的次数后就说明所有goroutine都运行完毕了,可以继续往下执行了(就是退出):
package main import "fmt" func main() { sign := make(chan struct{}, 10) for i := 0; i < 10; i++ { go func() { fmt.Println(i) sign <- struct{}{} }() } for j := 0; j < 10; j++ { <- sign } }
这里声明的通道的类型是 chan struct{} ,是一个空结构体。它谭勇的内存空间是0字节。这个值在整个Go程序中永远都只会存在一份。虽然可以无数次的使用这个值字面量,但是用到的都是同一个值。当把通道仅仅刀座是传递某个简单信号的介质的时候,使用空结构体是最好的。
其他方式在标准库中,有一个sync包,里面有一个sync.WaitGroup类型。这应该是一个更好的实现方式。不过这要等后面讲sync包的时候再说了。
让多个goroutine按照既定的顺序执行
首先改造一下一只使用的例子,把变量i的值传递给每个goroutine,这样输出的是0-9各一次,不过是乱序的:
for i := 0; i < 10; i++ { go func(i int) { fmt.Println(i) sign <- struct{}{} }
讲师的例子
package main import ( "fmt" "sync/atomic" "time" ) var count uint32 func trigger (i uint32, fn func()) { for { if n := atomic.LoadUint32(&count); n == i { fn() atomic.AddUint32(&count, 1) break } time.Sleep(time.Nanosecond) } } func main() { for i := uint32(0); i < 10; i++ { go func(i uint32) { fn := func() { fmt.Println(i) } trigger(i, fn) }(i) } trigger(10, func() {}) }
主要就是trigger函数。在trigger里会检查i,并把要执行的语句打包成fn函数也传入,只有在trigger里判断后符合条件,就会执行fn函数的语句。
trigger里会检查i和count是否相等,在执行了fn函数后,需要把count加1,这里用了原子操作。里有是trigger函数会被多个goroutine并发的调用,所以这个变量被多个用户级线程共用了。因此对它的操作就产生了竞态条件(race condition),破坏了程序的并发安全性。
在最后退出的时候,应该有了trigger函数,只要检查count是否到10了,就表示其他goroutine都执行完了,所以也就不需要通道了。
另外在trigger函数里,是一个for语句的无限循环,在判断条件不成立后,先进行了一个1纳秒的Sleep。如果不加这句的话,测试下来,偶尔会出现程序卡住的情况(甚至是死机)。这里加上Sleep语句应该是希望这个时候程序可以进行一下切换,否则当前应该执行的那个goroutine如果拿不到执行的机会,其他goroutine也都无法通过if条件的判断。
自己的实现
package main import ( "fmt" "time" ) func main() { sign := make(chan struct{}, 10) var count int for i := 0; i < 10; i++ { go func(i int) { for { if count == i{ fmt.Println(i) count ++ sign <- struct{}{} break } time.Sleep(time.Nanosecond) } }(i) } for j := 0; j < 10; j++ { <- sign } }
主要两个问题,当时没有意识到在for无限循环之后,进入下一个迭代前,这个1纳秒Sleep的意义。还有就是我没有使用原子操作。不过这里即使不用原子操作也没问题的样子,因为逻辑上通知只有一个goroutine满足条件会去操作共用的变量count。所以这里和上面讲师的示例就差在对变量count的比较和判断是否是原子操作的问题上了。
原子操作这里再自我做一些补充。
原子操作,即执行过程不能被中断的操作(并发)。
经典问题:i++是不是原子操作?
答案是否,因为i++看上去只有一行,但是背后包括了多个操作:取值,加法,赋值。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。