并发编程
并发介绍
1、并发和并行
A. 多线程程序在一个核的cpu上运行,就是并发。
B. 多线程程序在多个核的cpu上运行,就是并行。
并发:本质还是串行
食堂窗口一个大妈(同一时间类只能给一个人打饭)
python本质没有并行的线程
并行:任务分布在不同CPU上,同一时间点同时执行
并行就是有多个食堂大妈,同事给不同人打饭
2、协程和线程
- 协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。
- 线程:一个线程上可以跑多个协程,协程是轻量级的线程。
- 线程和协程最大的区别
- 开启一个线程需要大概2M空间,而且需要cpu调度才能执行,线程会强cpu
- 开启一个协程大概只需要2K的空间,而且是有go解释器自己实现的GPM调度,主动退出
- 所以我们可以同时启动成千上万个goroutine二不会过大的占用内存
- 相反如果我们开启成千上万个线程,第一,会大量的占用内存,甚至导致机器奔溃,第二,操作系统调度线程,本身也需要耗费大量时间
- 协程如果需要用CPU才会去使CPU,如果没有使用CPU的需求,他就会主动把cpu让给其他协程执行
- 线程在时间片内,即使不使用cpu,比如当前正在从磁盘读数据,他也不会让出cpu
goroutine
1、多线程编程缺点
- 在java/c++中我们要实现并发编程的时候,我们通常需要自己维护一个线程池
- 并且需要自己去包装一个又一个的任务,同时需要自己去调度线程执行任务并维护上下文切换
2、gouroutine
- Go语言中的goroutine就是这样一种机制,goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。
- Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。
- Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经 内置了调度和上下文切换的机 制 。
- 在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能–goroutine
- 当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数
- 开启一个goroutine去执行这个函数就可以了,就是这么简单粗暴。
协程基本使用
1、启动一个协程
- 主线程中每个100毫秒打印一次,总共打印2次
- 另外开启一个协程,打印10次
- 情况一:打印是交替,证明是并行的
- 情况二:开启的协程打印两次,就退出了(因为主线程退出了)
package main
import (
"fmt"
"time"
)
func main() {
// goroutine并行测试
//test()
// go中开启一个协程 go关键字 方法名()
go test()
for i := 0; i < 10; i++ {
fmt.Println("main", i)
time.Sleep(time.Microsecond * 100)
}
time.Sleep(time.Second)
fmt.Println("over")
}
func test() {
for i := 0; i < 10; i++ {
fmt.Println("test", i)
time.Sleep(time.Microsecond * 100)
}
}
/*
main 0
test 0
main 1
test 1
test 2
main 2
test 3
main 3
test 4
test 5
main 4
main 5
test 6
main 6
test 7
main 7
test 8
main 8
main 9
test 9
over
*/
2、WaitGroup
主线程退出后所有的协程无论有没有执行完毕都会退出
所以我们在主进程中可以通过WaitGroup等待协程执行完毕
sync.WaitGroup
内部维护着一个计数器,计数器的值可以增加和减少。
例如当我们启动了N 个并发任务时,就将计数器值增加N。
每个任务完成时通过调用Done()
方法将计数器减1
。
通过调用Wait()
来等待并发任务执行完,当计数器值为0
时,表示所有并发任务已经完成。
var wg sync.WaitGroup // 第一步:定义一个计数器 wg.Add(1) // 第二步:开启一个协程计数器+1 wg.Done() // 第三步:协程执行完毕,计数器-1
wg.Wait() // 第四步:计数器为0时推出
package main
import (
"fmt"
"sync"
"time"
)
/*
// 计数器怎么知道有一共有多少次的
goroutine开启的时候进行 wg.Add(1) 加一
goroutine结束是进行 wg.Done() 加1
wg.Wait() 会判断当前的goroutine是否为0,为0就退出
var wg sync.WaitGroup // 第一步:定义一个计数器
wg.Add(1) // 第二步:开启一个协程计数器+1
wg.Done() // 第三步:协程执行完毕,计数器-1
wg.Wait() // 第四步:计数器为0时推出
*/
var wg sync.WaitGroup // 第一步:定义一个计数器
func main() {
wg.Add(2) // 第二步:开启一个协程计数器+1
go test()
go test()
// wg.Wait()发现没有goroutine在使用而我们的wg不为0,那么就会触发异常
wg.Wait() // 第四步:计数器为0时推出
//time.Sleep(time.Second)
fmt.Println("main over")
}
func test() {
for i := 0; i < 10; i++ {
fmt.Println(i)
time.Sleep(100 * time.Microsecond)
}
wg.Done() // 第三步:协程执行完毕,计数器-1
}
3、开启多个协程
在 Go 语言中实现并发就是这样简单,我们还可以启动多个 goroutine。
这里使用了 sync.WaitGroup 来实现等待 goroutine 执行完毕
多次执行上面的代码,会发现每次打印的数字的顺序都不一致。
这是因为 10 个 goroutine是并发执行的,而 goroutine 的调度是随机的。
上面例子中使用循环并多次调用。
channel
1、Channel说明
- 共享内存交互数据弊端
- 单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。
- 虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。
- 为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。
- channel好处
- Go 语言中的通道(channel)是一种特殊的类型。
- 通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。
- 每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
- 如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。
- channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。
2、channel类型
channel 是一种类型,一种引用类型
声明管道类型的格式如下:
var 变量 chan 元素类型
var ch1 chan int // 声明一个传递整型的管道
var ch2 chan bool // 声明一个传递布尔型的管道
var ch3 chan []int // 声明一个传递 int 切片的管道
3、创建channel
声明的管道后需要使用 make
函数初始化之后才能使用。
创建 channel 的格式如下: make(chan 元素类型, 容量)
// 创建一个能存储 10 个 int 类型数据的管道
ch1 := make(chan int, 10)
// 创建一个能存储 4 个 bool 类型数据的管道
ch2 := make(chan bool, 4)
// 创建一个能存储 3 个[]int 切片类型数据的管道
ch3 := make(chan []int, 3)
4、channel操作
package main
import "fmt"
/*
channel是给不同的goroutine进行数据传递,类似于队列
*/
func main() {
// 1、定义一个channel
// make可以给切片、map、channel分配内存
/*
chan : 定义channel的关键字
int : 指定当前这个channel的数据类型
5 : 定义这个channel的大小
*/
ch := make(chan int, 5)
// 2、向channel存入数据
ch <- 10
// 3、从channel中取数据
v1 := <-ch
fmt.Println("v1", v1)
// 4、如果取值是一个没有数据且没有close的channel,就会报错
//v2 := <-ch
//fmt.Println(v2)
}
5、优雅的从channel取值
当通过通道发送有限的数据时,我们可以通过close函数关闭通道来告知从该通道接收值的goroutine停止等待。
当通道被关闭时,往该通道发送值会引发panic,从该通道里接收的值一直都是类型零值。
那如何判断一个通道是否被关闭了呢?
for range的方式判断通道关闭
package main
import "fmt"
/*
channel是给不同的goroutine进行数据传递,类似于队列
*/
func main() {
// 5、for range从channel取值
ch := make(chan int, 5)
ch <- 1
ch <- 2
ch <- 3
ch <- 4
ch <- 5
//ch <- 6
close(ch)
// 当我们循环一个没有close的channel,而且还为空就会报错
// fatal error: all goroutines are asleep - deadlock!
for i := range ch {
fmt.Println(i)
}
}
select 多路复用
1、select说明
传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock,在实际开发中,可能我们不好确定什么关闭该管道。
这种方式虽然可以实现从多个管道接收值的需求,但是运行性能会差很多。
为了应对这种场景,Go 内置了 select 关键字,可以同时响应多个管道的操作。
select 的使用类似于 switch 语句,它有一系列 case 分支和一个默认的分支。
每个 case 会对应一个管道的通信(接收或发送)过程。
select 会一直等待,直到某个 case 的通信操作完成时,就会执行 case 分支对应的语句。
具体格式如下:
package main
import (
"fmt"
"time"
)
func main() {
// 定义channel
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
}
// 定义channel
strChan := make(chan string, 10)
for i := 0; i < 10; i++ {
strChan <- "aaa"
}
for {
select {
case v := <-strChan:
// 写业务逻辑
fmt.Println("strChan", v)
time.Sleep(time.Microsecond * 100)
case v := <-intChan:
fmt.Println("intChan", v)
time.Sleep(time.Microsecond * 100)
default:
fmt.Println("所有数据取出")
return
}
}
}
2、select 的使用
使用 select 语句能提高代码的可读性。
可处理一个或多个 channel 的发送/接收操作。
如果多个 case 同时满足,select 会随机选择一个。
对于没有 case
的 select{}
会一直等待,可用于阻塞 main
函数。
package main
import (
"fmt"
"time"
)
func main() { // 在某些场景下我们需要同时从多个通道接收数据,这个时候就可以用到golang中给我们提供的select多路复用
// 1.定义一个管道 10个数据int
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
} //2.定义一个管道 5个数据string
stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan <- "hello" + fmt.Sprintf("%d", i)
} //使用select来获取channel里面的数据的时候不需要关闭channel
for {
select {
case v := <-intChan:
fmt.Printf("从 intChan 读取的数据%d\n", v)
time.Sleep(time.Millisecond * 50)
case v := <-stringChan:
fmt.Printf("从 stringChan 读取的数据%v\n", v)
time.Sleep(time.Millisecond * 50)
default:
fmt.Printf("数据获取完毕")
return //注意退出...
}
}
}
互斥锁
互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个 goroutine 可以访问共享资源。
Go语言中使用 sync 包的 Mutex 类型来实现互斥锁。
使用互斥锁来修复上面代码的问题:
package main
import (
"fmt"
"sync"
)
/*
当开启多个协程对同一个资源进行操作的时候,就出现了资源竞争
比如开两个协程对x同时加5000次正确结构是10000,实际结构是不相符
*/
var x int
var wg sync.WaitGroup
var lock sync.Mutex
func main() {
fmt.Println(x)
for i := 0; i < 4000; i++ {
wg.Add(1)
go add()
}
wg.Wait()
fmt.Println(x) // 期待结构是10000
}
func add() {
for i := 1; i <= 5000; i++ {
// 当要对x操作时,先加锁,能获取锁再操作
// 保证同一时刻只有一个协程能够对变量进行操作
lock.Lock()
x = x + 1
lock.Unlock() // 操作完成释放锁
}
wg.Done()
}