Go语言学习之路-12-并发(1)-goroutine
概念回顾
进程/线程
进程是程序在操作系统中的一次执行过程,每次程序执行的时候操作系统都会给这个程序打一个标识:资源、ID,它是一个独立的单位
线程是进程的一个执行实体,是 CPU 调度和分派的基本单位
一个进程可以创建和撤销多个线程,同一个进程中的多个线程之间可以并发执行
并发/并行
拿两个任务来说:
并发:1个CPU,通过时间的切换来干两件事,同一时刻只有一件事情能做:9:10分任务1在占用CPU,那么任务2就的等待(下图1)
并行:2个CPU,两件事同时做各自用各自的CPU,同一时刻两件事情都在做(下图2)
go语言并发
Go 语言通过编译器运行时(runtime),从语言上支持了并发的特性。Go 语言的并发通过 goroutine 特性完成。goroutine 类似于线程,但是可以根据需要创建多个 goroutine 并发工作。goroutine 是由 Go 语言的运行时调度完成,而线程是由操作系统调度完成。
为什么是goroutine
资源占用少
- Linux操作系统栈默认是8M, Go语言层面实现的goroutine会以一个很小的栈开始其生命周期,一般只需要2kb,资源占用很小
- goroutine栈是动态的最大有1GB当然如果出现这种情况你的程序就有问题了一半情况下用不到
- 基于上面的情况对于go程序来说,同时创建成百上千个goroutine是非常普遍的
调度更快
- Linux线程会被操作系统内核调度,有一个硬件计时器需要不断的切换、从寄存器存取数据进行调度
- Go在运行时包含了其自己的调度器,和操作系统的线程调度不同的是,Go调度器并不是用一个硬件计时器而是被Go语言本身进行调度的。他不需要进行硬件中断和内核调度而是go语言本身进行调度,相对更快
goroutine和线程的关系
goroutine 是 Go语言中的轻量级线程实现,由 Go 运行时(runtime)管理。Go 程序会智能地将 goroutine 中的任务启动合理的线程,并合理地分配给每个 CPU。
所以你只需要把任务进行封装,然后调用goroutine对外暴露的函数: go 就可以快速的启动一或者N个goroutine,来帮你完成并发工作
使用goroutine
通过go关键字来调用函数就启用了一个goroutine
go func()
创建goroutine
package main
import (
"fmt"
"time"
)
var input string
func main() {
// 启动一个goroutine,当他运行完逻辑后会正常退出
go run()
fmt.Printf("这是main函数执行的内容.....\n")
}
func run(){
for i := 0; i <4 ; i++ {
fmt.Printf("goroutine运行中....当前数字:%d\n", i)
time.Sleep(time.Second)
}
fmt.Println("run 函数执行完毕退出!!")
}
现在能做什么
并发获取数据
使用并发前
- 比如我有一个获取网站信息的代码
- 每次请求花费5秒 "func square"
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func main() {
for i := 1; i < 21; i++ {
fmt.Printf("%v\n", square(i))
}
}
func square(i int)(result int){
time.Sleep(time.Second * 5)
return i * i
}
使用并发后
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func main() {
for i := 1; i < 21; i++ {
go func(num int) {
fmt.Printf("%v\n", square(num))
}(i)
}
}
func square(i int)(result int){
time.Sleep(time.Second * 5)
return i * i
}
有什么问题
上面这个压测实例,如果不在主函数(main函数)最后增加,下面这一行就会出现问题
// 这里必须等待几秒钟
// 因为main函数不会等待goroutine的执行才推出
// 这个后面我们在并发控制的时候去说如何优化
time.Sleep(time.Second * 6)
运行上面的函数看看会出现下面的问题: 问题goroutine还没执行完程序就退出了
goroutine还没执行完程序就退出了解决方法
sync.WaitGroup 等goroutine运行完在退出(监工)
之前在没有使用WaitGroup的时候,main函数就想当于老板,老板一离开公司,goroutine就全不干活了
sync.WaitGroup就像老板请的监工,没当来一个goroutine就记录1次,当它干完活在记录一次,直到所有的goroutine都干完活,才下班走人
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
for i := 1; i < 21; i++ {
go func(num int) {
fmt.Printf("%v\n", square(num))
wg.Done()
}(i)
wg.Add(1)
}
wg.Wait()
}
func square(i int)(result int){
return i * i
}
sync.WaitGroup 有3个方法(var wg sync.WaitGroup 创建一个wg)
- wg.Add(1) 每当启动一个goroutine就+1,
**知晓当前有多少个goroutine**
- wg.Done() 在goroutine的运行函数内,最后执行完后 -1,
**就知道有多少个goroutine运行完毕了**
- wg.Wait() 在主main函数内等待,所有的goroutine运行完毕后在退出
上面就是sync.WaitGroup的主要方法和作用
并发的问题
sync.Wait解决了goroutine整体状态退出的问题
并发操作一个变量(把结果进行汇总)出现互相覆盖的问题,看栗子
- 把并发的结果汇总,写入到一个变量内
- 然后在进行操作
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
// 第1步: 创建一个共享变量
ret := []int{}
for i := 1; i < 21; i++ {
// 第2步: 并发请求并把结果写入到共享变量
go func(num int) {
ret = append(ret, square(i))
wg.Done()
}(i)
wg.Add(1)
}
wg.Wait()
// 第3步: 把结果进行汇总操作
fmt.Printf("平方Ret结果是:%v \n", ret)
}
func square(i int)(result int){
return i * i
}
结果: 平方Ret结果是:[9 144 144 144 144 169 441 441 441] # 这个结果时错误的
当前并发的问题
通过上面的例子我们可以看出来
- 存在互相竞争、相互覆盖的问题,所有goroutine都拿到了这个变量然后同时写入,(相互覆盖)最后一个写入的是大家看到的结果
go语言中给的解决方案是:通过通信来解决它
- 我不关注goroutine的执行顺序
- goroutine也不直接操作最终的变量
- goroutine内把执行完的结果通过channel发消息,发出去后,有专门接收的步骤去处理它
这样就可以用生产者消费者模型来处理并发数据,并发请求的相当于生产者,我们在启动一个消费者来把生产的数据进行整理
看下一篇channel~