GoLang之启动goroutine、sync.WaitGroup

文章目录

  • GoLang之启动goroutine、sync.WaitGroup
    • 1.go关键字
    • 2.串行执行函数
    • 3.启动单个goroutine
    • 4.启动单个goroutine结合time.Sleep函数
    • 5.启动单个goroutine结合sync.WaitGroup
    • 6.启动多个goroutine结合sync.WaitGroup
    • 7.go关键字放在遍历循环外面
    • 8.grountine启动匿名函数
    • 9.grountine启动匿名函数错误版

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
你好
*/
}

GoLang之启动goroutine、sync.WaitGroup_第1张图片

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就已经执行结束了

GoLang之启动goroutine、sync.WaitGroup_第2张图片

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*/
}

GoLang之启动goroutine、sync.WaitGroup_第3张图片

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

GoLang之启动goroutine、sync.WaitGroup_第4张图片

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

GoLang之启动goroutine、sync.WaitGroup_第5张图片
GoLang之启动goroutine、sync.WaitGroup_第6张图片

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个小弟干活的效率是不一样的

GoLang之启动goroutine、sync.WaitGroup_第7张图片

GoLang之启动goroutine、sync.WaitGroup_第8张图片

GoLang之启动goroutine、sync.WaitGroup_第9张图片

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

GoLang之启动goroutine、sync.WaitGroup_第10张图片

8.grountine启动匿名函数

GoLang之启动goroutine、sync.WaitGroup_第11张图片

GoLang之启动goroutine、sync.WaitGroup_第12张图片

GoLang之启动goroutine、sync.WaitGroup_第13张图片

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

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

GoLang之启动goroutine、sync.WaitGroup_第14张图片

GoLang之启动goroutine、sync.WaitGroup_第15张图片

GoLang之启动goroutine、sync.WaitGroup_第16张图片

GoLang之启动goroutine、sync.WaitGroup_第17张图片

你可能感兴趣的:(GoLang,golang)