本文一切实例代码运转go版别:go version go1.18.10 windows/amd64

1 并发编程介绍

1.1 串行、并发、并行

  • 串行:一切使命一件一件做,按照事先的次序顺次履行,没有被履行到的使命只能等候。最终履行完的时刻等于各个子使命之和。

Go并发编程 Goroutine、Channel、Select、Mutex锁、sync、Atomic等

  • 并发:是以交替的办法运用等候某个使命的时刻来处理其他使命核算逻辑,在核算机中,例如一个单核CPU,会经过时刻片算法,来高效合理的分配cpu核算资源。从用户视点来看似乎是多个使命在一起履行。

Go并发编程 Goroutine、Channel、Select、Mutex锁、sync、Atomic等

  • 并行:在同一时刻处理核算多个使命,以多核CPU为例,能够完结一起处理核算多个使命,一个CPU担任一个使命的核算逻辑,大家做到一起进行,就像三个使命有三个工人一起干活相同。

Go并发编程 Goroutine、Channel、Select、Mutex锁、sync、Atomic等

1.2 进程、线程、协程

  • 进程:是程序运转的根本单位,每个进程都有自己的独立内存空间,不同的进程能够经过进程之间的彼此通讯进行交流。比如:电脑上的 QQ、微信、WPS等都有各自的进程。在操作体系等级来看,进程是操作体系对一个正在运转的程序的一种笼统。一个体系里边能够一起运转多个进程,而每一个进程又好像是在独占的运用硬件资源,经过处理器在进程之间不停的切换来完结。
  • 线程:线程是处理器(CPU)资源分配和调度的根本单位,在一个进程中能够有多个线程,每个线程都运转在进程的环境上下文中,不同线程之间能够经过线程之间通讯进行数据交换。比如:在360安全卫视进程中,你能够一起进行废物清理和病毒查杀,在微信中,你能够刷朋友圈一起接纳音讯。
  • 进程和线程差异
    • 创立和开支方面,进程的创立需求体系分配内存和CPU,文件句柄等资源,毁掉时也要进行相应的回收,所以进程的办理开支很大;可是线程的办理开支则很小。
    • 进程之间不会彼此影响;而一个线程溃散或许会导致进程溃散,从而影响同个进程里边的其他线程。
    • 线程是进程的子使命,是处理器(CPU)分配和调度的根本单位,进程是对运转时程序的封装,是体系进行资源分配和调度的根本单元。
  • 协程:在理解协程之前,需求明白线程的几个问题:
    • 1、在履行过程中分为用户态和内核态,两个状况的切换会造成资源开支;
    • 2、面线程创立的越多,CPU切换的就越频频,由于操作体系的调度要确保相对公正
    • 3、线程的创立、毁掉都需求调用体系调用,每次请求都创立,高并发下开支就显得很大,而且线程的数量不能太多,占用内存是 MB 等级。
    • 根据上面的问题,协程被提出,协程是用户态(用户空间)的一种笼统,对操作体系内核而言并没有这个概念,依然是以线程维度调度。协程的主要思维是在用户态完结调度算法,来达到用少数线程,处理很多使命的调度,由于是用户态调度切换,不触及内核切换和不同线程之间的上下文切换,大大减少开支。

最后来一个图描绘三者之间的联系:

Go并发编程 Goroutine、Channel、Select、Mutex锁、sync、Atomic等

2 并发中心-Goroutine

2.1 goroutine介绍

在Go中运用goroutine来完结并发,在Go中协程的概念最终落地到goroutine中,能够称为Go协程、协程Coroutine等。goroutine是由Go的运转时(runtime)调度和办理的,Go程序会将 goroutine 中的使命合理地分配给每个CPU。

在Go并发编程中无需重视:进程、线程、协程的概念,无需写创立和毁掉的代码,只需求重视goroutine即可。而且goroutine的运用适当简略,Go在言语层面供给go关键字去敞开一个goroutine。例如你想让函数 fun1 运用goroutine履行:

go func1()

2.2 运用goroutine

为一个函数创立一个goroutine,只需求在调用函数的时分在前面加上go关键字即可。

func func1() {
    fmt.Println("Hello F1!")
}
func main() {
    go func1()
    fmt.Println("main goroutine done!")
    // 这儿睡一会,避免main完毕后,func1 来不及运转
    time.Sleep(time.Second)
}

两次运转成果或许不相同

PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
Hello F1!
main done!
PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
main done!
Hello F1!

go关键字能够用于匿名函数,而且goroutine完结多个并发十分简略,如下发动是个goroutine:

func main() {
	for i := 0; i < 10; i++ {
		go func(n int) {
			fmt.Println("履行了:", n)
		}(i)
	}
	fmt.Println("main done!")
	// 这儿睡一会,避免main完毕后,func1 来不及运转
	time.Sleep(time.Second)
}

运转成果:

PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
main done!
履行了: 4
履行了: 5
履行了: 1
履行了: 0
履行了: 7
履行了: 2
履行了: 3
履行了: 8
履行了: 9
履行了: 6

2.3 goroutine资源

在第一章中知道,线程是由操作体系内核进行调度的,触及内核态与用户态之间的频频切换,包括内存的分配与开释,调度本钱和开支比较大,而goroutine是由go runtime进行办理调度,很多的goroutine映射到少数的线程中去,其调度和切换愈加轻量,根本都在用户态完结。一个goroutine的栈只有几K巨细,十分轻量,轻松是完结10W等级并发支持。

3 数据交换-Channel

3.1 Channel是什么

在第二章介绍知道,go天然有高并发的特性,而且完结简略,但在实践开发中,不免会遇到不同并发线程(协程)之间进行数据交换和通讯,在Java等编程言语中能够经过共享内存数据(即某个目标后者变量)完结不同线程之间数据传递和通讯,一起为了确保数据的安全性需求合理的加锁。

在goroutine中,引入了一个新的概念 channel 通道,来完结数据传递,能够将channel看做是联通多个goroutine的数据桥梁,能够让一个goroutine发送特定的数据到另一个goroutine中去,而且确保先进先出的次序。

Go并发编程 Goroutine、Channel、Select、Mutex锁、sync、Atomic等

3.2 Channel的语法和运用

3.2.1 channel界说

在go中channel是一种类型,能够经过和变量相同的办法进行界说,如下:

var ch1 chan int   // 声明一个传递整型的通道
var ch2 chan bool  // 声明一个传递布尔型的通道
var ch3 chan []string // 声明一个传递string切片的通道
var ch3 chan MyStruct // 声明一个结构体类型的通道
fmt.Printf("ch1:%#v\n", ch1)
fmt.Printf("ch2:%#v\n", ch2)
fmt.Printf("ch3:%#v\n", ch3)
fmt.Printf("ch4:%#v\n", ch4)
PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
ch1:(chan int)(nil)
ch2:(chan bool)(nil)
ch3:(chan []string)(nil)
ch4:(chan main.MyStruct)(nil)

从程序履行成果能够看出channel是引用类型(nil),通道声明后默认值nil值。

3.2.2 channel初始化

能够运用go内置make函数进行初始化:

fmt.Println("开端初始化")
ch1 = make(chan int, 10)     // 初始化一个int通道,通道缓冲巨细10
ch2 = make(chan bool, 20)    // 初始化一个bool通道,通道缓冲巨细20
ch3 = make(chan []string)    // 初始化一个[]string通道,无通道缓冲
ch4 = make(chan MyStruct, 5) // 初始化一个MyStruct通道,通道缓冲巨细5
PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
ch1:(chan int)(nil)
ch2:(chan bool)(nil)
ch3:(chan []string)(nil)
ch4:(chan main.MyStruct)(nil)
开端初始化
ch1:(chan int)(0xc0000180b0)
ch2:(chan bool)(0xc000112080)
ch3:(chan []string)(0xc00005c060)
ch4:(chan main.MyStruct)(0xc00005c0c0)

3.2.3 channel操作

channel发送接纳数据运用 <-符号

  • 发送:向ch1通道中发送一个1-5五个数字
ch1 <- 1
ch1 <- 2
ch1 <- 3
ch1 <- 4
ch1 <- 5
  • 接纳:从ch1中接纳数字,这儿为了便利就for循环接纳五次,留意:通道在没有数据接纳时,会进行堵塞
for i := 0; i < 5; i++ {
    n := <-ch1
    fmt.Println("从ch1中接纳数据:", n)
}
PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
从ch1中接纳数据: 1
从ch1中接纳数据: 2
从ch1中接纳数据: 3
从ch1中接纳数据: 4
从ch1中接纳数据: 5
  • 封闭:一个通道能够经过内置函数close进行封闭,封闭后的通道不能再发送数据,但还能够接纳数据,假如管道中还有数据则正常接纳,假如没有则回来0
close(ch1)
ch1 <- 6 // 这儿会报错
fmt.Println("从ch1中接纳数据:", <-ch1)
panic: send on closed channel
goroutine 1 [running]:
main.main()
        D:/dev/go/workspace/go_demo_code/temp/t1.go:36 +0x32c
exit status 2
  • 通道封闭后,怎么感知?当通道被封闭时,往该通道发送值会引发panic,从该通道里接纳的值一直都是类型值,那么在循环操作通道的时分怎么感知通道被封闭了,能够经过如下办法。
// 通道封闭后手动break
for {
    n, ok := <-ch1
    if !ok {
        break
    }
    fmt.Println("从ch1中接纳数据:", n)
}
// 通道封闭后会自动退出for range循环
for i := range ch1 {
    fmt.Println(i)
}

完好代码:

package main
import "fmt"
type MyStruct struct {
}
func main() {
	var ch1 chan int      // 声明一个传递整型的通道
	var ch2 chan bool     // 声明一个传递布尔型的通道
	var ch3 chan []string // 声明一个传递string切片的通道
	var ch4 chan MyStruct // 声明一个结构体类型的通道
	fmt.Printf("ch1:%#v\n", ch1)
	fmt.Printf("ch2:%#v\n", ch2)
	fmt.Printf("ch3:%#v\n", ch3)
	fmt.Printf("ch4:%#v\n", ch4)
	fmt.Println("开端初始化")
	ch1 = make(chan int, 10)     // 初始化一个int通道,通道缓冲巨细10
	ch2 = make(chan bool, 20)    // 初始化一个bool通道,通道缓冲巨细20
	ch3 = make(chan []string)    // 初始化一个[]string通道,无通道缓冲
	ch4 = make(chan MyStruct, 5) // 初始化一个MyStruct通道,通道缓冲巨细5
	ch1 <- 1
	ch1 <- 2
	ch1 <- 3
	ch1 <- 4
	ch1 <- 5
	for i := 0; i < 5; i++ {
		n := <-ch1
		fmt.Println("从ch1中接纳数据:", n)
	}
	close(ch1)
	ch1 <- 6
	fmt.Println("从ch1中接纳数据:", <-ch1)
	fmt.Println("从ch1中接纳数据:", <-ch1)
}

3.3 channel实战

需求:界说两个int类型的 channel,敞开三个goroutine,go1 发送数据到到通道 ch1,go2接纳ch1通道中的数值进行平方操作,再将成果写入到ch2,go3接纳ch2的数据进行打印输出。

package main
import (
	"fmt"
	"time"
)
func main() {
	var ch1 = make(chan int, 5)
	var ch2 = make(chan int, 5)
	go func1(ch1)
	go func2(ch1, ch2)
	go func3(ch2)
    // 主函数睡眠
	time.Sleep(time.Second)
}
func func1(ch chan int) {
	for i := 0; i < 10; i++ {
		fmt.Println("发送一个数据:", i)
		ch <- i
	}
}
func func2(ch1, ch2 chan int) {
    // 这儿死循环无限接纳
	for {
		n := <-ch1
		fmt.Println("对数据进行平方处理:", n)
		ch2 <- n * n
	}
}
func func3(ch chan int) {
    // 这儿死循环无限接纳
	for {
		n := <-ch
		fmt.Println("接纳到数据了直接打印:", n)
	}
}

运转成果:

PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
发送一个数据: 0
发送一个数据: 1
发送一个数据: 2
发送一个数据: 3
发送一个数据: 4
发送一个数据: 5
发送一个数据: 6
对数据进行平方处理: 0
对数据进行平方处理: 1
对数据进行平方处理: 2
对数据进行平方处理: 3
对数据进行平方处理: 4
对数据进行平方处理: 5
对数据进行平方处理: 6
发送一个数据: 7
发送一个数据: 8
发送一个数据: 9
接纳到数据了直接打印: 0
接纳到数据了直接打印: 1
接纳到数据了直接打印: 4
接纳到数据了直接打印: 9
接纳到数据了直接打印: 16
接纳到数据了直接打印: 25
接纳到数据了直接打印: 36
对数据进行平方处理: 7
对数据进行平方处理: 8
对数据进行平方处理: 9
接纳到数据了直接打印: 49
接纳到数据了直接打印: 64
接纳到数据了直接打印: 81

4 多路复用-Select

第三章咱们知道了能够经过channel进行多个goroutine的数据交换,在运用通道时,假如没有数据接纳会堵塞,处理监听多个通道,就无法经过一个goroutine很好的接纳数据。这种状况go供给了select,多路复用,能够一起监听多个channel,运用如下:

select {
   case c1 := <- ch1:
      fmt.Println("c1=", c1)
   case c2 := <- ch1:
      fmt.Println("c2=", c2)
}
  • select句子是专为通道而设计的,所以每个case表达式中都只能包含操作通道的表达式
  • select 默认堵塞,只有监听的channel中有发送或许承受数据时才运转
  • 设置default则不堵塞,通道内没有待承受的数据则履行default
  • 多个channel准备好时,会随机选一个履行

5 并发安全-Mutex锁

在运用多个goroutine操作临界资源(共享资源),就会产生竞赛状况,呈现数据安全问题,最总成果和期望的不一致,如下:


var num int
func main() {
	go add()
	go add()
	time.Sleep(time.Second * 2)
	fmt.Println(num)
}
func add() {
	for i := 0; i < 10000; i++ {
		num++
	}
}

运转三次,两次成果都错误:

PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
15737
PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
20000
PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
15825

5.1 互斥锁

互斥锁顾名思义彼此排斥,同一时刻确保只有一个goroutine能够持有锁,进行共享资源的拜访,在go中互斥锁运用sync.Mutex完结。

  1. 声明锁目标:var lock sync.Mutex
  2. 调用锁办法加锁或许解锁
    • lock.Lock():会一直等候直到获取锁
    • lock.TryLock():尝试取得锁,获取失败当即回来
    • lock.Unlock():开释锁
  3. 多个goroutine一起等候一个锁时,唤醒的策略是随机的。
var num int
// 声明一把锁
var lock sync.Mutex
func main() {
	go add()
	go add()
	time.Sleep(time.Second * 2)
	fmt.Println(num)
}
func add() {
	for i := 0; i < 10000; i++ {
        // 共享资源拜访前加锁
		lock.Lock()
		num++
        // 操作完开释锁
		lock.Unlock()
	}
}

运转成果:

PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
20000
PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
20000
PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
20000

5.2 读写锁

和其他编程言语相似,go也供给了读写锁,进步读多写少场景的锁性能,分为读锁写锁两部分具有一下特性:

  1. 并发读操作不加锁, R R 不堵塞
  2. 一个goroutine获取读锁后,其他goroutine,获取写锁就会等候,R W 堵塞
  3. 一个goroutine获取写锁后,其他goroutine,获取读写锁都会等候,W R、W W 堵塞

一句话:读读共享,读写互斥,写写互斥


var num int
//var lock sync.Mutex
var rwlock sync.RWMutex
func main() {
	go add()
	go add()
	go read()
	time.Sleep(time.Second * 2)
	fmt.Println(num)
}
func read() {
	// 测验作用读取5次
	for i := 0; i < 5; i++ {
		// 加读锁
		rwlock.RLock()
		fmt.Println("读取:", num)
		// 开释读锁
		rwlock.RUnlock()
		// 让出CPU履行时刻,后面会介绍
		runtime.Gosched()
	}
}
func add() {
	for i := 0; i < 10000; i++ {
		// 加写锁
		rwlock.Lock()
		num++
		// 开释写锁
		rwlock.Unlock()
	}
}
PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
读取: 0
读取: 999
读取: 1212
读取: 1334
读取: 1335
20000

6 并发操控-Sync

多个goroutine并发运转时,不免需求对并发程序进行操控,如第五章的锁操控并发安全拜访,灾还有一些常见的状况:

  1. 一个goroutine,需求等候多个goroutine完结使命再履行业务代码。
  2. 某些特定代码在并发场景下只期望被履行一次。
  3. 多个等候中的goroutine,接纳到一个goroutine告诉后,开端处理一些业务(假如单纯运用 chan 或互斥锁,那么只能有一个协程能够等候,并读取到数据)
  4. go默认的map是并发不安全的,实践开发中咱们需求一些并发类的容器,例如map等

sync包供给了一些开箱即用的api和目标,协助咱们操控并发goroutine,解决实践需求。

6.1 sync.WaitGroup

WaitGroup相似Java的CountDownLatch,能够完结等候并发使命履行完, sync.WaitGroup有以下几个办法:

  • Add(delta int) 计数器+delta

  • Done() 计数器-1

  • Wait() 堵塞直到计数器变为0


var num int
//var lock sync.Mutex
var rwlock sync.RWMutex
//界说一个WaitGroup
var wg sync.WaitGroup
func main() {
    // 三个 goroutine,这儿直接加三
	wg.Add(3)
	go add()
	go add()
	go read()
    // 等候一切goroutine使命完毕
	wg.Wait()
	fmt.Println(num)
}
func read() {
	// 测验作用读取5次
	for i := 0; i < 5; i++ {
		// 加读锁
		rwlock.RLock()
		fmt.Println("读取:", num)
		// 开释读锁
		rwlock.RUnlock()
		// 让出CPU履行时刻
		runtime.Gosched()
	}
    // 完毕一个减一
	wg.Done()
}
func add() {
	for i := 0; i < 10000; i++ {
		// 加写锁
		rwlock.Lock()
		num++
		// 开释写锁
		rwlock.Unlock()
	}
	wg.Done()
}

6.2 sync.Once

sync.Once,用来确保某种行为只会被履行一次,高并发场景,能够用来处理一次性操作

中心函数:

func (o *Once) Do(f func())

比如:

package main
import (
	"fmt"
	"sync"
)
var wg sync.WaitGroup
func main() {
	wg.Add(3)
	var once sync.Once
	go load(&once)
	go load(&once)
	go load(&once)
	wg.Wait()
	fmt.Println("主办法完毕")
}
// 留意这儿需求传入指针类型
func load(once *sync.Once) {
	fmt.Println("load办法被调用了")
	once.Do(func() {
		fmt.Println("不管多少次,once.Do只会调用一次")
	})
	wg.Done()
}

运转成果

PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
load办法被调用了
不管多少次,once.Do只会调用一次
load办法被调用了
load办法被调用了
主办法完毕

6.3 sync.Cond

sync.Cond 根据互斥锁/读写锁,经常用在多个 goroutine 等候,一个 goroutine 告诉的场景,当共享资源的状况产生变化的时分,它能够用来告诉被互斥锁堵塞的 goroutine。

package main
import (
	"fmt"
	"sync"
	"time"
)
func main() {
	cond := sync.NewCond(&sync.Mutex{})
	go fun1(cond)
	go fun1(cond)
	go fun1(cond)
	go func() {
		time.Sleep(time.Second)
		fmt.Println("发出信号了")
		cond.Broadcast()
		//cond.Signal()
	}()
	time.Sleep(time.Second * 2)
	fmt.Println("主办法完毕")
}
func fun1(cond *sync.Cond) {
	cond.L.Lock()
	fmt.Println("func1被调用了...")
	cond.Wait()
	fmt.Println("func1完毕了")
	cond.L.Unlock()
}
PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
func1被调用了...
func1被调用了...
func1被调用了...
发出信号了
func1完毕了
func1完毕了
func1完毕了
主办法完毕

6.4 sync.Map

看一个并发状况下操作map的代码,履行后会报错:fatal error: concurrent map writes


func main() {
	m := make(map[int]int)
	go put(m, 0)
	go put(m, 10)
	go put(m, 20)
	time.Sleep(time.Second * 2)
	fmt.Println("主办法完毕")
}
func put(m map[int]int, start int) {
	for i := start; i < start+10; i++ {
		m[i] = i * i
	}
}

当然解决这个问题能够经过对map操作加锁,Go言语的sync包中还供给了一个开箱即用的并发安全版map sync.Map


func main() {
    // 声明一个并发map
	syncMap := &sync.Map{}
	go syncPut(syncMap, 0)
	go syncPut(syncMap, 10)
	go syncPut(syncMap, 20)
	time.Sleep(time.Second * 1)
	fmt.Println("主办法完毕")
    // 遍历打印
	syncMap.Range(func(key, value any) bool {
		fmt.Printf("key=%v, value=%v\n", key, value)
		return true
	})
}
func syncPut(m *sync.Map, start int) {
	for i := start; i < start+10; i++ {
        // 安全的存储数据
		m.Store(i, i*i)
	}
}

7 原子操作 Atomic

在go中咱们运用加锁来确保并发场景下数据安全拜访,但加锁的价值比较大,触及到内核态的上下文切换会比较耗时,go中针对根本数据类型的操作供给了atomic包,能够完结原子的操作根本数据类型。

// 原子性的获取*addr的值。
func LoadInt64(addr *int64) (val int64)
func LoadUint32(addr *uint32) (val uint32)
func LoadUint64(addr *uint64) (val uint64)
func LoadUintptr(addr *uintptr) (val uintptr)
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
//原子性的将val的值保存到*addr。
func StoreInt32(addr *int32, val int32)
func StoreInt64(addr *int64, val int64)
func StoreUint32(addr *uint32, val uint32)
func StoreUint64(addr *uint64, val uint64)
func StoreUintptr(addr *uintptr, val uintptr)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
//原子性的将val的值添加到*addr并回来新值。
func AddInt32(addr *int32, delta int32) (new int32)
func AddInt64(addr *int64, delta int64) (new int64)
func AddUint32(addr *uint32, delta uint32) (new uint32)
func AddUint64(addr *uint64, delta uint64) (new uint64)
func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)
//原子性的将新值保存到*addr并回来旧值。
func SwapInt32(addr *int32, new int32) (old int32)
func SwapInt64(addr *int64, new int64) (old int64)
func SwapUint32(addr *uint32, new uint32) (old uint32)
func SwapUint64(addr *uint64, new uint64) (old uint64)
func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)
//原子性的比较*addr和old,假如相同则将new赋值给*addr并回来真。
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)

举一个简答的比如:

package main
import (
	"fmt"
	"sync"
	"sync/atomic"
)
func main() {
	var wg sync.WaitGroup
	var num int32
	wg.Add(3)
	go func() {
		for i := 0; i < 1000; i++ {
			atomic.AddInt32(&num, 1)
		}
		wg.Done()
	}()
	go func() {
		for i := 0; i < 1000; i++ {
			atomic.AddInt32(&num, 1)
		}
		wg.Done()
	}()
	go func() {
		for i := 0; i < 1000; i++ {
			atomic.AddInt32(&num, 1)
		}
		wg.Done()
	}()
	wg.Wait()
	fmt.Println(num)
}
PS D:\dev\go\workspace\go_demo_code> go run .\temp\t1.go
3000

8 底层操控-Runtime

Go Runtime 后续专门写一篇文章深化介绍,这儿只介绍几个简略的办法,初步了解Go Runtime。

  • runtime.GOMAXPROCS 设置多少个OS线程来一起履行Go代码,默认值是机器上的CPU中心数
  • runtime.Gosched() 让出CPU时刻片,重新等候安排使命
  • runtime.Goexit() 退出当时协程
  • runtime.NumGoroutine() 查看当时Goroutine数量
  • runtime.NumCPU() 回来cpu数量
  • runtime.GC() 让运转时体系进行一次强制性的废物搜集