Go 并发操作
goroutine
在其他的编程语言中,线程调度是交由os
来进行处理的。
但是在Go
语言中,会对此做一层封装,Go
语言中的并发由goroutine
来实现,它类似于用户态的线程,更类似于其他语言中的协程。它是交由Go
语言中的runtime
运行时来进行调度处理,这使得Go
语言中的并发性能非常之高。
一个Go
进程,可以启动多个goroutine
。
一个普通的机器运行几十个线程负载已经很高了,然而Go
可以轻松创建百万goroutine
。
Go
标准库的net
包,写出的go web server性能直接媲美Nginx
。
比如在java/c++
里,开发者通常要去自己维护一个线程池,并且需要包装多个线程任务,同时还要由开发者手动调度线程执行任务并且维护上下文切换,这非常的耗费心智,故在Go
语言中出现了goroutine
,它的概念类似于线程与协程,Go
语言内置的就有调度与上下文切换机制,所以不用开发人员再去注意这些,并且goroutine
的使用也非常的简单,它相较于其他语言的多并发编程更加轻松。
goroutine与线程
动态栈
操作系统中的线程都有固定的栈内存(一般为2MB),这使得开启大量的线程会面临性能下降的问题。
但是goroutine
在生命周期之处的栈内存一般只有2KB,并且它会按需进行增大和缩小。最大的栈限制可达到1GB,所以在Go
语言中一次创建上万级别的goroutine
是没有任何问题的。
goroutine调度
GPM
是Go
语言运行时runtime
层面的实现,这是Go
语言自己实现的一套调度系统,区别于操作系统来调度os
线程。
- G很好理解,就是单个goroutine的信息,里面除了存放本goroutine信息外 还有与所在P的绑定等信息。
- P管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。
- M(machine)是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系, 一个groutine最终是要放到M上执行的;
P与M一般也是一一对应的。他们关系是: P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime
会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。
P的个数是通过runtime.GOMAXPROCS
设定(最大256),Go1.5版本之后默认为物理线程数。 在并发量大的时候会增加一些P和M,但不会太多,切换太频繁的话得不偿失。
单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine
则是由Go运行时(runtime
)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。 其一大特点是goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。
上面这么多专业术语看起来比较头痛,这边用一幅图来明确的进行表示。
goroutine使用
在调用函数前加上go
关键字,就可以为函数创建一个goroutine
。
一个goroutine
必定对应一个函数,可以创建多个goroutine
去执行相同的函数。
每个Go
语言都有一个goroutine
,类似于主线程的概念。
goroutine
的启动是随机进行调度的,这个无法手动控制。
基本使用
下面是创建单个goroutine
与主goroutine
进行并发执行任务。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func(){
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println(i)
}
fmt.Println("子goroutine执行完毕")
}() // 立即执行函数,一个goroutine任务
wg.Wait()
fmt.Println("主goroutine执行完毕")
}
下面是创建多个goroutine
与主goroutine
进行并发执行任务。
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func f1(){
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任务1",i)
}
fmt.Println("子goroutine1执行完毕")
}
func f2(){
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任务2",i)
}
fmt.Println("子goroutine2执行完毕")
}
func main() {
wg.Add(2)
go f1()
go f2()
wg.Wait()
fmt.Println("主goroutine执行完毕")
}
sync.WaitGroup
该属性类似于一把全局锁,只有当子goroutine
任务结束后,主goroutine
任务才能结束。
类似于守护线程。
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup // 当前有任务 0 个
func f1(){
defer wg.Done() // 执行完成后,任务减 1
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任务1",i)
}
fmt.Println("子goroutine执行完毕")
}
func main() {
wg.Add(1) // 任务加 1 注意,一定要放外面,不能放函数中
go f1()
wg.Wait() // 任务必须为0时才继续向下执行
fmt.Println("主goroutine执行完毕")
}
GOMAXPROCS
该函数可设定开启多少os
线程来运行子goroutine
任务。
默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。
Go语言中可以通过runtime.GOMAXPROCS()
函数设置当前程序并发时占用的CPU逻辑核心数。
Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。
如下示例,两个子goroutine
任务在一个线程上运行,会通过时间片轮询等策略来抢占执行权。
package main
import (
"fmt"
"sync"
"runtime"
)
var wg sync.WaitGroup
func f1(){
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任务1",i)
}
fmt.Println("子goroutine1执行完毕")
}
func f2(){
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任务2",i)
}
fmt.Println("子goroutine2执行完毕")
}
func main() {
runtime.GOMAXPROCS(1) // 设置最多开启1个子线程
wg.Add(2)
go f1()
go f2()
wg.Wait()
fmt.Println("主goroutine执行完毕")
}
时间轮询
由于底层的os
线程切换机制是依照时间轮询进行切换,所以goroutine
的切换时机也是由时间片轮询来决定的。
使用runtime.Gosched()
可让当前任务让出线程占用,交由其他任务进行执行。
package main
import (
"fmt"
"sync"
"runtime"
)
var wg sync.WaitGroup
func f1(){
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任务1",i)
if i == 300 {
runtime.Gosched() // 让出线程占用
}
}
fmt.Println("子goroutine1执行完毕")
}
func f2(){
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任务2",i)
}
fmt.Println("子goroutine2执行完毕")
}
func main() {
runtime.GOMAXPROCS(1)
wg.Add(2)
go f1()
go f2()
wg.Wait()
fmt.Println("主goroutine执行完毕")
}
终止任务
runtime.Goexit()
终止当前任务。
package main
import (
"fmt"
"sync"
"runtime"
)
var wg sync.WaitGroup
func f1(){
wg.Add(1)
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任务1",i)
if i == 300 {
runtime.Goexit() // 终止任务
fmt.Println("子goroutine任务被终止")
}
}
fmt.Println("子goroutine执行完毕")
}
func main() {
go f1()
wg.Wait()
fmt.Println("主goroutine执行完毕")
}
通道使用
多个goroutine
中必须要有某种安全的机制来进行数据共享,这就出现了channel
通道。
它类似于管道或者队列,作用在于保证多goroutine
访问同一资源时达到数据安全的目的。
类型声明
channel
是引用类型,这就代表必须要使用make()
进行内存分配。
初始值为nil
。
下面是进行声明的示例:
var ch1 chan int // 声明一个传递整型的通道
var ch2 chan bool // 声明一个传递布尔型的通道
var ch3 chan []int // 声明一个传递int切片的通道
channel使用
使用前要进行内存分配,并且它还可选缓冲区。
代表该通道最多可容纳多少数据。当然,缓冲区大小是可选的,它具有动态扩容的特性。
make(chan 元素类型, [缓冲大小])
示例如下:
ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)
channel操作
以下是channel
的操作:
方法 | 说明 |
---|---|
ch <- 数据 | 将数据放入通道中 |
数据 <- ch | 将数据从通道取出 |
close() | 关闭通道 |
现在我们先使用以下语句定义一个通道:
ch := make(chan int)
将一个值发送到通道中。
ch <- 10 // 把10发送到ch中
从一个通道中接收值。
x := <- ch // 从ch中接收值并赋值给变量x
<-ch // 从ch中接收值,忽略结果
我们通过调用内置的close()
函数来关闭通道。
close(ch)
关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
关闭后的通道有以下特点:
- 对一个关闭的通道再发送值就会导致panic。
- 对一个关闭的通道进行接收会一直获取值直到通道为空。
- 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
- 关闭一个已经关闭的通道会导致panic。
阻塞通道
当一个通道无缓冲区时,将被称为阻塞通道。
通道中存放一个值,但该值并没有被取出时将会引发异常。
必须先收,后发。因为发送后会产生阻塞,如果没有接收者则会导致死锁异常
必须将通道中的值取尽,否则会发生死锁异常,也就是说放了几次就要取几次
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func f1(ch chan string){
defer wg.Done()
rose := <- ch // 等待取玫瑰花
lily := <- ch // 等待取百合花
fmt.Println(rose)
fmt.Println(lily)
}
func main(){
wg.Add(1)
ch := make(chan string)
go f1(ch) // 必须先有接收者
ch <- "玫瑰花" // 开始放入玫瑰花
ch <- "百合花" // 开始放入百合花
wg.Wait()
fmt.Println("主goroutine运行完毕")
}
非阻塞通道
非阻塞通道即为有缓冲区的通道。
只要通道的容量大于零,则代表该缓冲区中能够去存放值。
非阻塞通道相较于阻塞通道,它的使用其实更加符合人类逻辑
阻塞通道必须要先接收再存入
非阻塞通道可以先存入再接收
并且,非阻塞通道中的值可以不必取尽
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func f1(ch chan string){
defer wg.Done()
rose := <- ch // 等待取玫瑰花
fmt.Println(len(ch)) // 获取元素数量 1 代表还剩下一个没取
fmt.Println(cap(ch)) // 获取容量 10 代表最多可以放10个
fmt.Println(rose)
}
func main(){
wg.Add(1)
ch := make(chan string,10)
ch <- "玫瑰花" // 放入玫瑰花
ch <- "百合花" // 放入百合花
go f1(ch)
wg.Wait()
fmt.Println("主goroutine运行完毕")
}
单向通道
单向通道即是只能取,或者只能发。
上面的通道都是双向通道,可能造成阅读不明确的问题,故此Go
还提供了单向通道。
在函数传参中,可以将双向通道转换为单项通道,这也是最常用的方式。
通道标识 | 说明 |
---|---|
ch <- string | 代表只能写入string类型的值 |
<- ch string | 代表只能取出string类型的值 |
package main
import (
"sync"
"fmt"
)
var wg sync.WaitGroup
func recv(ch <-chan string) { // 只能取
defer wg.Done()
rose := <- ch
fmt.Println(rose)
}
func send(ch chan<- string) { // 只能放
defer wg.Done()
ch <- "玫瑰花"
}
func main() {
wg.Add(2)
ch := make(chan string, 10)
go send(ch)
go recv(ch)
wg.Wait()
fmt.Println("主goroutine运行完毕")
}
常见情况
以下是通道的使用常见情况。
关闭已经关闭的channel也会引发panic。
任务池
多个goroutine
的切换会带来性能损耗问题。
所以我们可以通过做一个goroutine
的池来解决这种问题,当一个goroutine
的任务结束后,它不会kill
掉该goroutine
,而是让它继续的取下一个任务。
所以我们需要与chan
结合进行构造一个简单的任务池。
如下示例,构建了一个简单的任务池并且开启了3个goroutine
,并且放了6个任务在task
这个chen
中交由run
进行处理。
处理结果放在result
这个chen
中。
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func run(id int, task <-chan string, result chan<- string) {
defer wg.Done()
for {
t, ok := <-task
if !ok {
fmt.Println("处理完了所有任务")
break
}
time.Sleep(time.Second * 2)
t += fmt.Sprintf(":已由%d处理", id)
result <- t
}
}
func main() {
task := make(chan string, 10)
result := make(chan string, 10)
wg.Add(3)
for i := 0; i < 3; i++ {
go run(i, task, result) // 开三个goroutine来处理
}
urlRequeste := []string{
"www.baidu.com",
"www.google.com",
"www.cnblog.com",
"www.xinlang.com",
"www.csdn.com",
"www.taobao.com",
}
for _, url := range urlRequeste {
task <- url // 开启了六个任务
}
close(task)
for i := 0; i < len(urlRequeste); i++ {
fmt.Println(<-result)
}
close(result)
wg.Wait()
fmt.Println("主goroutine运行完毕")
}
// www.google.com:已由2处理
// www.cnblog.com:已由1处理
// www.baidu.com:已由0处理
// 处理完了所有任务
// 处理完了所有任务
// 处理完了所有任务
// www.taobao.com:已由0处理
// www.xinlang.com:已由2处理
// www.csdn.com:已由1处理
// 主goroutine运行完毕
select多路复用
类似于事件循环,我们来监听多个通道。
当一个通道可用时就来操纵该通道。
select{
case <-ch1:
...
case data := <-ch2:
...
case ch3<-data:
...
default:
默认操作
}
这个示例还是要在具体的应用场景中比较常见,并且一般的库都已经写好了。
只要知道其中理论就行,没必要白手写select
,除非你要做开源框架或公司框架等。
可处理一个或多个channel的发送/接收操作。
如果多个case同时满足,select会随机选择一个。
对于没有case的select{}会一直等待,可用于阻塞main函数。
小例子:
package main
import (
"fmt"
)
func main() {
ch := make(chan int, 1)
for i := 0; i < 10; i++ {
select {
case x := <-ch: // 允许赋值
fmt.Println("可以读了,已经读出了:", x) // 可读
case ch <- i: // 可写
fmt.Println("可以写了,已经写入了:", i)
}
}
}
锁相关
锁是为了解决资源同步的问题。
但是对于多个goroutine
通信应该是去使用channel
,而不是用锁进行解决。
互斥锁
如下代码,会产生资源竞争问题。致使结果不正确:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main(){
num := 10000
wg.Add(2)
go func(){
defer wg.Done()
for i:=0; i<10000; i++{
num ++
}
}()
go func(){
defer wg.Done()
for i:=0; i<10000; i++{
num --
}
}()
wg.Wait()
fmt.Println(num)
}
// 13966
// 7578
// 9475
此时添加互斥锁即可,让其变为串行执行:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
var lock sync.Mutex
func main(){
num := 10000
wg.Add(2)
go func(){
defer wg.Done()
for i:=0; i<10000; i++{
lock.Lock() // 加锁
num ++
lock.Unlock() // 解锁
}
}()
go func(){
defer wg.Done()
for i:=0; i<10000; i++{
lock.Lock() // 加锁
num --
lock.Unlock() // 解锁
}
}()
wg.Wait()
fmt.Println(num)
}
读写互斥锁
互斥锁是完全互斥,将并发执行转变为串行执行,性能损耗比较大。
但是在更多的场景中,我们则不需要完全互斥。
比如多个人访问统一资源但是并未对资源本身做修改时可以不加锁,但是当有人对资源做修改时其他人将无法访问。
以上场景使用读写锁更加合适,读写锁在读多写少的场景下非常高效。
读锁:我获取了读锁你不能去修改,必须等我释放
写锁:我获取了写锁你不能去读,必须等我释放
如下,写入200次,读取2000次的用时为1s左右。
package main
import (
"fmt"
"time"
"sync"
)
var wg sync.WaitGroup
var rwlock sync.RWMutex // 读写锁
var variety = 10
func read() {
defer wg.Done()
rwlock.RLock() // 加读锁
fmt.Println(variety)
rwlock.RUnlock() // 释放读锁
}
func write() {
defer wg.Done()
rwlock.Lock() // 加写锁
variety ++
fmt.Println(variety)
rwlock.Unlock() // 释放写锁
}
func main() {
start := time.Now()
for i := 0; i < 200; i++ {
wg.Add(1)
go write()
}
for i := 0; i < 2000; i++ {
wg.Add(1)
go read()
}
wg.Wait()
end := time.Now()
fmt.Println("运行时间:",end.Sub(start)) // 1s左右
}
如果单纯使用互斥锁,时间会更长:
package main
import (
"fmt"
"time"
"sync"
)
var wg sync.WaitGroup
var lock sync.Mutex // 互斥锁
var variety = 10
func read() {
defer wg.Done()
lock.Lock() // 加互斥锁
fmt.Println(variety)
lock.Unlock() // 释放互斥锁
}
func write() {
defer wg.Done()
lock.Lock() // 加互斥锁
variety ++
fmt.Println(variety)
lock.Unlock() // 释放互斥锁
}
func main() {
start := time.Now()
for i := 0; i < 200; i++ {
wg.Add(1)
go write()
}
for i := 0; i < 2000; i++ {
wg.Add(1)
go read()
}
wg.Wait()
end := time.Now()
fmt.Println("运行时间:",end.Sub(start)) // 2s左右
}
sync.Once
只执行一次,如果一个配置文件体积过于巨大,在初始化时进行加载会拖慢启动速度。
所以我们可以在要使用时进行加载(懒惰加载),如下示例,有10个goroutine
都需要用到配置文件。
该配置文件只会加载一次,之后便不会重复加载。
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
var once sync.Once
func load() {
fmt.Println("加载配置文件...")
}
func main() {
fmt.Println("运行代码逻辑...发现很多地方都要用配置文件了")
for i := 0; i < 10; i++ {
fmt.Printf("%v需要用到配置文件,开始加载\n", i)
wg.Add(1)
go func() {
defer wg.Done()
once.Do(load) // 只加载一次,并且该函数的格式必须是不能有参数与返回值
}()
}
wg.Wait()
}
sync.Map
Go
语言中内置的map
不是并发安全的。不要使用内置的map
进行数据传递,你应该使用channel
或者sync
给你提供的map
。该map
不用进行make
初始化内存。
sync
提供的map
有以下功能:
方法 | 描述 |
---|---|
Store(k,v) | 设置一组键值对 |
Load(k) | 根据k取出v |
LoadorStore(k,v) | 根据k取出v,如果没有该k则创建v |
Delete(k) | 删除一组键值对 |
Range | 循环遍历出k和v |
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
var once sync.Once
var m = sync.Map{} // g安全的map
func out() {
defer wg.Done()
gift, _ := m.Load("礼物")
fmt.Println(gift)
}
func put() {
m.Store("礼物", "玫瑰花")
defer wg.Done()
}
func main() {
wg.Add(2)
go put()
go out()
wg.Wait()
}
原子操作
功能概述
对于多个goroutine
访问同一资源造成的并发安全问题,可以通过加锁来进行解决。
但是加锁会使性能降低,所以这里Go
语言中sync/atomic
包提供了原子操作来代替加锁。
常用方法
主要对数字类型的数据的加减乘除等。
方法 | 描述 |
---|---|
func LoadInt32(addr *int32) (val int32) 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) |
读取操作 |
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) |
写入操作 |
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) |
修改操作 |
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) |
交换操作 |
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"
"time"
)
var wg sync.WaitGroup
func main() {
var num int64 = 10000
start := time.Now().UnixNano()
wg.Add(2)
go func() {
defer wg.Done()
for i := 0; i < 10000; i++ {
atomic.AddInt64(&num, 1)
}
}()
go func() {
defer wg.Done()
for i := 0; i < 10000; i++ {
atomic.AddInt64(&num, -1)
}
}()
wg.Wait()
end := time.Now().UnixNano()
fmt.Println("运行时间:", end - start) // 981600
fmt.Println(num)
}
加锁操作,速度会慢一些:
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
var lock sync.Mutex
func main() {
var num int64 = 10000
start := time.Now().UnixNano()
wg.Add(2)
go func() {
defer wg.Done()
for i := 0; i < 10000; i++ {
lock.Lock()
num++
lock.Unlock()
}
}()
go func() {
defer wg.Done()
for i := 0; i < 10000; i++ {
lock.Lock()
num--
lock.Unlock()
}
}()
wg.Wait()
end := time.Now().UnixNano()
fmt.Println("运行时间:", end - start) // 1000300
fmt.Println(num)
}