目录
3、runtime包Golang进程权限调度包runtimehttps://www.cnblogs.com/wt645631686/p/9656046.html
runtime.Gosched():让出CPU时间片,重新等待安排任务
runtime.GOMAXPROCS(),需要使用多少个OS线程来同时执行Go代码
1、概念:
进程和线程
A. 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
C.一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行。
并发和并行
A. 多线程程序在一个核的cpu上运行,就是并发。
B. 多线程程序在多个核的cpu上运行,就是并行。
协程和线程
协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。
线程:一个线程上可以跑多个协程,协程是轻量级的线程。
2、goroutine
goroutine 高并发,奉行通过通信来共享内存,而不是共享内存来通信。
goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。
把这个任务包装成一个函数,开启一个goroutine去执行这个函数就可以了
一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。
2.1 使用goroutine
package main
import (
"fmt"
"sync"
"time"
)
func hello() {
fmt.Println("Hello Goroutine!")
}
// 启动多个goroutine
// 使用了sync.WaitGroup来实现goroutine的同步
var wg sync.WaitGroup
func hellosync(i int) {
defer wg.Done() // goroutine结束就登记-1
fmt.Println("Hello Goroutine!", i)
}
func main() {
go hello() 启动另外一个goroutine去执行hello函数
fmt.Println("main goroutine done!")
//只会打印main goroutine done!
//因为当main()函数结束时,goroutine就一同结束了,还来不及执行打印
time.Sleep(time.Second)
// main goroutine done!
// Hello Goroutine!
for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个goroutine就登记+1
go hellosync(i)
}
wg.Wait() // 等待所有登记的goroutine都结束
}
主携程退出,则其他任务不执行
package main
import (
"fmt"
"time"
)
func main() {
// 合起来写
go func() {
i := 0
for {
i++
fmt.Printf("new goroutine: i = %d\n", i)
time.Sleep(time.Second)
}
}()
i := 0
for {
i++
fmt.Printf("main goroutine: i = %d\n", i)
time.Sleep(time.Second)
if i == 2 {
break
}
}
}
1.1.1. goroutin
main goroutine: i = 1
new goroutine: i = 1
new goroutine: i = 2
main goroutine: i = 2
2.2 可增长的栈
OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈不是固定的,他可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,虽然极少会用到这个大。所以在Go语言中一次创建十万左右的goroutine也是可以的。
2.3 goroutine调度
GPM是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程。
- 1.G很好理解,就是goroutine,里面除了存放本goroutine信息外 还有与所在P的绑定等信息。
- 2.Processor管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。P的个数是通过runtime.GOMAXPROCS设定(最大256)
- 3.M(machine)(thread)是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系, 一个groutine最终是要放到M上执行的;
P与M一般也是一一对应的。他们关系是: P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。
goroutine的优势:
- Go运行时(runtime)自己的调度器调度的
- goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池,充分利用多核优势
3、runtime包Golang进程权限调度包runtimehttps://www.cnblogs.com/wt645631686/p/9656046.html
runtime.Gosched():让出CPU时间片,重新等待安排任务
package main
import (
"fmt"
"runtime"
)
func main() {
go func(s string) {
for i := 0; i < 2; i++ {
fmt.Println(s)
}
}("world")
// 主协程
for i := 0; i < 2; i++ {
// 切一下,再次分配任务
//主携程让出时间片,先执行了另一个
runtime.Gosched()
fmt.Println("hello")
}
}
//world
//world
//hello
//hello
runtime.Goexit():终止携程
调用此函数会立即使当前的goroutine的运行终止(终止协程),而其它的goroutine并不会受此影响。
终止当前goroutine前会先执行此goroutine的还未执行的defer语句
不能再主函数调用runtime.Goexit,因为会引发panic
package main
import (
"fmt"
"runtime"
)
func main() {
go func() {
defer fmt.Println("A.defer")
func() {
defer fmt.Println("B.defer")
// 结束协程
runtime.Goexit()
defer fmt.Println("C.defer") //没执行
fmt.Println("B") //没执行
}()
fmt.Println("A") //没执行
}()
for {
}
}
//输出
//B.defer
//A.defer
runtime.GOMAXPROCS(),需要使用多少个OS线程来同时执行Go代码
默认值是机器上的CPU核心数,用来设置可以并行计算的CPU核数最大值,并返回之前的值。
package main
import (
"fmt"
"runtime"
"time"
)
func a() {
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
}
}
func b() {
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
}
}
func main() {
num := runtime.GOMAXPROCS(1)
go a()
go b()
fmt.Println(num) //输出了之前的值:8
time.Sleep(time.Second)
}
//做完一个任务再做另一个任务,而且是B先,先进后出?
8
B: 1
B: 2
B: 3
B: 4
B: 5
B: 6
B: 7
B: 8
B: 9
A: 1
A: 2
A: 3
A: 4
A: 5
A: 6
A: 7
A: 8
A: 9
两个逻辑核心
package main
import (
"fmt"
"runtime"
"time"
)
func a() {
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
}
}
func b() {
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
}
}
func main() {
runtime.GOMAXPROCS(2)
go a()
go b()
time.Sleep(time.Second)
}
B: 1
A: 1
A: 2
A: 3
A: 4
A: 5
A: 6
A: 7
A: 8
A: 9
B: 2
B: 3
B: 4
B: 5
B: 6
Go语言中的操作系统线程和goroutine的关系:
- 1.一个操作系统线程对应用户态多个goroutine。
- 2.go程序可以同时使用多个操作系统线程。
- 3.goroutine和OS线程是多对多的关系,即m:n。
4、Channel:函数与函数间交换数据
问题:可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。
Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。
channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。
channel遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
4.1 channel的基本使用:初始化,三种操作
package main
import (
"fmt"
"sync"
)
func recv(c chan int) {
ret := <-c
fmt.Println("接收成功", ret)
close(c)
fmt.Println("通道c关闭成功")
}
func main() {
//1、创建channel
var ch chan int
//通道是引用类型,通道类型的空值是nil
fmt.Println(ch) // <nil> 声明的通道后需要使用make函数初始化之后才能使用。
//make函数初始化
ch1 := make(chan int)
fmt.Println(ch1) //0xc000060060
go recv(ch1) // 启用goroutine从通道接收值
//2、三种操作
//2.1 发送(send)
ch1 <- 10 // 把10发送到ch中,能编译,但执行会出现deadlock错误
//因为是无缓冲通道,必须要有人接收,会在这里死锁
//解决办法1:启用一个goroutine去接收值,recv(ch)启动要在发送数据前,不然还是错
fmt.Println("解决办法1发送成功")
//解决办法2:使用 有缓冲通道
ch2 := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
//可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量,但少用
ch2 <- 10
fmt.Println("解决办法2发送成功")
//2.2 接收(receive),ret := <-c
//2.3 关闭(close) close(c)
//如果你的管道不往里存值或者取值的时候一定记得关闭管道
//close(ch1) //报错了?why是因为接收方已经关闭了吗
//fmt.Println("ch1关闭成功")
close(ch2)
fmt.Println("ch2关闭成功")
}
<nil>
0xc000060060
接收成功 10
通道c关闭成功
解决办法1发送成功
解决办法2发送成功
ch2关闭成功
4.2 两种方法在接收值的时候判断通道是否被关闭
package main
import (
"fmt"
)
// channel 练习
// 有两种方式在接收值的时候判断通道是否被关闭,常用for range
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
// 开启goroutine将0~50的数发送到ch1中
go func() {
for i := 0; i < 50; i++ {
ch1 <- i
}
close(ch1)
}()
// 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中
go func() {
for {
i, ok := <-ch1 // 方法1:通道关闭后再取值ok=false
if !ok {
break
}
ch2 <- i * i
}
close(ch2)
}()
// 在主goroutine中从ch2中接收值打印
for i := range ch2 { // 方法2:通道关闭后会退出for range循环
fmt.Println(i)
}
}
4.3 单向通道(限制只能收or发)
1.chan<- int是一个只能发送的通道,可以发送但是不能接收;
2.<-chan int是一个只能接收的通道,可以接收但是不能发送。
3.双向通道可以转为单向通道,反之不行
func counter(out chan<- int) {
for i := 0; i < 100; i++ {
out <- i
}
close(out)
}
func squarer(out chan<- int, in <-chan int) {
for i := range in {
out <- i * i
}
close(out)
}
func printer(in <-chan int) {
for i := range in {
fmt.Println(i)
}
}
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go counter(ch1)
go squarer(ch2, ch1)
printer(ch2)
}
4.4 通道异常总结
5、Goroutine池
- 本质上是生产者消费者模型
- 可以有效控制goroutine数量,防止暴涨
例题:(还是有点不太明白为什么会deadlock)
- 计算一个数字的各个位数之和,例如数字123,结果为1+2+3=6
- 随机生成数字进行计算
package main
import (
"fmt"
"math/rand"
"sync"
)
type Job struct {
Id int
RandNum int
}
type Result struct {
//这里必须传对象实例??
job *Job
sum int
}
// 使用等待组来等待所有协程完成
var wg sync.WaitGroup
var resultWg sync.WaitGroup
// 创建工作池
// 参数1:开几个协程
func createPool(num int, jobChan chan *Job, resultChan chan *Result) {
for i := 0; i < num; i++ {
wg.Add(1)
go func(jobChan chan *Job, resultChan chan *Result) {
defer wg.Done()
//获取job管道的数据
for job := range jobChan {
r_num := job.RandNum
//执行加操作
var sum int
for r_num != 0 {
tmp := r_num % 10
sum += tmp
r_num /= 10
}
//得到结果
r := &Result{job, sum}
//将结果传入管道
resultChan <- r
}
}(jobChan, resultChan)
}
}
func main() {
//生成随机数管道,需要记录job的id
jobChan := make(chan *Job, 128) //有缓冲通道
//计算结果管道
resultChan := make(chan *Result, 128)
//生成随机数,输入到管道
for id := 0; id < 100; id++ {
job := &Job{id, rand.Int()}
jobChan <- job
}
//创建工作池
createPool(16, jobChan, resultChan)
// 关闭jobChan,告诉协程不会再有新的工作,没有这个会报All goroutines are asleep, deadlock
close(jobChan)
//打印协程
resultWg.Add(1)
go func(resultChan chan *Result) {
defer resultWg.Done() // 打印协程完成后减少等待组计数
for result := range resultChan {
fmt.Printf("job id:%v randnum:%v result:%d\n", result.job.Id,
result.job.RandNum, result.sum)
}
}(resultChan) //匿名函数传参
// 等待所有工作协程完成
wg.Wait()
// 关闭 resultChan,告诉打印协程不再需要结果
close(resultChan)
// 等待打印协程完成
resultWg.Wait()
}
如果你不关闭 jobChan
,那么工作协程将一直等待从 jobChan
中接收任务,而主程序中也没有向 jobChan
中发送更多的任务,这将导致工作协程一直等待下去,最终出现死锁。因此,在使用有缓冲通道时,需要在合适的时机关闭通道以告知接收方没有更多的数据。
6、定时器
原理:传参是告诉 Timer 需要等待多长时间,Timer 是带有一个缓冲的 channal,在定时时间到达之前,没有数据写入 Timer.C,读取操作会阻塞当前协程,到达定时时间时,会向 channel 写入数据(当前时间),阻塞解除,被阻塞的协程得以恢复运行,达到延时或者定时执行的目的。Golang 定时器使用方法汇总-CSDN博客
type Timer struct {
C <-chan Time
r runtimeTimer
}
6.1 Timer基本使用
package main
import (
"fmt"
"time"
)
//定时器使用
func main() {
//1.timer基本使用
t1 := time.Now()
fmt.Println("now time1: ", t1.Format("2006-01-02 15:04:05"))
t2 := time.NewTimer(2 * time.Second)
t3 := <-t2.C 超时时间还没到时,协程会阻塞;超时时间到了之后会返回当前时间
fmt.Println("now time2: ", time.Now().Format("2006-01-02 15:04:05"))
fmt.Println("now time3: ", t3.Format("2006-01-02 15:04:05"))
// now time1: 2023-11-01 21:50:04
// now time2: 2023-11-01 21:50:06
// now time3: 2023-11-01 21:50:06
//2、timer.After
//After() 函数接受一个时长 d,然后 After() 等待 d 时长,等待时间到后,将等待完成时所处时间点写入到 channel 中并返回这个只读 channel。
//NewTimer(d).C 和after 每次都是 return 了一个新的对象,已踩坑
t4 := time.After(time.Second * 2)
fmt.Println("now time: ", time.Now().Format("2006-01-02 15:04:05"))
time.Sleep(time.Second * 4)
chant := <-t4
fmt.Println("now time: ", chant.Format("2006-01-02 15:04:05"))
fmt.Println("now time: ", time.Now().Format("2006-01-02 15:04:05"))
// now time: 2023-11-01 21:42:08
// now time: 2023-11-01 21:42:10
// now time: 2023-11-01 21:42:12
//3、time.Reset()
//<-t2.C 使用会报错,即timer只能响应1次,想要重新使用就需要reset
//如果调用 time.Reset() 和 time.Stop() 时,timer 已过期或者已停止,则会返回 false
fmt.Println("now time: ", time.Now().Format("2006-01-02 15:04:05"))
t5 := time.NewTimer(4 * time.Second)
//ok := t2.Reset(2 * time.Second) //返回false
ok := t5.Reset(2 * time.Second)
fmt.Println("ok: ", ok)
<-t5.C
fmt.Println("now time: ", time.Now().Format("2006-01-02 15:04:05"))
// now time: 2023-11-01 21:52:09
// ok: true
// now time: 2023-11-01 21:52:11
//4、time.Stop()
fmt.Println("now time: ", time.Now().Format("2006-01-02 15:04:05"))
t6 := time.NewTimer(4 * time.Second)
t6.Stop() //停止定时器
fmt.Println("now time: ", time.Now().Format("2006-01-02 15:04:05"))
}
6.2 Ticker
Ticker 是一个周期触发定时的定时器,按给定时间间隔往 channel 发送系统当前时间,而 channel 的接收者可以以固定的时间间隔从 channel 中读取
package main
import (
"fmt"
"time"
)
func main() {
for {
select {
case <-time.Tick(2 * time.Second):
fmt.Println("2 second over:", time.Now().Second())
case <-time.After(7 * time.Second):
fmt.Println("5 second over, timeover", time.Now().Second())
return
}
}
}
start second: 14
1 second over: 15
1 second over: 16
1 second over: 17
1 second over: 18
1 second over: 19
1 second over: 20
1 second over: 21
7 second over: 21
package main
import (
"fmt"
"time"
)
func main() {
//创建定时器,每隔1秒后,定时器就会给channel发送一个事件(当前时间)
ticker := time.NewTicker(time.Second * 1)
i := 0
go func() {
for { //循环
<-ticker.C
i++
fmt.Println("i = ", i)
if i == 5 {
ticker.Stop() //停止定时器
}
}
}() //别忘了()
//死循环,特地不让main goroutine结束
for {
}
}