并行与并发
并行:并行是指两者同时执行,比如赛跑,两个人都在不停的往前跑;(资源够用,比如三个线程,四核的CPU )
并发:并发是指资源有限的情况下,两者交替轮流使用资源,比如一段路(单核CPU资源)同时只能过一个人,A走一段后,让给B,B用完继续给A ,交替使用,目的是提高效率。
goroutine
Go语言通过goroutine实现并发。goroutine类似于线程,属于用户态的线程。goroutine是由Go语言运行时候的(runtime)调度完成,而线程由操作系统调度完成。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU.
使用goroutine
只要在函数前添加go关键字,就可以为函数创建一个goroutine。一个goroutine必对应一个函数,可以创建多个goroutine执行相同的函数。
启动单个goroutine
在函数或者匿名函数前添加go关键字
var wg sync.WaitGroup
func hello() {
defer wg.Done() // goroutine结束就减1
fmt.Println("hello ")
}
func main() {
wg.Add(1) // 启动一个goroutine就添加1
go hello()
wg.Wait() // 等待所有goroutine结束
}
启动多个goroutine
func hello(i int) {
defer wg.Done() // goroutine结束就减1
fmt.Println("hello ", i)
}
func main() {
for i := 0; i < 1000; i++ {
wg.Add(1) // 启动一个goroutine就添加1
go hello(i)
}
wg.Wait() // 等待所有goroutine结束
}
// 打印顺序为乱序,因为是并发执行,goroutine调度是随机的
匿名函数使用goroutine
func main() {
wg.Add(100)
for i := 0; i < 100; i++ {
go func() {
fmt.Println(i)
wg.Done()
}()
}
wg.Wait()
}
// 注意:闭包函数会引起打印结果重复,解决:将i传到匿名函数里边做值拷贝
func main() {
wg.Add(100000)
for i := 0; i < 100000; i++ {
go func(i int) {
fmt.Println(i)
wg.Done()
}(i)
}
wg.Wait()
}
Golang调度器之GMP模型
GOMAXPROCS
Go运行时调度器使用GOMAXPROCS参数来确定需要使用多少OS线程来同时执行Go代码。
Go语言通过runtime.GOMAXPROCS() 函数设置当前程序并发时占用cpu逻辑核心数
var wg sync.WaitGroup
func a() {
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
}
wg.Done()
}
func b() {
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
}
wg.Done()
}
func main() {
wg.Add(2)
runtime.GOMAXPROCS(1)
go a()
go b()
wg.Wait()
}
运行结果为:先打印完a函数或者b函数值后,在打印另外一个函数值,因为使用1个逻辑核心,属于并发状态。如果设置多个逻辑核心后,打印顺序为乱序,因为此时状态为并行状态。
Go语言中操作系统线程和goroutine的关系:
- 一个操作系统线程对应用户态多个goroutine
- go程序可以同时使用多个操作系统线程
- goroutine和OS线程是多对多的关系,即m:n
channel
channel可以让一个goroutine发送特定的值到另外一个goroutine的通信机制。Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
channel类型
channel是一种引用类型,使用需要make()初始化
var 变量 chan 元素类型
make(chan 元素类型,[缓冲大小])
channel操作
通道有发送(send),接收(receive)和关闭(close)三种操作
ch := make(chan int,2) // 定义一个通道
ch <- 10 // 发送一个数据到通道中
x:= <- ch // 从一个通道中接收值
<- ch // 接收值但忽略结果
close(ch) // 关闭通道
关闭通道注意事项:只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
关闭后通道的特点:
- 对一个关闭的通道再发送值就会导致panic。
- 对一个关闭的通道进行接收会一直获取值直到通道为空。
- 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
- 关闭一个已经关闭的通道会导致panic。
无缓冲通道
无缓冲通道又称为阻塞通道,即创建时候没有添加缓冲大小。一对一服务,发送后需要接收,如果先接收,那就阻塞直到收到发送数据。使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道又称为同步通道。
func recv(c chan int){
ret := <-c
fmt.Println("接收成功",ret)
}
func main(){
ch := make(chan int)
go recv(ch) // 如果没有这个接收,将会死锁
ch <- 10
fmt.Println("发送成功")
}
有缓冲通道
通道初始化时候,指定通道容量。通道容量即为元素数量。使用len函数可以获取通道内元素数量,使用cap可以获取通道的容量。
func main() {
ch := make(chan int, 2)
// go recv(ch) // 如果没有这个接收,将会报错
ch <- 10
fmt.Println("发送成功")
fmt.Printf("通道元素数量:%d,通道容量:%d", len(ch), cap(ch))
}
for range从通道取值
package main
import "fmt"
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
for i := 0; i < 10; i++ {
ch1 <- i
}
close(ch1)
}()
go func() {
for {
i, ok := <-ch1
if !ok {
break
}
ch2 <- i * i
}
close(ch2)
}()
// 在主goroutine中从ch2中取值
for i := range ch2 { // 通道关闭会退出for range
fmt.Println(i)
}
}
// 通常使用for range遍历通道
单项通道
对通道进行限制,只能接收或者发送。 只能发送的通道:chan<- 数据类型 ;只能接收的通道:<- chan 数据类型
func send(ch1 chan<- int) {
for i := 0; i < 10; i++ {
ch1 <- i
}
close(ch1)
}
func recv(ch2 chan<- int, in <-chan int) {
for v := range in {
ch2 <- v * v
}
close(ch2)
}
func printer(in <-chan int) {
for i := range in {
fmt.Println(i)
}
}
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go send(ch1)
go recv(ch2, ch1)
printer(ch2)
}
worker pool(goroutine池)
指定启动goroutine数量,防止goroutine泄露或者暴涨。
package main
import (
"fmt"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("work:%d start job:%d\n", id, j)
time.Sleep(time.Second * 2)
fmt.Printf("work:%d end job:%d\n", id, j)
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
// 开启三个goroutine
for i := 0; i < 3; i++ {
go worker(i, jobs, results)
}
// 5个任务
for j := 0; j < 5; j++ {
jobs <- j
}
close(jobs)
for k := 0; k < 5; k++ {
<-results
}
}
select多路复用
select有case分支,类似于switch,用于处理异步IO操作。select会监听case中channel读写操作,当非阻塞状态会触发相应操作。
- 如果有多个case都可以运行,select会随机公平地选出一个执行,其他不会执行
- 如果没有可运行的case语句,且有default语句,那么就会执行default的动作
- 如果没有可运行的case语句,且没有default语句,select将阻塞,直到某个case通信可以运行
select{
case <-ch1:
...
case data := <-ch2:
...
case ch3<-data:
...
default:
默认操作
}
func main() {
ch := make(chan int, 1) // 当修改通道容量时候,返回值也会不同
for i := 0; i < 10; i++ {
select {
case x := <-ch:
fmt.Println(x) // 0 2 4 6 8 首先进行写入操作,然后才读取操作
case ch <- i:
}
}
}
并发安全和锁
当多个goroutine同时操作一个资源区时,会产生数据竞争,导致结果与预期不符。
var x int64
var wg sync.WaitGroup
func add() {
for i := 0; i < 10000; i++ {
x = x + 1
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}
互斥锁
保证同时只有一个goroutine访问共享资源。使用sync包的Mutex类型来实现。
var x int64
var wg sync.WaitGroup
var lock sync.Mutex
func add() {
for i := 0; i < 10000; i++ {
lock.Lock()
x = x + 1
lock.Unlock()
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}
读写互斥锁
适用于读多写少的场景。对某个受到读写锁保护的共享资源,多个写不能同时操作,读写也不可以同时操作,但是多个读可以同时进行。
- 如果设置了一个写锁,那么其它读的线程以及写的线程都拿不到锁,这个时候,与互斥锁的功能相同
- 如果设置了一个读锁,那么其它写的线程是拿不到锁的,但是其它读的线程是可以拿到锁
var count int
var rwLock sync.RWMutex
var wg sync.WaitGroup
func write() {
rwLock.Lock()
count++
fmt.Printf("写数据%v\n", count)
rwLock.Unlock()
wg.Done()
}
func read() {
rwLock.RLock()
count++
fmt.Printf("读数据%v\n", count)
rwLock.RUnlock()
wg.Done()
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1)
go write()
}
for i := 0; i < 1000; i++ {
wg.Add(1)
go read()
}
wg.Wait()
}
sync.Once
在编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等。
var icons map[string]image.Image
var loadIconsOnce sync.Once
func loadIcons() {
icons = map[string]image.Image{
"left": loadIcon("left.png"),
"up": loadIcon("up.png"),
"right": loadIcon("right.png"),
"down": loadIcon("down.png"),
}
}
// Icon 是并发安全的
func Icon(name string) image.Image {
loadIconsOnce.Do(loadIcons)
return icons[name]
}
sync.Map
Go中Map并不是并发安全的,需要使用sync.Map来实现,里面内置了Store、Load、LoadOrStore、Delete、Range等方法。
var wg sync.WaitGroup
var m = sync.Map{}
func main() {
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
m.Store(key, n)
value, _ := m.Load(key)
fmt.Println(key, value)
wg.Done()
}(i)
}
wg.Wait()
}
在生产环境中最好捕获goroutine异常
defer func() {
if err := recover(); err != nil {
fmt.Println("panic:", err)
}
}()
使用go build -race xxx.go 查看是否存在竞争
go help build可以查看命令操作,其中-race可以看出是否有竞争,编译后运行有竞争会有提示
原子操作
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) | 比较并交换操作 |