GoLang之启动goroutine、sync.WaitGroup

GoLang之启动goroutine、sync.WaitGroup

注:本文基于Windos系统上Go SDK v.1.8进行操作

1.go关键字

Go语言中使用 goroutine 非常简单,只需要在函数或方法调用前加上go关键字就可以创建一个 goroutine ,从而让该函数或方法在新创建的 goroutine 中执行。
一个 goroutine 必定对应一个函数/方法,可以创建多个 goroutine 去执行相同的函数/方法。

go f()  // 创建一个新的 goroutine 运行函数f

匿名函数也支持使用go关键字创建 goroutine 去执行。

go func(){
  // ...
}() 

2.串行执行函数

我们先来看一个在 main 函数中执行普通函数调用的示例。
代码中 hello 函数和其后面的打印语句是串行的。

package main

import (
	"fmt"
)

func hello() {
	fmt.Println("hello")
}

func main() {
	hello()
	fmt.Println("你好")
	/*
hello
你好
*/
}

在这里插入图片描述

3.启动单个goroutine

启动 goroutine 的方式非常简单,只需要在调用函数(普通函数和匿名函数)前加上一个go关键字。
接下来我们在调用 hello 函数前面加上关键字go,也就是启动一个 goroutine 去执行 hello 这个函数。
这一次的执行结果只在终端打印了”你好”,并没有打印 hello。这是为什么呢?
其实在 Go 程序启动时,Go 程序就会为 main 函数创建一个默认的 goroutine 。在上面的代码中我们在 main 函数中使用 go 关键字创建了另外一个 goroutine 去执行 hello 函数,而此时 main goroutine 还在继续往下执行,我们的程序中此时存在两个并发执行的 goroutine。当 main 函数结束时整个程序也就结束了,同时 main goroutine 也结束了,所有由 main goroutine 创建的 goroutine 也会一同退出。也就是说我们的 main 函数退出太快,另外一个 goroutine 中的函数还未执行完程序就退出了,导致未打印出“hello”。
main goroutine 就像是《权利的游戏》中的夜王,其他的 goroutine 都是夜王转化出的异鬼,夜王一死它转化的那些异鬼也就全部GG了。

func hello() {
	fmt.Println("hello")
}

func main() {
	go hello() // 启动另外一个goroutine去执行hello函数
	fmt.Println("main goroutine done!")
}
/*
main goroutine done!
*/

这个代码讲解:
在hello()前开启了一个独立的gorountine去执行hello()这个函数,也就是说main函数在开启的时候会开启了一个主gorountine去执行main函数,在main函数内部遇到了一个go关键字之后,它会开启一个grountine去执行helle()这个函数,即主grountine派了一个小弟去执行这个hello()函数
以下代码会有不同的输出情况
第一种输出:
hello main
hello 娜炸
第二种输出:
hello 娜炸
hello main
原因:主grountine派小弟去执行hello()的时候,这个小弟可能会执行非常快、也可能执行非常慢;比如main函数执行完了但是小弟还没有开始干活,也比如小弟已经干完活了,main才开始干活
第三种输出:
hello main
原因:发现只有main,没有娜扎,这是由于hello()没来得及打印main就已经执行结束了

image-20220122163141021

4.启动单个goroutine结合time.Sleep函数

我们要想办法让 main 函数‘“等一等”将在另一个 goroutine 中运行的 hello 函数。其中最简单粗暴的方式就是在 main 函数中“time.Sleep”一秒钟了(这里的1秒钟只是我们为了保证新的 goroutine 能够被正常创建和执行而设置的一个值)。
执行以下代码,短暂停顿一会儿,程序会在终端输出如下结果
为什么会先打印你好呢?
这是因为在程序中创建 goroutine 执行函数需要一定的开销,而与此同时 main 函数所在的 goroutine 是继续执行的。

package main

import (
	"fmt"
	"time"
)

func hello() {
	fmt.Println("hello")
}

func main() {
	go hello()
	fmt.Println("你好")
	time.Sleep(time.Second)
	/*
	你好
hello*/
}

time.Sleep(time.Second)会让主grountine等待1秒,这个样的话就一定会执行到hello(),而且只有两种输出
第一种输出:
hello main
hello 娜炸
第二种输出:
hello 娜炸
hello main
注:最好不要使用time.Sleep的方法

image-20220122164241020

5.启动单个goroutine结合sync.WaitGroup

在上面的程序中使用time.Sleep让 main goroutine 等待 hello goroutine执行结束是不优雅的,当然也是不准确的。
Go 语言中通过sync包为我们提供了一些常用的并发原语,在这一小节,我们会先介绍一下 sync 包中的WaitGroup。当你并不关心并发操作的结果或者有其它方式收集并发操作的结果时,WaitGroup是实现等待一组并发操作完成的好方法。
下面的示例代码中我们在 main goroutine 中使用sync.WaitGroup来等待 hello goroutine 完成后再退出。
将代码编译后再执行,得到的输出结果和之前一致,但是这一次程序不再会有多余的停顿,hello goroutine 执行完毕后程序直接退出。

package main

import (
	"fmt"
	"sync"
)

// 声明全局等待组变量
var wg sync.WaitGroup

func hello() {
	fmt.Println("hello")
	wg.Done() // 告知当前goroutine完成
}

func main() {
	wg.Add(1) // 登记1个goroutine
	go hello()
	fmt.Println("你好")
	wg.Wait() // 阻塞等待登记的goroutine完成
	/*有两种输出情况:
	你好
	hello
	*/
	/*
	你好
	hello
	*/
}

sync包里有一个WaitGroup结构体,我们定义一个这个结构体变量
wg.Add(1)意思是:在启动grountine的时候使用wg.Add()给这个grountine登记一下,派出一个小弟去干活;相当于一个计数牌
wg.Wait()等到标记的所有小弟干完活回来之后才收回小弟,结束的标记是标记牌为0
wg.Done() //来使标记牌减1
注:不能把WaitGroup结构体变量定义在main()函数里,这样的话hello()里的wg.Done()识别不到,所以需要把WaitGroup结构体变量定义成全局变量,这样的话在main()里与hello()里都会识别到WaitGroup结构体变量
只有两种输出
第一种输出:
hello main
hello 娜炸
第二种输出:
hello 娜炸
hello main

image-20220122170454103
image-20220224090613311

6.启动多个goroutine结合sync.WaitGroup

在 Go 语言中实现并发就是这样简单,我们还可以启动多个 goroutine 。让我们再来看一个新的代码示例。这里同样使用了sync.WaitGroup来实现 goroutine 的同步。
多次执行上面的代码会发现每次终端上打印数字的顺序都不一致。这是因为10个 goroutine 是并发执行的,而 goroutine 的调度是随机的。

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func hello(i int) {
	defer wg.Done() // goroutine结束就登记-1
	fmt.Println("hello", i)
}
func main() {
	for i := 0; i < 10; i++ {
		wg.Add(1) // 启动一个goroutine就登记+1
		go hello(i)
	}
	wg.Wait() // 等待所有登记的goroutine都结束
}

也可以使用使用WaitGroup结构体启动多个grountine
在for循环里面写go hello() ,相当于开启了100个grountine
运行后输出查看控制台发现grountine的顺序是不固定的,这100个小弟干活的效率是不一样的

image-20220122172719341

image-20220122172504139

image-20220122172701415

7.go关键字放在遍历循环外面

image-20220106091047484

8.grountine启动匿名函数

image-20220123130233406

image-20220123130207593

image-20220123130219184

9.grountine启动匿名函数错误版

执行输出后发现有很多相同的输出
原因:形成了闭包,这个匿名函数里包含了一个外部函数作用域的一个i

image-20220123125333768

image-20220123125352542

image-20220123125422281

image-20220123125406524

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GoGo在努力

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值