李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
GO
正文
21.Golang协程介绍
Leefs
2022-07-12 PM
1411℃
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://lilinchao.com/archives/2246.html
上一篇
【转载】20.Golang之GMP模型
下一篇
22.Golang之Channel
评论已关闭
栏目分类
随笔
2
Java
326
大数据
229
工具
31
其它
25
GO
47
NLP
4
标签云
并发编程
DataX
工具
Kibana
Thymeleaf
高并发
Elasticsearch
链表
GET和POST
Azkaban
MyBatis-Plus
JavaWeb
nginx
Quartz
数学
算法
字符串
Spring
JavaWEB项目搭建
Spark Streaming
微服务
Livy
Spark Core
gorm
SpringCloudAlibaba
FileBeat
Golang
人工智能
HDFS
CentOS
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞
评论已关闭