李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
GO
正文
21.Golang协程介绍
Leefs
2022-07-12 PM
876℃
0条
[TOC] ### 前言 ![21.Golang协程介绍01.jpeg](https://lilinchao.com/usr/uploads/2022/07/761314095.jpeg) ### 一、概述 协程(`Goroutines`) 是与其他函数或方法同时运行的函数或方法。 `Goroutines`可以被认为是轻量级线程。与线程相比,创建 `Goroutine` 的成本很小。因此,Go 应用程序通常会同时运行数千个 `Goroutine`。 **协程的优势:** - **与线程相比,`Goroutines` 非常小。** 它们的堆栈大小只有几 kb,堆栈可以根据应用程序的需要增长和缩小,而在线程的情况下,堆栈大小必须指定并固定。 - **`Goroutines` 被多路复用到较少数量的 OS 线程。** 一个包含数千个 `Goroutine` 的程序中可能只有一个线程。如果该线程块中的任何 `Goroutine` 要等待用户输入,则创建另一个 OS 线程,并将剩余的 `Goroutine` 移动到新的 OS 线程。 所有这些都由运行时处理,开发者可以从这些复杂的细节中抽象出来,并获得了一个干净的 API 来处理并发。 - **`Goroutine` 使用通道(channel)进行通信。** 通道的设计可以防止在使用 `Goroutine` 访问共享内存时发生竞争条件。通道可以被认为是一个管道,`Goroutines` 使用它进行通信。 ### 二、goroutine快速入门 #### 2.1 并发执行 > 编写一个函数,该函数每隔1秒输出"Hello GO" ```go import ( "fmt" "strconv" "time" ) func test(){ for i := 1;i<=5;i++{ fmt.Println("test() Hello GO",strconv.Itoa(i)) //strconv.Itoa 数字转字符串 time.Sleep(time.Second) } } func main() { test() for i:= 1;i<=5;i++{ fmt.Println("main() Hello Golang",strconv.Itoa(i)) //strconv.Itoa 数字转字符串 time.Sleep(time.Second) } } ``` **运行结果** ``` test() Hello GO 1 test() Hello GO 2 test() Hello GO 3 test() Hello GO 4 test() Hello GO 5 main() Hello Golang 1 main() Hello Golang 2 main() Hello Golang 3 main() Hello Golang 4 main() Hello Golang 5 ``` **分析** 上面并发的场景下,程序会先执行test()函数下的代码进行输出,而主线程等待 当test()执行完毕后,才会继续执行主线程,这样就是并发,将所有任务放在一个cpu上。 #### 2.2 并行执行 > 在主线程(可以理解成进程)中,开启一个 goroutine, 该协程每隔 1 秒输出"test() Hello GO" > > 在主线程中也每隔 1 秒输出"main() Hello Golang", 输出 5 次后,退出程序,要求主线程和goroutine 同时执行。 ```go import ( "fmt" "strconv" "time" ) func test02(){ for i := 1;i<=10;i++{ fmt.Println("test() Hello GO",strconv.Itoa(i)) //strconv.Itoa 数字转字符串 //睡眠1s time.Sleep(time.Second) } } func main() { //在函数前面加上一个关键字go 表明开启一个协程来运行这个函数 go test02() for i:= 1;i<=5;i++{ fmt.Println("main() Hello Golang",strconv.Itoa(i)) //strconv.Itoa 数字转字符串 //睡眠1s time.Sleep(time.Second) } } ``` **运行结果** ``` main() Hello Golang 1 test() Hello GO 1 main() Hello Golang 2 test() Hello GO 2 test() Hello GO 3 main() Hello Golang 3 main() Hello Golang 4 test() Hello GO 4 test() Hello GO 5 main() Hello Golang 5 ``` **说明** + 从运行结果可以看出,**主线程执行完毕后即使协程没有执行完毕,程序也会退出**; + 协程可以在主线程没有执行完毕前提前退出,协程是否执行完毕不会影响主线程的执行。 为了保证程序可以顺利执行,让协程执行完毕后在执行主线程退出,可以使用`sync.WaitGroup`等待协程执行完毕。 ### 三、sync.WaitGroup介绍 `sync.WaitGroup`用来实现启动一组`goroutine`,并等待任务做完再结束`goroutine`。 **sync.WaitGroup方法** | 方法 | 说明 | | ----------- | ------------------------------------------------------------ | | `wg.Add()` | main协程通过调用 `wg.Add(delta int)` 设置worker协程的个数,然后创建worker协程 | | `wg.Done()` | worker协程执行结束以后,都要调用 `wg.Done()`,表示做完任务,`goroutine`减1 | | `wg.Wait()` | main协程调用 `wg.Wait()` 且被block,直到所有worker协程全部执行结束后返回 | 针对可能panic的`goroutine`,可以使用`defer wg.Done()`来结束`goroutine`。 **示例** ```go import ( "fmt" "sync" "time" ) //主协程退出后所有协程无论有没有执行完毕都会退出,所以我们在主进程中可以通过WaitGroup等待协程执行完毕 var wg sync.WaitGroup func test03(){ for i := 0; i < 3; i++ { fmt.Println("test03 - Hello GO",i) time.Sleep(time.Millisecond * 100) } wg.Done() //协程计数器-1 } func test04(){ for i := 0; i < 3; i++ { fmt.Println("test04 - Hello Golang",i) time.Sleep(time.Millisecond * 100) } wg.Done() //协程计数器-1 } func main() { wg.Add(1) //协程计数器+1 go test03() //开启一个协程 wg.Add(1) //协程计数器+1 go test04() //开启一个协程 wg.Wait() //等待协程执行完毕 fmt.Println("主线程退出...") } ``` **运行结果** ```go test04 - Hello Golang 0 test03 - Hello GO 0 test04 - Hello Golang 1 test03 - Hello GO 1 test03 - Hello GO 2 test04 - Hello Golang 2 主线程退出... ``` ### 四、启动多个 Goroutine > 通过在主线程中使用for循环,可以启动多个`goroutine` > > 同时使用`sync.WaitGroup` 来实现等待 `goroutine` 执行完毕 ```go import ( "fmt" "sync" "time" ) var wg2 sync.WaitGroup func test05(num int){ defer wg2.Done() for i := 1;i <= 2;i++{ fmt.Printf("协程(%v)输出的第\t%v\t条数据\n", num, i) time.Sleep(time.Millisecond * 100) } } func main() { for i := 1; i <= 2; i++ { wg2.Add(1) go test05(i) } wg2.Wait() fmt.Println("主线程关闭...") } ``` **运行结果** ``` 协程(2)输出的第 1 条数据 协程(1)输出的第 1 条数据 协程(1)输出的第 2 条数据 协程(2)输出的第 2 条数据 主线程关闭... ``` 多次执行上面的代码,会发现每次打印的数字的顺序都不一致。这是因为`goroutine`之间是并发执行的,而 `goroutine` 的调度是随机的。 ### 五、设置 Golang 并行运行的时候占用的cup数量 + Go 运行时的调度器使用 `GOMAXPROCS` 参数来确定需要使用多少个OS 线程来同时执行Go代码。默认值是机器上的 CPU 核心数。 例如在一个 8 核心的机器上,调度器会把Go 代码同时调度到 8 个 OS 线程上。 + Go 语言中可以通过 `runtime.GOMAXPROCS()`函数设置当前程序并发时占用的CPU 逻辑核心数。 Go1.5 版本之前,默认使用的是单核心执行。Go1.5 版本之后,默认使用全部的CPU逻辑核心数。 **示例** ```go import ( "fmt" "runtime" ) func main() { //获取当前计算机上面的CPU个数 numCPU := runtime.NumCPU() fmt.Println("numCPU=",numCPU) //可以自己设置使用多个CPU runtime.GOMAXPROCS(numCPU - 1) fmt.Println("OK") } ``` ### 六、Goroutine 统计素数 #### 6.1 传统for循环实现 > 需求:要统计1-120000的数字中哪些是素数 ```go import ( "fmt" "time" ) func main() { start := time.Now().Unix() for num := 2;num < 120000;num++{ var flag = true for i := 2; i < num; i++ { if num % i == 0 { flag = false break } } if flag { //fmt.Println(num,"是素数") } } end := time.Now().Unix() fmt.Println(end - start) //15毫秒 } ``` #### 6.2 goroutine 开启多个协程统计 ```go import ( "fmt" "sync" "time" ) /* 1 协程 统计 1-30000 2 协程 统计 30001-60000 3 协程 统计 60001-90000 4 协程 统计 90001-120000 // start:(n-1)*30000+1 end:n*30000 */ var wg3 sync.WaitGroup func test06(n int){ for num := (n-1)*30000 + 1;num < n*30000; num++ { if num > 1 { var flag = true for i := 2; i < num; i++{ if num % i == 0 { flag = false break } } if flag { //fmt.Println(num,"是素数") } } } wg3.Done() } func main() { start := time.Now().Unix() for i := 1; i <= 4; i++ { wg3.Add(1) go test06(i) } wg3.Wait() fmt.Println("执行完毕") end := time.Now().Unix() fmt.Println(end - start) //3毫秒 } ``` 从两种方式对比来看,开启多个协程统计方式大大提升了性能。
标签:
Golang基础
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:
https://www.lilinchao.com/archives/2246.html
上一篇
【转载】20.Golang之GMP模型
下一篇
22.Golang之Channel
取消回复
评论啦~
提交评论
栏目分类
随笔
2
Java
326
大数据
229
工具
31
其它
25
GO
47
标签云
Netty
Eclipse
并发编程
Spark RDD
JavaWeb
MyBatis-Plus
SQL练习题
VUE
Nacos
Tomcat
Stream流
Quartz
Flume
微服务
Sentinel
高并发
Thymeleaf
Golang基础
Docker
SpringCloudAlibaba
随笔
MySQL
FileBeat
Typora
Livy
算法
MyBatisX
nginx
Hadoop
Redis
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞