本文一切实例代码运转go版别:go version go1.18.10 windows/amd64
1 并发编程介绍
1.1 串行、并发、并行
- 串行:一切使命一件一件做,按照事先的次序顺次履行,没有被履行到的使命只能等候。最终履行完的时刻等于各个子使命之和。
- 并发:是以交替的办法运用
等候
某个使命的时刻来处理其他使命核算逻辑,在核算机中,例如一个单核CPU,会经过时刻片
算法,来高效合理的分配cpu核算资源。从用户视点来看似乎是多个使命在一起履行。
- 并行:在同一时刻处理核算多个使命,以多核CPU为例,能够完结一起处理核算多个使命,一个CPU担任一个使命的核算逻辑,大家做到一起进行,就像三个使命有三个工人一起干活相同。
1.2 进程、线程、协程
-
进程:是程序运转的根本单位,每个进程都有自己的独立内存空间,不同的进程能够经过
进程之间的彼此通讯
进行交流。比如:电脑上的 QQ、微信、WPS等都有各自的进程。在操作体系等级来看,进程是操作体系
对一个正在运转的程序的一种笼统
。一个体系里边能够一起运转多个进程,而每一个进程又好像是在独占的运用硬件资源,经过处理器在进程之间不停的切换来完结。 -
线程:线程是
处理器(CPU)资源分配和调度的根本单位
,在一个进程中能够有多个线程,每个线程都运转在进程的环境上下文中,不同线程之间能够经过线程之间通讯
进行数据交换。比如:在360安全卫视进程中,你能够一起进行废物清理和病毒查杀,在微信中,你能够刷朋友圈一起接纳音讯。 -
进程和线程差异:
- 创立和开支方面,进程的创立需求体系分配内存和CPU,文件句柄等资源,毁掉时也要进行相应的回收,所以进程的办理开支很大;可是线程的办理开支则很小。
- 进程之间不会彼此影响;而一个线程溃散或许会导致进程溃散,从而影响同个进程里边的其他线程。
- 线程是进程的子使命,是处理器(CPU)分配和调度的根本单位,进程是对运转时程序的封装,是体系进行资源分配和调度的根本单元。
-
协程:在理解协程之前,需求明白线程的几个问题:
- 1、在履行过程中分为用户态和内核态,两个状况的切换会造成资源开支;
- 2、面线程创立的越多,CPU切换的就越频频,由于操作体系的调度要确保相对公正
- 3、线程的创立、毁掉都需求调用体系调用,每次请求都创立,高并发下开支就显得很大,而且线程的数量不能太多,占用内存是 MB 等级。
- 根据上面的问题,协程被提出,协程是用户态(用户空间)的一种笼统,对操作体系内核而言并没有这个概念,依然是以线程维度调度。协程的主要思维是在用户态完结调度算法,来达到用少数线程,处理很多使命的调度,由于是用户态调度切换,不触及内核切换和不同线程之间的上下文切换,大大减少开支。
最后来一个图描绘三者之间的联系:
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中去,而且确保先进先出的次序。
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
完结。
- 声明锁目标:
var lock sync.Mutex
- 调用锁办法加锁或许解锁
- lock.Lock():会一直等候直到获取锁
- lock.TryLock():尝试取得锁,获取失败当即回来
- lock.Unlock():开释锁
- 多个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也供给了读写锁
,进步读多写少场景的锁性能,分为读锁
和写锁
两部分具有一下特性:
- 并发读操作不加锁, R R 不堵塞
- 一个goroutine获取
读锁
后,其他goroutine,获取写锁
就会等候,R W 堵塞 - 一个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并发运转时,不免需求对并发程序进行操控,如第五章的锁操控并发安全拜访,灾还有一些常见的状况:
- 一个goroutine,需求等候多个goroutine完结使命再履行业务代码。
- 某些特定代码在并发场景下只期望被履行一次。
- 多个等候中的goroutine,接纳到一个goroutine告诉后,开端处理一些业务(假如单纯运用 chan 或互斥锁,那么只能有一个协程能够等候,并读取到数据)
- 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() 让运转时体系进行一次强制性的废物搜集