Go并发编程:goroutine

介绍

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{728-940}

 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{728-940}

 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 := 01
 for i := 0; i < n; i++ {
  c <- x
  x, y = y, x+y
 }
 close(c)
}

func main() {
 c := make(chan int10)
 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 := 01
 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

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!