介绍
goroutine是由 Go 运行时管理的轻量级线程,它和线程相比开销更小。我们可以使用go关键字创建一个goroutine。它是go支持高并发的基础,也是go语言极为具有代表性的特性。首先做个小实验:
package main
import (
"fmt"
"time"
)
func say() {
for i := 0;i<5;i++{
time.Sleep(3*time.Millisecond)
fmt.Println("我在里面")
}
}
func main() {
go say()
time.Sleep(16*time.Millisecond)
fmt.Println("我在外面")
}
猜想一下打印结果:
我在里面
我在里面
我在里面
我在里面
我在里面
我在外面
输出的结果应该是上面这样?本地跑一下实际结果如下:
我在里面
我在里面
我在里面
我在里面
我在外面
没错,就是上面这样,怎么少了一个“我在里面”的输出呢?请继续往下看!
线程
进程是操作系统分配和调度的基本单元,线程是最小单元,线程承担了实际的程序执行工作。那么怎么解释上面的程序现象呢?先来看下主函数程序代码:
func main() {
go say() // 开辟新的空间,和main线程隔离
time.Sleep(16*time.Millisecond)
fmt.Println("我在外面")
}
这样我们就可以理解了。main执行到go say()开辟了一个新goroutine随后继续依次执行后面的程序,随后执行到最后一行fmt.Println("我在外面")
后退出主程序,原来新建的goroutine也随之结束。总结:来不及执行完成。
并发
介绍
了解到goroutine是新建一个程序执行的空间和main互不干扰,即say()和main()在一段时间是并发执行(就单核而论)。我们可以看到一个经典的生产者-消费者模型来更好地描述并发。
生产者-消费者模型
假定生产者生产一个商品流程如下三步:
register = count
register = register + 1
count = register
消费者消费商品的流程如下三步:
register = count
register = register - 1
count = register
宏观来看是没有问题的,但是在程序执行过程中可能会出现如下情况:
count = 10
register_p = count
register_p = register_p + 1
// 消费者开始消费
register_c = count
register_c = register_c - 1
count = register_c
count = register_p
这种情况下count最后结果是:11。显而易见问题是出在count这里,count作为竞争资源,我们也叫做临界资源。同时被两个程序操作导致的。
线程同步
线程的同步问题我们通常可以用互斥量、读写锁、自旋锁等来实现。思路就是在count被操作的时候对count进行加锁,其他程序这时不可对count进行修改,即阻塞,直到count被解锁。
在go中sync
包提供了这种能力,不过在 Go 中并不经常用到,因为还有其它的办法:channel信道。
信道
介绍
信道channel是带有类型的管道,你可以通过它用信道操作符 <-
来发送或者接收值。我们可以自定义信道的值缓冲长度,例:ch := make(chan int, 100)
创建了一个100容量的ch信道。在信道中没有值的时候取值操作会被阻塞。下面来看一组官方的例子:
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
// 切片遍历求和
for _, v := range s {
sum += v
}
c <- sum // 将和送入 c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int) // make创建信道
go sum(s[:len(s)/2], c) // 数组后半程求和
go sum(s[len(s)/2:], c) // 数组前半程求和
x, y := <-c, <-c // 从 c 中接收
fmt.Println(x, y, x+y)
}
这里我们主要看main函数:
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int) // make创建信道
go sum(s[:len(s)/2], c) // 数组后半程求和
go sum(s[len(s)/2:], c) // 数组前半程求和
x, y := <-c, <-c // 从 c 中接收
fmt.Println(x, y, x+y)
}
可以知道,make关键字创建信道c,数组后半程求和
的goroutine先创建,而后创建数组前半程求和
的goroutine,再从c信道中取值。因为在两个sum()的goroutine执行完成往信道传值前,取值操作会造成阻塞,随意程序会停留在x, y := <-c, <-c
等待两个协程执行完成,最后执行打印语句。这里给出官网例子执行结果:
-5 17 12
Program exited.
可以看到结果是和我们预期的相同的,并未发生消费者-生产者模型的问题。这里给出goroutine相关的另外两个知识点:range、close()。
close&range
官网例子:
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)
}
}
range可以循环从信道中取值,发送者可以通过close关闭信道。结果:
0
1
1
2
3
5
8
13
21
34
select
select
会阻塞到某个分支可以继续执行为止,这时就会执行该分支。官网例子:
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)
}
执行结果:
0
1
1
2
3
5
8
13
21
34
quit
为了在尝试发送或者接收时不发生阻塞,可使用 default
分支。
最后给出go协程官网文档案例地址:https://tour.go-zh.org/concurrency/1
。
📢📢📢欢迎大家在公众号后台留言交流学习!!!📢📢📢
原文始发于微信公众号(fairy with you):Go并发编程:goroutine
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/29673.html