写在前面
- 这一部分,在官方教程中并没有太多详细的说明,建议读者先看以下文章:
- http://legendtkl.com/2017/07/30/understanding-golang-channel/
- https://studygolang.com/articles/12342
- https://studygolang.com/articles/12402
并发 Concurrency
- go的并发是通过goroutine来实现的
- 它的使用,就是在某个操作前加上go关键字
go f(x, y, z)
- 什么叫当前的goroutine?
- 文字苍白,且看代码:
package main import "fmt" func main() { fmt.println("hello,world!") }
- 如果程序开始执行,那么上面的的就是当前的goroutine了
- 什么是非当前的goroutine
- 使用go关键词就能添加新的goroutine了
import "fmt" func NewGoroutine() { fmt.Println("new goroutine!") } func main() { go NewGoroutine() //这里就添加了新的goroutine了(运行起来才算) fmt.println("hello,world!") }
- 但是,运行起来并没有看到打印出"new goroutine"
- 这是因为当前的goroutine并没有义务等待这个新的goroutine执行,它自己执行完就结束了
- 那么应该如何改变呢?Go提供了一些列管理goroutine的方法,其中最主要就是channel和Mutex了
Channel
- Channel也是一种类型,它的定义和使用比较特别
ch := make(chan int) ch <- v // Send v to channel ch. v := <-ch // Receive from ch, and // assign value to v.
- 定义一般采用make函数进行,chan关键字是固定的,后面该channel可以放置的类型,例子中的就是int
- 它的基本使用有两种模式:
- 其他变量往channel传送数据:ch<-v
- 其他变量从channel接受数据:v := <-ch
- 两种操作是互相等待的,以例子中的说明
- 当A处的数据创送给ch时(ch <- A),它会先判断,是否其它地方需要从ch中获取数据
- 若是:则数据从A处传到指定的地方
- 若不是:A处的goroutine将被阻塞(其实也就是停下来),等到其它任意地方需要从ch中获取数据时,A处的goroutine才开始运行
- 而B处需要从ch获取数据(B := <-ch),它会先判断,是否某个地方给ch传送数据
- 若是:则数据从某个地方传送到B处
- 若不是:B处的goroutine将被阻塞(其实也就是停下来),等到其它任意地方传送数据给ch,B处的goroutine才开始运行
- 简单的比喻就是传送门两侧,需要等待两边的人都同意才能打开
- 或者这样理解,如果把这个过程比喻成接力赛
- 接力棒是数据,每个队有多个成员
- A成员的目标就是将接力棒交给B成员,但需要奔跑,需要时间,B成员需要等待A成员(被阻塞)
- 下面是例子代码:
package main import "fmt" func sum(s []int, c chan int) { sum := 0 for _, v := range s { sum += v } c <- sum // send sum to c } func main() { s := []int{7, 2, 8, -9, 4, 0} c := make(chan int) go sum(s[:len(s)/2], c) go sum(s[len(s)/2:], c) x, y := <-c, <-c // receive from c fmt.Println(x, y, x+y) }
- 注意到 x, y := <-c, <-c // receive from c :
- 这段代码在当前goroutine中出现,当程序运行起来,这个位置的channel c将会等待从某处传来的数据
- 而某处就是go关键字标记出来的两个新goroutine了:
go sum(s[:len(s)/2], c) go sum(s[len(s)/2:], c)
- 这两处都是执行sum函数,执行的最后把结果传给channel c
- 这就解决了当前goroutine不等待其它goroutine运行结束就自个儿运行完成的问题
- buffered channel是允许存储多个值的channel,它的空间可以理解为一个数组
- 相当于接力赛中,A队员要拿着多根接力棒跑,他很辛苦,不只要给B队员,还要给C队员,还要。。。。
package main import "fmt" func main() { ch := make(chan int, 2) ch <- 1 ch <- 2 fmt.Println(<-ch) fmt.Println(<-ch) }
- 读者们可以试着把ch定义成普通的channel:ch := make(chan int)
- 这样会报错,原因在于:
- 首先这是在一个goroutine中
- 其次,ch <-1执行时,会一直等待其它goroutine执行从ch中拿数据的操作
关键字:range,close
- 当某个goroutine中的某个操作需要不停从channel c中获取数据时,可以用到range关键字
- 而对于上述操作,并不需要担心当channel c不再有数据的事情,这个事情是由运行发送数据到channel c的goroutine关心的
- 如果发送数据给channel c的goroutine不需要再发送数据了,那就需要执行close(c)的操作,否则程序会报错
package main import ( "fmt" ) func fibonacci(n int, c chan int) { x, y := 0, 1 for i := 0; i < n; i++ { c <- x x, y = y, x+y } close(c) } func main() { c := make(chan int, 10) go fibonacci(cap(c), c) for i := range c { fmt.Println(i) } }
- 对于上述代码,先关注main函数
- go fibonacci(cap(c), c)这一句新建了一个goroutine,其中cap(c)是用来计算channel c的容量
- for i := range c这句,
- 将不停循环判断,
- 一有数据传送到channel c,i 就能获取该数据,并执行循环语句中的内容
- 当channel c被关闭,循环判断将结束
- 再看fibonacci函数
- fibonacci数列的计算,其中for循环里的内容c <- x这句,就是向channel c传送数据,之后main中的for语句会执行相应操作
- 最后close(c)这句,关闭channel c,之后main的for语句结束执行
关键字 select
- select很特别,它一般会搭配for循环使用,行为上和switch语句类似
- select语句判断是是否执行了某些操作,如果是,我就将执行XXX操作
- switch语句判断的通常是某某值是否等于某某值这种true or false的问题
- 代码:
package main import "fmt" func fibonacci(c, quit chan int) { x, y := 0, 1 for { select { case c <- x: x, y = y, x+y case <-quit: fmt.Println("quit") return } } } func main() { c := make(chan int) quit := make(chan int) go func() { for i := 0; i < 10; i++ { fmt.Println(<-c) } quit <- 0 }() fibonacci(c, quit) }
- main()中,创建了channel c和channel quit
- 一个匿名函数作为新建goroutine的运行内容
- 然后运行fibonacci函数(这是当前goroutine中的)
- 在fibonacci函数中,使用for进行无限循环不停执行select语句
- 在这里select语句会不停判断每个case语句中的操作
- 像case c <- x,判断条件也是一种操作,所以会先把x传送给c,然后阻塞,等待其他向channel c要数据的操作
- 这时,匿名函数(新建的goroutine)的for循环中fmt.Println(<-c)将会执行,执行完成后,又会进行等待其他操作向channel c 传递数据
- 而在fibonacci中,case c <- x能顺利进行后,将执行x, y = y, x+y,然后进入新的循环中执行select语句
- 过程就像上述那样,进行循环,判断,阻塞,解除阻塞。。。
- 等到goroutine的循环结束,并执行quit <- 0后,就会执行select语句中的case <-quit的内容了,之后程序返回,当前goroutine结束
- 当然,select也会搭配关键字default:
package main import ( "fmt" "time" ) func main() { tick := time.Tick(100 * time.Millisecond) boom := time.After(500 * time.Millisecond) for { select { case <-tick: fmt.Println("tick.") case <-boom: fmt.Println("BOOM!") return default: fmt.Println(" .") time.Sleep(50 * time.Millisecond) } } }
- 当其它case中的channel都每准备好,就会执行default中的语句
sync.Mutex
- 解决并发冲突的办法还有比较经典的Mutex
- Mutex可以进行加锁Lock()和解锁操作Unlock()
- 这里不展开了,留在下一篇,也是a tour of go的最后一道习题,个人认为这个题目出得非常好,值得详细讲解
来源:https://www.cnblogs.com/laiyuanjing/p/10895880.html