Golang 并发编程
并行和并发
- 并行:在同一时刻,有多条指令在多个 CPU 处理器上同时执行
- 并发:在同一时刻,只能有一条指令执行,但多个进程指令被快速地轮换执行
go 语言并发优势
- go 从语言层面就支持了并发
- 简化了并发程序的编写
goroutine 是什么
- 它是 go 并发设计的核心
- goroutine 就是协程,它比线程更小,十几个 goroutine 在底层可能就是五六个线程
- go 语言内部实现了 goroutine 的内存共享,执行 goroutine 只需极少的栈内存(大概是 4~5KB)
Go 语言的并发是基于 goroutine 的,goroutine 类似于线程,但并非线程。可以将 goroutine 理解为一种虚拟线程。Go 语言运行时会参与调度 goroutine,并将 goroutine 合理地分配到每个 CPU 中,最大限度地使用 CPU 性能。开启一个 goroutine 的消耗非常小(大约 2KB 的内存),你可以轻松创建数百万个 goroutine。
goroutine 的特点:
- goroutine 具有可增长的分段堆栈。这意味着它们只在需要时才会使用更多内存。
- goroutine 的启动时间比线程快。
- goroutine 原生支持利用 channel 安全地进行通信。
- goroutine 共享数据结构时无需使用互斥锁。
创建 goroutine
- 只需要在语句前添加 go 关键字,就可以创建并发执行单元
- 开发⼈员无需了解任何执⾏细节,调度器会自动将其安排到合适的系统线程上执行
package main
import (
"fmt"
"time"
)
// 测试协程
func newTask() {
i := 0
for {
i++
fmt.Printf("new goroutine: i = %d\n", i)
time.Sleep(time.Second)
}
}
func main() {
// 子协程调用方法
go newTask()
i := 0
for {
i++
fmt.Printf("main goroutine: i = %d\n", i)
time.Sleep(time.Second)
}
}
如果主协程退出了,其他任务还执行吗?
package main
import (
"fmt"
"time"
)
func main() {
go func() {
i := 0
for {
i++
fmt.Printf("new froutine: i = %d\n", i)
time.Sleep(time.Second)
}
}()
// 主程序退出子协程也会退出
for {}
}
runtime 包
runtime.Gosched()
:让出 CPU 时间片,重新等待安排任务
package main
import (
"fmt"
"runtime"
)
func main() {
go func(s string) {
for i := 0; i < 2; i++ {
fmt.Println(s)
}
}("world")
// 主协程
for i := 0; i < 2; i++ {
// 切一下,再次分配任务
runtime.Gosched()
fmt.Println("hello")
}
}
runtime.Goexit(),退出当前协程
package main
import (
"fmt"
"runtime"
)
func main() {
go func() {
defer fmt.Println("A.defer")
func() {
defer fmt.Println("B.defer")
// 结束协程
runtime.Goexit()
defer fmt.Println("C.defer")
fmt.Println("B")
}()
fmt.Println("A")
}()
for {
}
}
runtime.GOMAXPROCS(),设置跑程序的 CPU 核数
package main
import (
"fmt"
"runtime"
)
func main() {
//runtime.GOMAXPROCS(1)
runtime.GOMAXPROCS(3)
for {
go fmt.Println(0)
fmt.Println(1)
}
}
channel 是什么
- goroutine 运行在相同的地址空间,因此访问共享内存必须做好同步,处理好线程安全问题
- goroutine 奉行通过通信来共享内存,而不是共享内存来通信
- channel 是一个引用类型,用于多个 goroutine 通讯,其内部实现了同步,确保并发安全
channel 的基本使用
- channel 可以用内置 make()函数创建
- 定义一个 channel 时,也需要定义发送到 channel 的值的类型
make(chan 类型)
make(chan 类型, 容量)
- 当 capacity= 0 时,channel 是无缓冲阻塞读写的,当 capacity> 0 时,channel 有缓冲、是非阻塞的,直到写满 capacity 个元素才阻塞写入
- channel 通过操作符<-来接收和发送数据,发送和接收数据语法:
channel <- value //数据存入管道
<-channel //管道取数据
x := <-channel //接收管道数据
x, ok := <-channel //功能根上面类似,OK是布尔值,检查管道是否为空或已经关闭
package main
import "fmt"
func main() {
// 创建一个存储int类型的channel
c := make(chan int)
// 一个协程写入数据
go func() {
defer fmt.Println("子协程结束")
fmt.Println("子协程正在运行")
// 存数据
c <- 666
}()
// 外面读数据
num := <- c
fmt.Println("num = ", num)
fmt.Println("main 结束")
}
无缓冲的 channel
- 无缓冲的通道是指在接收前没有能力保存任何值的通道
有缓冲的 channel
- 有缓冲的通道是一种在被接收前能存储一个或者多个值的通道
package main
import (
"fmt"
"time"
)
func main() {
// 0 代表无缓冲通道
//c := make(chan int, 0)
c := make(chan int, 3)
fmt.Printf("len(c)=%d, cap(c)=%d\n", len(c), cap(c))
// 子协程去写数据
go func() {
defer fmt.Printf("子协程结束了")
for i := 0; i < 3; i++ {
c <- i
fmt.Printf("子协程在运行[%d], len(c)=%d, cap(c)=%d\n", i, len(c), cap(c))
}
}()
time.Sleep(2 * time.Second)
for i := 0; i < 3; i++ {
num := <-c
fmt.Println("num=", num)
}
fmt.Println("main 结束")
}
close()
- 可以通过内置的 close()函数关闭 channel
package main
import "fmt"
func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
close(c)
}()
for {
if data, ok := <-c; ok {
fmt.Println(data)
} else {
fmt.Println("结束了")
break
}
}
fmt.Println("main 结束")
}
单方向的 channel
- 默认情况下,通道是双向的,也就是,既可以往里面发送数据也可以接收数据
- go 可以定义单方向的通道,也就是只发送数据或者只接收数据,声明如下
var ch1 chan int //正常的,可以读,可以写
var ch2 chan<- float64 //只写float64的管道
var ch3 <-chan int //只读int的管道
- 可以将 channel 隐式转换为单向队列,只收或只发,不能将单向 channel 转换为普通 channel
package main
import "fmt"
func main() {
// 定义通道
c := make(chan int, 3)
// chan转为只写的
var send chan<- int = c
// chan 转为只读
var recv <-chan int = c
send <- 1
fmt.Println(<-recv)
// 不能从单向转换回去
}
package main
import "fmt"
// 生产者 只写
func producter(out chan<- int) {
defer close(out)
for i := 0; i < 5; i++ {
out <- i
}
}
// 消费者 只读
func cunsumer(in <-chan int) {
for num := range in {
fmt.Println(num)
}
}
func main() {
// 定义通道
c := make(chan int, 3)
// chan转为只写的
var send chan<- int = c
// chan 转为只读
var recv <-chan int = c
go producter(send)
cunsumer(recv)
fmt.Println("main 结束")
}
Workpool 模型
- 本质上是生产者消费者模型
- 可以有效控制 goroutine 数量,防止暴涨
- 需求:
- 计算一个数字的各个位数之和,例如数字 123,结果为 1+2+3=6
- 随机生成数字进行计算
package main
import (
"fmt"
"math/rand"
)
type Job struct {
Id int
RandNum int
}
type Result struct {
job *Job
sum int
}
// 创建任务池
func createPoll(poolSize int, jobChan chan *Job, resultChan chan *Result) {
// 根据指定的poolSize去启动协程个数
for i := 0; i < poolSize; i++ {
go func(jobChan chan *Job, resultChan chan *Result) {
for job := range jobChan {
r_num := job.RandNum
var sum int
for r_num != 0 {
tmp := r_num % 10
sum += tmp
r_num /= 10
}
r := &Result{
job: job,
sum: sum,
}
resultChan <- r
}
}(jobChan, resultChan)
}
}
func main() {
// 初始化管道
jobChan := make(chan *Job, 128)
resultChan := make(chan *Result, 128)
// 创建任务
createPoll(64, jobChan, resultChan)
// 开启协程去读数据
go func(resultChan chan *Result) {
for result := range resultChan {
fmt.Printf("job id: %d, randNum: %d, result: %d\n",
result.job.Id, result.job.RandNum, result.sum)
}
}(resultChan)
// 循环生成随机数
var id int
for {
id++
rand_num := rand.Int()
job := &Job{
Id: id,
RandNum: rand_num,
}
jobChan <- job
}
}
定时器
Timer:时间到了,执行只执行 1 次
package main
import (
"fmt"
"time"
)
func main() {
// 1. timer基本使用
//timer01 := time.NewTimer(2 * time.Second)
//t1 := time.Now()
//fmt.Printf("t1:%v\n", t1)
//t2 := <- timer01.C
//fmt.Printf("t2:%v\n", t2)
// 2. 验证timer只能响应1次
//timer02 := time.NewTimer(time.Second)
//for {
// <- timer02.C
// fmt.Println("时间到了")
//}
// 3. 实现延时的功能
// 1)
//time.Sleep(time.Second)
// 2)
//timer03 := time.NewTimer(2*time.Second)
//<- timer03.C
//fmt.Println("2秒到")
// 3)
//<- time.After(2*time.Second)
//fmt.Println("2秒到")
// 4. 停止定时器
//timer04 := time.NewTimer(2*time.Second)
//go func() {
// <-timer04.C
// fmt.Println("定时器执行力")
//}()
//b := timer04.Stop()
//if b {
// fmt.Println("timer04定时器关闭了")
//}
// 5. 重置定时器
timer05 := time.NewTimer(3*time.Second)
timer05.Reset(1*time.Second)
fmt.Println(time.Now())
fmt.Println(<-timer05.C)
}
Ticker:时间到了,多次执行
package main
import (
"fmt"
"time"
)
func main() {
// 1. 获取ticker对象
ticker := time.NewTicker(1*time.Second)
i := 0
go func() {
for {
i++
fmt.Println(<-ticker.C)
if i == 5 {
ticker.Stop()
}
}
}()
for{}
}
select
- go 语言提供了 select 关键字,可以监听 channel 上的数据流动
- 语法与 switch 类似,区别是 select 要求每个 case 语句里必须是一个 IO 操作
select {
case <-chan1:
// 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行该case处理语句
default:
// 如果上面都没有成功,则进入default处理流程
}
select 可以同时监听一个或多个 channel,直到其中一个 channel ready
package main
import (
"fmt"
"time"
)
func test01(ch chan string) {
time.Sleep(2 * time.Second)
ch <- "hello"
}
func test02(ch chan string) {
time.Sleep(5 * time.Second)
ch <- "world"
}
func main() {
chan01 := make(chan string)
chan02 := make(chan string)
go test01(chan01)
go test02(chan02)
select {
case s1 := <-chan01:
fmt.Println("s1=", s1)
case s2 := <-chan02:
fmt.Println("s2=", s2)
}
}
** 如果多个 channel 同时 ready,则随机选择一个执行**
package main
import "fmt"
func main() {
ch01 := make(chan int)
ch02 := make(chan int)
go func() {
ch01 <- 1
}()
go func() {
ch02 <- 2
}()
select {
case val := <-ch01:
fmt.Println("ch01, ->", val)
case val := <-ch02:
fmt.Println("ch02, ->", val)
}
}
可以用于判断管道是否存满
package main
import (
"fmt"
"time"
)
func writeChan(ch chan int) {
var i int
for {
i++
select {
case ch <- i:
fmt.Println("set val: ", i)
default:
fmt.Println("channel full")
}
time.Sleep(500*time.Millisecond)
}
}
func main() {
// 定义管道
ch := make(chan int, 10)
// 开启协程去写数据
go writeChan(ch)
// 取数据
for i := range ch {
fmt.Println("get val: ", i)
time.Sleep(2*time.Second)
}
}
等待组
sync.WaitGroup
:用来等待一组子协程的结束,需要设置等待的个数,每个子协程结束后要调用 Done(),最后在主协程中 Wait()即可- 有 3 个方法
Add()
:添加计数Done()
:操作结束,计数减 1Wait()
:等待所有操作结束
package main
import "fmt"
// 手动实现协程等待
func main() {
// 创建管道
ch := make(chan int)
// 计数,代表子协程个数
count := 2
go func() {
fmt.Println("子协程01")
ch <- 1
}()
go func() {
fmt.Println("子协程02")
ch <- 1
}()
for range ch {
count --
if count == 0 {
close(ch)
}
}
}
package main
import (
"fmt"
"sync"
)
// 等待组
func main() {
// 声明等待组
var wg sync.WaitGroup
wg.Add(2)
go func() {
fmt.Println("子协程01")
// 协程完成执行done操作
wg.Done()
}()
go func(){
defer wg.Done()
fmt.Println("子协程02")
} ()
wg.Wait()
}
互斥锁
- go 中 channel 实现了同步,确保并发安全,同时也提供了锁的操作方式,确保多个协程的安全问题
- go 中 sync 包提供了锁相关的支持
- Mutex 互斥锁:以加锁方式解决并发安全问题
package main
import (
"fmt"
"sync"
)
// 没有对全局变量加锁,多个修改操作同时对x修改会产生数据错误
var x int
var wg sync.WaitGroup
func add() {
defer wg.Done()
for i := 0; i < 5000; i++ {
x += 1
}
}
func main() {
wg.Add(3)
go add()
go add()
go add()
wg.Wait()
fmt.Println("x->", x)
}
package main
import (
"fmt"
"sync"
)
// 没有对全局变量加锁,多个修改操作同时对x修改会产生数据错误
var x int
var wg sync.WaitGroup
// 声明锁
var mutex sync.Mutex
func add() {
defer wg.Done()
for i := 0; i < 5000; i++ {
// 加锁
mutex.Lock()
x += 1
// 解锁
mutex.Unlock()
}
}
func main() {
wg.Add(3)
go add()
go add()
go add()
wg.Wait()
fmt.Println("x->", x)
}
读写锁
- 分为读锁和写锁
- 当读操作多时,不涉及数据修改,应该允许程序同时去读,写的时候再加锁
package main
import (
"fmt"
"sync"
"time"
)
// 声明读写锁
var rwLock sync.RWMutex
var wg sync.WaitGroup
// 全局变量
var x int
func write() {
rwLock.Lock()
fmt.Println("write rwlock")
x += 1
time.Sleep(2 * time.Second)
fmt.Println("write rwunlock")
rwLock.Unlock()
wg.Done()
}
func read(i int) {
rwLock.RLock()
fmt.Println("read rwlock")
fmt.Printf("gorountine: %d x=%d\n", i, x)
time.Sleep(5 * time.Second)
fmt.Println("read rwunlock")
rwLock.RUnlock()
wg.Done()
}
func main() {
wg.Add(1)
go write()
time.Sleep(time.Millisecond * 50)
for i := 0; i < 10; i++ {
wg.Add(1)
go read(i)
}
wg.Wait()
}
读写锁与互斥锁性能对比
package main
import (
"fmt"
"sync"
"time"
)
// 声明读写锁
var rwLock sync.RWMutex
// 声明互斥锁
var mutex sync.Mutex
// 声明等待组
var wg sync.WaitGroup
// 全局变量
var x int
// 写数据
func write() {
for i := 0; i < 100; i++ {
//mutex.Lock()
rwLock.Lock()
x += 1
time.Sleep(10 * time.Millisecond)
//mutex.Unlock()
rwLock.Unlock()
}
wg.Done()
}
func read(i int) {
for i := 0; i < 100; i++ {
//mutex.Lock()
rwLock.RLock()
time.Sleep(time.Millisecond)
//mutex.Unlock()
rwLock.RUnlock()
}
wg.Done()
}
// 互斥锁运行时间: 14551861000
// 读写锁运行时间: 1321229000
func main() {
start := time.Now().UnixNano()
wg.Add(1)
go write()
for i := 0; i < 100; i++ {
wg.Add(1)
go read(i)
}
wg.Wait()
end := time.Now().UnixNano()
fmt.Println("运行时间: ", end-start)
}
原子操作
- 加锁操作比较耗时,整数可以使用原子操作保证线程安全
- 原子操作在用户态就可以完成,因此性能比互斥锁高
- 原子操作方法在 doc.go 文件中
- AddXxx():加减操作
- CompareAndSwapXxx():比较并交换
- LoadXxx():读取操作
- StoreXxx():写入操作
- SwapXxx():交换操作
原子操作与互斥锁性能对比
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
// 声明互斥锁
var mutex sync.Mutex
// 声明等待组
var wg sync.WaitGroup
// 全局变量
var x int32
// 互斥锁操作
func add01() {
for i := 0; i < 500; i++ {
mutex.Lock()
x += 1
mutex.Unlock()
}
wg.Done()
}
// 原子操作
func add02() {
for i := 0; i < 500; i++ {
atomic.AddInt32(&x, 1)
}
wg.Done()
}
// 互斥锁运行时间: 1896256000
// 原子操作运行时间: 89723000
func main() {
start := time.Now().UnixNano()
for i := 0; i < 10000; i++ {
wg.Add(1)
//go add01()
go add02()
}
wg.Wait()
end := time.Now().UnixNano()
fmt.Println("x: ", x)
fmt.Println("运行时间: ", end-start)
}
Map 的并发操作
Map 不是并发安全的,但并发读是没问题的
package main
import (
"fmt"
"sync"
)
func main() {
wg := sync.WaitGroup{}
m := make(map[int]int)
// 添加一些数据
for i := 0; i < 10; i++ {
m[i] = i
}
// 并发去读
for i := 0; i < 10; i++ {
wg.Add(1)
go func(k int) {
fmt.Println(m[k])
wg.Done()
}(i)
}
wg.Wait()
fmt.Println(m)
}
Map 并发写是有问题的
package main
import (
"fmt"
"sync"
)
func main() {
wg := sync.WaitGroup{}
m := make(map[int]int)
mutex := sync.Mutex{}
// 并发写
for i := 0; i < 10; i++ {
wg.Add(1)
go func(k int) {
defer wg.Done()
mutex.Lock()
m[k] = k
mutex.Unlock()
}(i)
}
wg.Wait()
fmt.Println(m)
}
Golang 为什么这么块: Go 为什么这么“快”-腾讯技术