Go语言基础
介绍
并发
介绍
- 本文介绍Go语言中 channel、goroutine、互斥锁、读写锁、原子操作、select、超时处理、sync包、runtime包等相关知识。
并发
- 进程是是最小的资源管理单元,属于操作系统对一个正在运行的程序的一种抽象。可以同时运行多个进程,每个进程都独立的使用系统资源。
- 线程属于轻量级进程,从属于进程,是 CPU 调度的最小单位。一个进程可以开启多个线程,各线程共享进程的部分资源,同时具有少量各自的资源。
- 协程又称微线程,纤程。不同于进程或线程,协程拥有自己的寄存器上下文和栈。通过语言层面支持,在用户态执行,避免操作系统多次系统状态切换。一个线程可以拥有多个协程。
- 并发主要目的是充分利用现代 CPU 多核处理器,通过合理的软件设计将原本顺序执行的任务变成并行执行的任务(同时处理),提高系统的吞吐率。
- 在 Go 语言中,每个并发执行的单元叫 goroutine,使用关键字 go 启动协程,可较容易开发高并发程序。
channel
- channel 是 go 语言中特殊的数据类型,可以看作队列(FIFO)。通道提供了一种安全、同步的方式来共享数据,所以通常用来在各个 goroutine 之间传输数据,数据在不同协程中的传输都是通过拷贝的形式完成。
- channel 按照接收和发送数据分为三种:
chan T // 发送和接收类型 T 的值
chan<- T // 发送类型 T 的值
<-chan T // 接收类型 T 的值
- channel 声明与初始化。
package main
import "fmt"
func main() {
// 声明通道,存储 []int 切片数据类型,默认为 nil
var c1 chan []int
fmt.Println("c1: ", c1)
// 使用 make 创建通道,默认容量为 0,无缓冲通道
c2 := make(chan int)
fmt.Println("c2: ", c2, "cap: ", cap(c2))
// 使用 make 创建通道,指定通道容量,有缓冲通道
c3 := make(chan int, 1)
fmt.Println("c3: ", c3, "cap: ", cap(c3))
// 初始化只发送通道
c4 := make(chan<- int)
fmt.Println("c4: ", c4, "cap: ", cap(c4))
// 初始化只接收通道
c5 := make(<-chan int)
fmt.Println("c5: ", c5, "cap: ", cap(c5))
}
- channel 发送操作: 将数据发送到 channel 中,语法为 ch <- data。
- channel 接收操作: 从 channel 中接收数据,语法为 data := <- ch。
- channel 关闭操作: 关闭 channel,语法为 close(ch)。
- 发送和接收操作都是阻塞的,当没有 goroutine 同时操作相同通道时,通道将一直阻塞(deadlock),直到有 goroutine 进行操作。
- 关闭操作是非阻塞的。若 channel 已经被关闭,仍然可以从中读取数据。
package main
import (
"fmt"
"time"
)
func main() {
// 初始化通道,存储 int 数据类型,无缓冲通道
c1 := make(chan int)
defer close(c1)
// 开启 goroutine 访问通道
go func() {
data := <-c1
fmt.Println("go func c1 len: ", len(c1), ", cap: ", cap(c1), ", data: ", data)
}()
// 发送数据到通道,此时需要其它 goroutine 访问此通道,否则死锁编译报错
c1 <- 1
fmt.Println("c1 len: ", len(c1), ", cap: ", cap(c1))
// main 函数延时 10 ms,goroutine 在 main 函数中启动,需要一点点时间
// 若启动过程中 main 函数退出,则 goroutine 也会退出,看不到结果输出
time.Sleep(time.Millisecond * 10)
}
- channel 有未关闭、已关闭和 nil 三种状态:
操作 | 未关闭 | 已关闭 | nil |
---|---|---|---|
发送 | 阻塞或成功发送 | panic | 永久阻塞 |
读取 | 阻塞或成功读取 | 成功读取或返回零值 | 永久阻塞 |
关闭 | 成功关闭 | panic | panic |
- 无并发情况下,无缓冲区通道发送与读取会报错误fatal error: all goroutines are asleep - deadlock!。
package main
import (
"fmt"
)
func main() {
// 初始化通道,存储 int 切片数据类型,无缓冲通道,以下编译报错
c1 := make(chan int)
// c1 := make(chan int, 1) // 应该使用有缓冲区通道
defer close(c1)
c1 <- 1
fmt.Println("c1 len: ", len(c1), ", cap: ", cap(c1))
<-c1
}
- channel 有缓冲区发送时,缓冲区数据容量满了后,继续发送数据会报错fatal error: all goroutines are asleep - deadlock!。
package main
import (
"fmt"
)
func main() {
// 初始化通道,存储 int 切片数据类型,有缓冲通道
c1 := make(chan int, 1)
// c1 := make(chan int, 2) // 应该预留两个缓冲区
defer close(c1)
c1 <- 1
fmt.Println("c1 len: ", len(c1), ", cap: ", cap(c1))
c1 <- 2
fmt.Println("c1 len: ", len(c1), ", cap: ", cap(c1))
<-c1
}
- channel 数据遍历。
package main
import "fmt"
func Test() {
// 初始化通道,存储 int 切片数据类型,无缓冲通道
c1 := make(chan int)
// 创建协程,发送数据
go func(c chan int) {
for i := 0; i < 5; i++ {
c1 <- 1 + i
}
close(c1)
}(c1)
// 使用 for range 遍历,实际上由于不知道通道中的数据个数,如果不在协程中关闭通道,
// 会一直遍历,通道中无数据时,报错 deadlock
for v := range c1 {
fmt.Println(v)
}
}
func Test2() {
// 初始化通道,存储 int 切片数据类型,无缓冲通道
c1 := make(chan int)
// 创建协程,发送数据
go func(c chan int) {
for i := 0; i < 5; i++ {
c1 <- 1 + i
}
}(c1)
// 使用普通 for 循环,知道发送多少个数据,此处接收多少个数据
for i := 0; i < 5; i++ {
fmt.Println(<-c1)
}
close(c1)
}
func main() {
Test()
Test2()
}
goroutine
- go 语言中,使用每个并发执行的单元叫 goroutine,启动 goroutine 使用 go 关键字,后边跟函数创建。可以在单个进程中执行成千上万的并发任务。
- go 的并发编程采用的 CSP (Communicating Sequential Process) 模型,通过 GMP 调度模型,在用户态实现了 M:N 的线程模型(M 个 goroutine 在 N 个线程上调度,go 语言实现了其调度机制)。golang 内置的调度器,可以充分利用多核 CPU 资源。
- 创建协程,由此例可知,使用 go 关键字创建 goroutine 非常容易,但观察输出结果可以发现,创建的 goroutine 执行顺序并不是按照创建顺序依次执行,多次执行此程序可以发现会输出多种不同的执行结果。
package main
import (
"fmt"
"time"
)
func Test(index int) {
fmt.Println("go Test ", index)
}
func main() {
// 使用匿名函数创建 goroutine
go func() {
fmt.Println("go func")
}()
// 使用函数创建多个 goroutine
for i := 0; i < 10; i++ {
go Test(i)
}
// 主协程等待工作协程执行
time.Sleep(time.Second)
}
输出结果
go func
go Test 9
go Test 0
go Test 1
go Test 2
go Test 3
go Test 4
go Test 5
go Test 6
go Test 7
go Test 8
- 使用 time.Sleep 函数等待工作协程结束,实际上并不能确定工作协程执行多长时间,所以 go 语言中提供等待组(sync.WaitGroup)来等待所有协程结束。
package main
import (
"fmt"
"sync"
)
func main() {
// 初始化等待组变量,默认内部计数为 0
wg := &sync.WaitGroup{}
// 由于知道创建多少个协程,所以此处直接将内部计数添加 10,
// 注意添加的数不能大于需要等待的协程数量,否则执行报错
wg.Add(10)
// 使用匿名函数创建 goroutine
for i := 0; i < 10; i++ {
// wg.Add(1) // 或创建协程时依次增加内部计数
go func() {
fmt.Println(i)
wg.Done()
}()
}
wg.Wait()
}
- 使用协程并发处理业务时,由于资源数据可能在不同的协程中访问修改,为了保持资源数据访问的正确性,使其运行符合预期逻辑流程,需要对共享资源数据(多个协程修改资源数据时)进行保护,达到同一时间资源数据(尤其是修改资源数据时)只能被一个协程访问。
- 下边的例程是未对共享资源数据做任何保护,多次执行从其结果分析,预期的结果输出不稳定,也就是说在多协程同时修改共享资源数据时,结果不一定符合预期,存在资源访问竞争。
package main
import (
"fmt"
"sync"
)
func main() {
// 初始化等待组变量,默认内部计数为 0
wg := &sync.WaitGroup{}
wg.Add(10) // 等待 10 个子协程
var index int32 = 0 // 共享变量
for i := 0; i < 5; i++ { // 创建协程 5 * 2 = 10 个
go func() { // 协程 1
for j := 0; j < 1000; j++ {
index++
}
wg.Done()
}()
go func() { // 协程 2
for j := 0; j < 1000; j++ {
index--
}
wg.Done()
}()
}
wg.Wait() // 主协程等待子协程结束
fmt.Println("index: ", index) // 打印修改后的最终值
}
输出结果
互斥锁
- Go语言中提供锁机制来处理数据竞争状态,基本的锁有互斥锁(Mutex)与读写锁(RWMutex),通过对资源加锁和释放锁提供对资源同步方式访问(同一时刻只能有一个协程访问)。
- 注意:使用互斥锁会影响程序性能(主要是程序执行速度),所以对性能要求较高的模块开发人员需要仔细斟酌取舍。
package main
import (
"fmt"
"sync"
)
func main() {
// 初始化等待组变量,默认内部计数为 0
wg := &sync.WaitGroup{}
lock := sync.Mutex{} // 初始化互斥锁
wg.Add(10) // 等待 10 个子协程
var index int32 = 0 // 共享变量
for i := 0; i < 5; i++ { // 创建协程 5 * 2 = 10 个
go func() { // 协程 1
for j := 0; j < 1000; j++ {
// atomic.AddInt32(&index, 1)
lock.Lock() // 加锁
index++
lock.Unlock() // 解锁
}
wg.Done()
}()
go func() { // 协程 2
for j := 0; j < 1000; j++ {
// atomic.AddInt32(&index, -1)
lock.Lock() // 加锁
index--
lock.Unlock() // 解锁
}
wg.Done()
}()
}
wg.Wait() // 主协程等待子协程结束
fmt.Println("index: ", index) // 打印修改后的最终值
}
读写锁
- Go 语言也提供读写锁来保证资源同步,此锁应用于读需求远大于写需求,也就是不需要频繁修改共享资源内容。读时加锁不影响,写时加锁保证资源数据同步性。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
// 初始化等待组变量,默认内部计数为 0
wg := &sync.WaitGroup{}
lock := sync.RWMutex{} // 初始化互斥锁
wg.Add(6) // 等待 6 个子协程
var index int32 = 0 // 共享变量
go func() { // 协程, 定时修改变量值
for j := 0; j < 5; j++ {
lock.Lock() // 加写锁
index++
lock.Unlock() // 解锁
time.Sleep(time.Second) // 定时 1 秒
}
wg.Done()
}()
for i := 0; i < 5; i++ { // 创建协程 5 个
go func() { // 协程 1
for {
lock.RLock() // 加读锁
fmt.Printf("%v, ", index)
lock.RUnlock() // 解锁
time.Sleep(time.Millisecond * 250) // 定时 0.25 秒
if index >= 5 { // 满足条件退出协程
break
}
}
wg.Done()
}()
}
wg.Wait() // 主协程等待子协程结束
fmt.Println("\nindex: ", index) // 打印修改后的最终值
}
输出结果
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
index: 5
原子操作
- 原子操作是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch(上下文切换)。
- go 语言中也提供原子操作用于解决并发编程中的资源数据竞争问题,在 sync/atomic 包提供了对基本数据类型的原子操作支持。
- 原子操作主要提供了五类操作函数:Add*(增加值)、Load*(读取值)、Store*(存储值)、Swap*(更新值)、CompareAndSwap*(比较第一个参数引用的值是否与第二个参数值相同,若相同则将第一个参数值更新为第三个参数,同时返回 true,若不相同,第一个参数值不变,返回 false)。
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
// 初始化等待组变量,默认内部计数为 0
wg := &sync.WaitGroup{}
wg.Add(10) // 等待 10 个子协程
var index int32 = 0 // 共享变量
for i := 0; i < 5; i++ { // 创建协程 5 * 2 = 10 个
go func() { // 协程 1
for j := 0; j < 1000; j++ {
atomic.AddInt32(&index, 1)
}
wg.Done()
}()
go func() { // 协程 2
for j := 0; j < 1000; j++ {
atomic.AddInt32(&index, -1)
}
wg.Done()
}()
}
wg.Wait() // 主协程等待子协程结束
fmt.Println("index: ", index) // 打印修改后的最终值
}
输出结果
select
- 对于 channel,当写入无缓冲区通道或有缓冲区通道已被写满时,如果继续写入通道会阻塞,直到通道中有数据被读取。同样的,当通道中无元素,继续读取也会阻塞,直到通道被写入数据。
- go 语言从语言层面提供了一种 IO 多路复用机制,可同时对多个通道进行监听(写入或读取)。select-case 是一种控制结构,写法有点类似于用于 switch 语句,实际规则不相同。
- 如果所有的 case 的 channel 都不可读或不可写,此时若有 default 分支会执行此分支然后退出 select 流程。
package main
import (
"fmt"
"time"
)
func main() {
// 定义两个通道
chan1 := make(chan int)
chan2 := make(chan int)
go func() { // 协程 1,向通道1中写入数据
time.Sleep(time.Second) // 延时 1 秒后写入数据
chan1 <- 1
}()
go func() { // 协程 2,向通道2中写入数据
for {
time.Sleep(time.Second) // 延时 1 秒后写入数据
chan2 <- 1
}
}()
select { // 主协程监听两个通道
case <-chan2:
fmt.Println("channel 2 ready.")
case <-chan1:
fmt.Println("channel 1 ready.")
default: // 默认分支
fmt.Println("default case")
}
fmt.Println("main function exit.")
}
输出结果
default case
main function exit.
- 若没有 default 分支,select 将阻塞所在协程,直至有可读或可写的通道为止。若存在多个 case 的 channel 可读或可写,则随机选择一个 case 进行处理,然后退出 select 流程。
package main
import (
"fmt"
"time"
)
func main() {
// 定义两个通道
chan1 := make(chan int)
chan2 := make(chan int)
go func() { // 协程 1,向通道1中写入数据
time.Sleep(time.Second)
chan1 <- 1
}()
go func() { // 协程 2,向通道2中写入数据
for {
time.Sleep(time.Second)
chan2 <- 1
}
}()
// 无 default 分支,会阻塞主协程,直到有通道可读或可写时执行 case 分支,然后退出 select 流程
select {
case <-chan2:
fmt.Println("channel 2 ready.")
case <-chan1:
fmt.Println("channel 1 ready.")
}
fmt.Println("main function exit.")
}
输出结果
- 若 select 语句块为空,则会阻塞 select 所在协程,遇到 panic 退出,若无通道,空 select 执行报错。
package main
import (
"fmt"
"time"
)
func main() {
// 定义两个通道
chan1 := make(chan int)
chan2 := make(chan int)
go func() { // 协程 1,向通道1中写入数据
for {
time.Sleep(time.Second)
chan1 <- 1
}
}()
go func() { // 协程 2,向通道2中写入数据
for {
time.Sleep(time.Second)
chan2 <- 1
}
}()
go func() { // 读取通道值,避免死锁
for {
<-chan1
<-chan2
}
}()
// 主协程在此阻塞,不能执行到最后一行的 fmt 语句
select {}
fmt.Println("main function exit.")
}
超时处理
- 在实际开发中,超时处理机制比较常见,可以通过 select、select + time.After、select + context 实现对执行操作超时的控制。
- select 方式实现超时处理机制。
package main
import (
"fmt"
"time"
)
func main() {
// 定义两个通道
chan1 := make(chan int)
timeout := make(chan bool)
go func() { // 协程 1,定时向通道1中写入数据
i := 100
for {
time.Sleep(time.Second)
i++
chan1 <- i
}
}()
go func() { // 协程 2,模拟 5 秒超时
time.Sleep(time.Second * 5)
timeout <- true
}()
for { // 循环监听通道
var tmout bool = false
select {
case data := <-chan1:
fmt.Printf("%v ", data)
case tmout = <-timeout:
fmt.Println("\ntimeout ", tmout)
}
if tmout { // 超时,退出监听通道
break
}
}
fmt.Println("main function exit.")
}
输出结果
101 102 103 104
timeout true
main function exit.
- 使用 select + time.After 方式实现超时处理机制。
package main
import (
"fmt"
"time"
)
func main() {
// 定义两个通道
chan1 := make(chan int)
go func() { // 协程 1,定时向通道1中写入数据
i := 100
var t int64 = 0
for {
t++
time.Sleep(time.Second * time.Duration(t)) // 模拟写数据延时 1,2,3...
i++
chan1 <- i
}
}()
for { // 循环监听通道
var tmout bool = false
select {
case data := <-chan1:
fmt.Printf("%v ", data)
case <-time.After(time.Second * 2): // 模拟 select 2 秒未处理通道表示超时
tmout = true
fmt.Println("\ntimeout ", tmout)
}
if tmout { // 超时,退出监听通道
break
}
}
fmt.Println("main function exit.")
}
输出结果
101
timeout true
main function exit.
- 使用 select + context 方式实现超时处理机制。
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 定义两个通道
chan1 := make(chan int)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) // 设置模拟 3 秒超时时间
defer cancel()
go func() { // 协程 1,定时向通道1中写入数据
i := 100
var t int64 = 0
for {
t++
time.Sleep(time.Second * time.Duration(t)) // 模拟写数据延时 1,2,3...
i++
chan1 <- i
}
}()
for { // 循环监听通道
var tmout bool = false
select {
case data := <-chan1:
fmt.Printf("%v ", data)
case <-ctx.Done(): // 模拟 3 秒时间超时
tmout = true
fmt.Println("\ntimeout ", tmout)
}
if tmout { // 超时,退出监听通道
break
}
}
fmt.Println("main function exit.")
}
输出结果
101 102
timeout true
main function exit.
sync包
- go 语言中的 sync 包是一个重要的同步原语库,它提供了一些基本的同步原语。包中括互斥锁、读写锁、原子变量、等待组、条件变量、单次执行、协程安全映射、对象池等。前边已经使用过互斥锁、读写锁、原子变量、等待组,接下来对其它几个使用例子进行简单介绍。
- 条件变量(sync.Cond)是用于多个goroutine之间进行同步和互斥的一种机制。如下例程实现了一个简单的生产者-消费者模型。
package main
import (
"fmt"
"sync"
"time"
)
var (
mtx sync.Mutex // 定义互斥锁
cond *sync.Cond // 定义条件变量
wg *sync.WaitGroup // 定义等待组
)
func Consumer(stop *bool, ch chan int, id int) {
cond.L.Lock() // 加锁
defer cond.L.Unlock() // 延迟解锁
for !(*stop) {
cond.Wait() // 等待条件变量触发
fmt.Println("go ", id, ": ", <-ch)
}
fmt.Println("exit ", id, "Consumer!!!")
wg.Done()
}
func Producer(stop *bool, ch chan int) {
for i := 0; i < 8; i++ {
time.Sleep(time.Second)
ch <- i + 25 // 写入数据
cond.L.Lock() // 加锁
cond.Signal() // 通知一个消费者
cond.L.Unlock() // 解锁
}
*stop = true
fmt.Println("exit Producer!!!")
wg.Done()
close(ch) // 关闭通道
cond.L.Lock()
cond.Broadcast() // 通知所有消费者
cond.L.Unlock()
}
func main() {
cond = sync.NewCond(&mtx) // 初始化条件变量
stop := false
ch := make(chan int, 10)
wg = &sync.WaitGroup{}
wg.Add(6)
go Producer(&stop, ch) // 启动生产者
for i := 0; i < 5; i++ { // 创建 5 个消费者
go Consumer(&stop, ch, i)
}
wg.Wait() // 等待所有协程结束
fmt.Println("exit main function")
}
输出结果
go 4 : 25
go 0 : 26
go 1 : 27
go 2 : 28
go 3 : 29
go 4 : 30
go 0 : 31
exit Producer!!!
go 1 : 32
exit 1 Consumer!!!
go 0 : 0
exit 0 Consumer!!!
go 3 : 0
exit 3 Consumer!!!
go 2 : 0
exit 2 Consumer!!!
go 4 : 0
exit 4 Consumer!!!
exit main function
- 单次执行(sync.Once)保证某个操作只执行一次。如下例程模拟初始化环境函数只执行一次的场景。
package main
import (
"fmt"
"sync"
)
var once sync.Once
func InitEnv() { // 此初始化环境函数全局只执行一次
fmt.Println("InitEnv")
}
func Test(wg *sync.WaitGroup, id int) { // 协程测试函数
once.Do(InitEnv) // 调用执行函数
fmt.Printf("Test %v\t", id)
wg.Done()
}
func main() {
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ { // 开启 10 个测试协程
go Test(&wg, i)
}
wg.Wait()
fmt.Println("\nexit main function")
}
输出结果
InitEnv
Test 0 Test 9 Test 1 Test 2 Test 3 Test 4 Test 5 Test 6 Test 7 Test 8
exit main function
- go 语言内置的 map 数据类型不是并发安全的,在 sync 包中提供(go 1.9 及之后)的协程安全映射(sync.Map)是并发安全的。sync.Map 的读取、插入、删除一般都是常数级的时间复杂度。
- sync.Map 的零值是有效的,就是一个空 map。适用于读多写少的场景,当 sync.Map 在第一次使用之后,就不允许被拷贝。
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// 写入
m.Store("Intel", 10000)
m.Store("AMD", 10001)
// 读取
str, ok2 := m.Load("Intel")
fmt.Println("read: ", ok2, "value: ", str.(int))
// 遍历
m.Range(func(key, value any) bool {
name := key.(string)
id := value.(int)
fmt.Println("name: ", name, "id: ", id)
return true
})
// 删除
m.Delete("AMD")
str1, ok3 := m.Load("AMD")
if ok3 {
fmt.Println("read: ", ok3, "value: ", str1.(int))
} else {
fmt.Println("read: ", ok3, "value: ", "***")
}
// 读取或写入
m.LoadOrStore("Intel+", 10001)
str2, ok4 := m.Load("Intel+")
fmt.Println("read: ", ok4, "value: ", str2.(int))
fmt.Println(m)
}
输出结果
read: true value: 10000
name: Intel id: 10000
name: AMD id: 10001
read: false value: ***
read: true value: 10001
{{0 0} {[] {} 0xc0000240f0} map[Intel:0xc000056028 Intel+:0xc000056040] 1}
- 对象池,可以理解为存储对象的容器,对于很多需要重复分配、回收内存的地方,对象池(sync.Pool)是一个很好的选择。因为频繁的创建对象和回收内存,垃圾回收机制(GC)会频繁调用从而影响程序性能,可以提前创建一些对象放入对象池中,若需要使用对象时从对象池中获取,使用完成还给对象池,这样避免运行时使用时创建对象,用完时GC清理对象的开销。
package main
import (
"fmt"
"sync"
)
var pool *sync.Pool
type Cpu struct {
Name string
Id int
}
func Init() {
}
func main() {
pool = &sync.Pool{
New: func() any { // 创建 New 函数
fmt.Println("create a new Cpu object")
return new(Cpu)
},
}
p := pool.Get().(*Cpu) // 获取对象
fmt.Println("get object: ", p)
// 修改对象属性
p.Name = "Intel"
p.Id = 10000
fmt.Println("p value: ", p)
pool.Put(p) // 将对象放入对象池中
fmt.Println("get object 1: ", pool.Get().(*Cpu)) // 可以获取到对象
fmt.Println("get object 2:", pool.Get().(*Cpu)) // 获取到临时对象
p = pool.Get().(*Cpu)
p.Name = "AMD"
p.Id = 10010
fmt.Println("p value: ", p)
pool.Put(p)
fmt.Println("get object3: ", pool.Get().(*Cpu)) // 获取对象
fmt.Println("get object 4:", pool.Get().(*Cpu)) // 获取对象
}
输出结果
create a new Cpu object
get object: &{ 0}
p value: &{Intel 10000}
get object 1: &{Intel 10000}
create a new Cpu object
get object 2: &{ 0}
create a new Cpu object
p value: &{AMD 10010}
get object3: &{AMD 10010}
create a new Cpu object
get object 4: &{ 0}
runtime包
- go 语言中提供 runtime 包,用于和程序的运行时环境进行交互,包中提供了一系列函数和变量方便控制、管理和监视程序的执行情况。
常用操作函数 | 描述 |
---|---|
Gosched | 当前 goroutine 让出时间片 |
GOROOT | 获取 Go 安装路径 |
NumCPU | 获取可使用的逻辑 CPU 数量 |
GOMAXPROCS | 设置当前进程可使用的逻辑 CPU 数量 |
NumGoroutine | 获取当前进程中 goroutine 的数量 |
Goexit | 退出当前 goroutine(defer语句会照常执行) |
GOOS | 获取目标操作系统 |
GC | 执行垃圾回收机制 |
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func main() {
fmt.Println("GOOS: ", runtime.GOOS)
fmt.Println("GOROOT: ", runtime.GOROOT())
fmt.Println("NumCPU: ", runtime.NumCPU())
fmt.Println("GOMAXPROCS: ", runtime.GOMAXPROCS(8)) // 返回上一次设置值
wg := &sync.WaitGroup{}
wg.Add(2)
go func() {
defer func() { // 协程退出时执行此函数
fmt.Println("go func eixt!!!")
wg.Done()
}()
for { // 协程死循环,正常运行时此协程会一直运行直到程序退出
time.Sleep(time.Second * 2)
runtime.Goexit() // 主动退出协程
}
}()
go func() {
for i := 0; i < 5; i++ {
runtime.Gosched() // 让出协程时间片
}
wg.Done()
}()
fmt.Println("NumGoroutine: ", runtime.NumGoroutine())
runtime.GC() // 主动调用垃圾回收机制
wg.Wait()
}
输出结果
GOOS: windows
GOROOT: C:\Program Files\Go
NumCPU: 12
GOMAXPROCS: 12
NumGoroutine: 3
go func eixt!!!
- GC(Garbage Collection 的缩写)表示一种自动的存储器管理机制。当一个计算机上的动态存储器不再需要时,就应该将其释放,让出存储器,这种存储器资源管理,称为垃圾回收。
- Go 语言中提供 GC 机制,可以自动管理程序内存,释放不再使用的内存,帮助开发者管理内存,尽量使其避免一些常见的内存错误。Go 语言的GC由早起的标记-清除(mark and sweep)算法到后来的三色标记 + 混合写屏障算法,优化 GC 工作时尽量减少对主程序的影响。
- GC 的触发方式一般有三种,第一种是内存分配达到当前内存分配的阈值(动态变化,一般下一次会设置为当前垃圾集的2倍时会再次调用 GC)时自动触发;第二种是定时(一般为 2 分钟)自动触发;第三种事手动调用触发(runtime.GC()函数)。
- GC 调优:读程序代码进行内存优化,尽量减少额外内存开销,调整 GOGC(Go 为了保证使用 GC 的简洁性,只提供了一个参数 GOGC,减少 GOGC 对 CPU 的占用)。实际开发过程中,一般都会对程序内存优化,所以大多数情况下不需要关注 GC 相关。
起始