一、并发和并行
并发:
同一时间段内,多个任务再执行。(单个CPU,执行多个任务)
Go语言原生支持并发,Go使用Go协程(Goroutine)和信道(Channel)来处理并发
并行
同一时刻,多个任务在执行(多个CPU的支持)
go实现并发
package main
import (
"fmt"
"time"
)
// goroutine--->协程 ---2kb大小
// 线程:启动一个线程需要2-5m 也就是python开进程,其余的都是开线程
// go协程会复用线程,
// go协程间使用channel(信道),而不推崇用共享变量通信(锁,死锁)
//启动一个goroutine
func test() {
fmt.Println("GO!")
}
func main() {
fmt.Println("主线程开始执行!")
go test()
go test()
go test()
go test()
go test()
for i := 0; i < 10; i++ {
go func() {
fmt.Println("Go Go!---", i)
}()
}
time.Sleep(1 * time.Second) // time 从纳秒开始 所以要选择规格
// go语言中,主线程不会等待goroutine执行完成,要等待结束需要自己处理
fmt.Println("主线程执行结束!")
}
// Go关键字开启goroutine,一个goroutine只占2kb左右
Go的GMP模型
G(Goroutine)
Goroutine启动之后其实是放在全局的队列里,开多少个就放了多少个,然后通过P交给M执行。
M(当成操作系统真正的线程,实际上是用户线程)
既然M是线程那么他的作用肯定是执行我们的Goroutine(通过P),因为P后面携带一个或多个Goroutine,当线程执行Goroutine时发现有IO他就会自动切换到P后面的下一个Groutine执行,把阻塞的放在后面,如此反复。
用户线程,操作系统线程
python中,开的线程开出用户线程,用户线程跟操作系统线程1:1
的对应关系,所有语言开线程都是通知操作系统开线程,而不是直接开,所以用户线程是在操作系统线程之上的。
某些语言用户线程和操作系统线程是N:1
的关系
Go语言,用户线程和操作系统线程是N:M
关系,但是我们不用管用户线程和操作系统线程,所以我们可以把他当为1:1
关系
P(Processor)
现在版本默认情况是CPU核数,老版本是1,,负责和M做交互,将全区队列中的G取出来交给M执行,每个P携带的G的个数比较均匀
操作系统调度器把操作系统的线程
丢到CPU
去执行,一个CPU执行多个系统线程,当其中一个线程出现了IO交出了时间片,下一个线程就会被替补上,根本没有休息的!
Channel(信道)
协程之间的通信,通过channel实现
定义Channel
package main
import (
"fmt"
"time"
)
func test1(a chan string) {
fmt.Println("Go down!")
time.Sleep(1 * time.Second)
// 往信道放值 很像python中的队列
a <- "111"
}
func main() {
//1 定义channel
// 可以定义为空接口
var c chan string
//2 信道的零值nil(引用类型,空值为nil吗,当做参数传递时,不需要取值,改就是原值,需要初始化)
fmt.Println(c)
//3 信道初始化
sr := make(chan string,1)
//4 信道的放值(注意放值和复制)
sr<- "ssss" // 赋值会报错 c = "sss"
//5 信道取值
//<-c
//6 取出来赋值给一个变量
a := <-sr //这样类型推到 不用关注取出来的类型
fmt.Println(a)
// 默认情况下往信道中放值取值都是阻塞的
//go test1(c)
}
此时的ch1通道可以称为缓冲通道,在缓冲满载(缓冲被全部使用)之前,给一个带缓冲的通道发送数据是不会阻塞的,而从通道读取数据也不会阻塞,直到缓冲空了。定义方法如:ch:=make(chan type, value)。这个value表示缓冲容量,它的大小和类型无关,所以可以给一些通道设置不同的容量,只要它们拥有相同的元素类型。内置的cap函数可以返回缓冲区的容量,如果容量大于0,通道就是异步的了。缓冲满载(发送)或变空(接收)之前通信不会阻塞,元素会按照发送的顺序被接收。如果容量是0或者未设置,通信仅在收发双方准备好的情况下才可以成功。
这种异步channel可以减少排队阻塞,在你的请求激增的时候表现得更好,更具伸缩性。
Channel的小栗子
package main
import (
"fmt"
"time"
)
// 程序中有一个数串,求出每一位的平方和立方和,并将它们加起来,打印
func calcSquares(number int, squareop chan int) {
sum := 0 //总和
for number != 0 {
digit := number % 10 // 对10取余数 19999取余9
sum += digit * digit
number /= 10 // 除以10 num=1999
}
time.Sleep(2*time.Second)
squareop <- sum
}
func calcCubes(number int, cubeop chan int) {
sum := 0 //总和
for number != 0 {
digit := number % 10 // 对10取余数 19999取余9
sum += digit * digit * digit
number /= 10 // 除以10 num=1999
}
time.Sleep(1*time.Second)
cubeop <- sum
}
func main() {
ctime := time.Now().Unix() // 获取当前时间
fmt.Println(ctime)
number := 19999
sqrch := make(chan int)
cubech := make(chan int)
go calcCubes(number, cubech)
go calcSquares(number, sqrch)
squares, cubes := <-sqrch, <-cubech
fmt.Println("最终结果是:", squares+cubes)
fmt.Println("花费时间是:", (time.Now().Unix() - ctime))
}
信道的关闭,循环和死锁
信道的死锁现象,默认都是阻塞的,一旦有一个放,没有人取,或者有人取没有人放,就会出现死锁
package main
import "fmt"
func main() {
var c = make(chan int)
//1 不要强塞
c <- 111
//2 没值强取
fmt.Println(<-c)
}
单向信道
package main
import "fmt"
func main() {
//只写信道,goroutine中只能写,不能往外读,但是可以人为控制
var c = make(chan<- int)
// 只读信道
var d = make(<-chan int)
c <- 1
c <- 2
c <- 3
}
关闭信道(循环信道)
package main
import (
"fmt"
"time"
)
func producer(ch chan int) {
for i := 0; i < 10; i++ {
ch <- i
fmt.Println("Put",i)
fmt.Println(len(ch))
}
close(ch)
}
func main() {
ch := make(chan int)
go producer(ch)
//循环信道,最开始没有数据,就一直阻塞在这里,等到producer起来
// 放一个取一个,放一个取一个,交叉着去执行
// 他会自动检测信道,如果close了 那循环也结束了
for v := range ch {
time.Sleep(1*time.Second) // 验证是否阻塞
fmt.Println("Reciber", v)
}
}
缓冲信道
信道默认是阻塞的,缓冲信道意思就是,信道里默认缓冲一点东西,就不阻塞了,只有放满了,或者没有了,才阻塞
package main
import "fmt"
// 也就是设置queue的大小 满了做什么没满做什么
//
func main() {
var c = make(chan int, 1) //无缓冲信道数字是0
c <- 1
//c <- 2 // 放满了 出现死锁
// 死锁(一个goroutine会出现)
a := <- c
fmt.Println(a)
}
长度和容量
len长度:目前有多少
cap容量:最多可以放多少
func main() {
var d = make(chan int,6)
d <-1
d <-1
d <-1
d <-1
fmt.Println(len(d))
fmt.Println(cap(d))
}
WaitGroup
等待所有的Goroutine执行完成
package main
import (
"fmt"
"sync"
"time"
)
// 也就是设置queue的大小 满了做什么没满做什么
//
func process(i int, wg *sync.WaitGroup) {
fmt.Println("Started", i)
time.Sleep(2 * time.Second)
fmt.Println("Ended", i)
wg.Done()
}
func main() {
var wg sync.WaitGroup // 没有初始化,值类型,当做参数传递,需要取地址
fmt.Println(wg)
for i := 0; i < 10; i++ {
wg.Add(1) //启动一个goroutine add+1,里面有个计数器就down-1
go process(i, &wg)
}
wg.Wait() // 一直阻塞在这里,直到10个done,计数器减到0
}
Select(比较有用)
select
语句用于在多个发送/接收信道操作中进行选择。select
语句会一直阻塞,直到发送/接收操作准备就绪。如果有多个信道操作准备完毕,select
会随机地选取其中之一执行。该语法与switch
类似,所不同的是,这里的每个case
语句都是信道操作。我们好好看一些代码来加深理解吧。
假设要去爬取百度,发出三次请求,由于网络原因不知道谁先回来,select就是谁先回来,就先用谁,类似于io多路复用
package main
// select 语句用于在多个发送/接收信道操作中进行选择。
import (
"fmt"
"time"
)
// 两个模拟去数据库拿数据,然后放到信道里
func server1(ch chan string) {
time.Sleep(6 * time.Second) // 模拟延时
ch <- "from server1"
}
func server2(ch chan string) {
time.Sleep(3 * time.Second)
ch <- "from server2"
}
func main() {
output1 := make(chan string)
output2 := make(chan string)
// 开启两个携程执行server
go server1(output1)
go server2(output2)
// 类似于switch 判断谁先拿到值,然后执行相应语句
select {
case s1 := <-output1: // 信道1取值
fmt.Println(s1)
case s2 := <-output2: // 信道2取值
fmt.Println(s2)
}
}
----------------------------------------------------------------
from server2
就是挑一个最快的去执行
package main
import (
"fmt"
"time"
)
func process(ch chan string) {
time.Sleep(10500 * time.Millisecond)
ch <- "process successful"
}
func main() {
output1 := make(chan string)
output2 := make(chan string)
// 开启两个携程执行server
go server1(output1)
go server2(output2)
// 类似于switch 判断谁先拿到值,然后执行相应语句
select {
case s1 := <-output1: // 信道1取值
fmt.Println(s1)
case s2 := <-output2: // 信道2取值
fmt.Println(s2)
//当其他情况都不匹配时,将运行默认情况,就可以做其他事,模拟非阻塞io
default:
fmt.Println("Not ready")
}
}
- 除 default 外,如果只有一个 case 语句评估通过,那么就执行这个case里的语句;
- 除 default 外,如果有多个 case 语句评估通过,那么通过伪随机的方式随机选一个;
- 如果 default 外的 case 语句都没有通过评估,那么执行 default 里的语句;
- 如果没有 default,那么 代码块会被阻塞,指导有一个 case 通过评估;否则一直阻塞
死锁与默认情况
package main
func main() {
ch := make(chan string)
select {
case <-ch:
}
}
上面的程序中,我们在第 4 行创建了一个信道 ch
。我们在 select
内部(第 6 行),试图读取信道 ch
。由于没有 Go 协程向该信道写入数据,因此 select
语句会一直阻塞,导致死锁
Mutex
临界区
在学习 Mutex 之前,我们需要理解并发编程中临界区(Critical Section)的概念。当程序并发地运行时,多个 [Go 协程]不应该同时访问那些修改共享资源的代码。这些修改共享资源的代码称为临界区。
x = x + 1
如果只有一个 Go 协程访问上面的代码段,那都没有任何问题。
但当有多个协程并发运行时,代码却会出错,让我们看看究竟是为什么吧。简单起见,假设在一行代码的前面,我们已经运行了两个 Go 协程。
在上一行代码的内部,系统执行程序时分为如下几个步骤(这里其实还有很多包括寄存器的技术细节,以及加法的工作原理等,但对于我们的系列教程,只需认为只有三个步骤就好了):
- 获得 x 的当前值
- 计算 x + 1
- 将步骤 2 计算得到的值赋值给 x
如果只有一个协程执行上面的三个步骤,不会有问题。
我们讨论一下当有两个并发的协程执行该代码时,会发生什么。下图描述了当两个协程并发地访问代码行 x = x + 1
时,可能出现的一种情况。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jl1HHHRU-1614922292346)(http://file.muyutech.com/error-img.png)]
我们假设 x
的初始值为 0。而协程 1 获取 x
的初始值,并计算 x + 1
。而在协程 1 将计算值赋值给 x
之前,系统上下文切换到了协程 2。于是,协程 2 获取了 x
的初始值(依然为 0),并计算 x + 1
。接着系统上下文又切换回了协程 1。现在,协程 1 将计算值 1 赋值给 x
,因此 x
等于 1。然后,协程 2 继续开始执行,把计算值(依然是 1)复制给了 x
,因此在所有协程执行完毕之后,x
都等于 1。
现在我们考虑另外一种可能发生的情况。
在上面的情形里,协程 1 开始执行,完成了三个步骤后结束,因此 x
的值等于 1。接着,开始执行协程 2。目前 x
的值等于 1。而当协程 2 执行完毕时,x
的值等于 2。
所以,从这两个例子你可以发现,根据上下文切换的不同情形,x
的最终值是 1 或者 2。这种不太理想的情况称为竞态条件(Race Condition),其程序的输出是由协程的执行顺序决定的。
在上例中,如果在任意时刻只允许一个 Go 协程访问临界区,那么就可以避免竞态条件。而使用 Mutex 可以达到这个目的。
Mutex
Mutex 用于提供一种加锁机制(Locking Mechanism),可确保在某时刻只有一个协程在临界区运行,以防止出现竞态条件。
共享变量–一定会涉及到并发安全问题
队列
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup) {
x = x + 1
wg.Done()
}
func main() {
var w sync.WaitGroup
for i := 0; i < 1000; i++ {
w.Add(1) // //启动一个goroutine add+1,里面有个计数器就down-1
go increment(&w)
}
w.Wait() // 一直阻塞在这里,直到1000个done,计数器减到0
fmt.Println("final value of x", x)
}
-------------------------------------------------------------
final value of x 971
理想情况下应该是1000,但是由于并发安全问题,操作同一个数据,导致最后的结果不准确
所以我们需要给他加锁,定义在全局
使用 Mutex
在前面的程序里,我们创建了 1000 个 Go 协程。如果每个协程对 x
加 1,最终 x
期望的值应该是 1000。在本节,我们会在程序里使用 Mutex,修复竞态条件的问题。
通过mutex
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup, m *sync.Mutex) {
m.Lock()
x = x + 1
m.Unlock()
wg.Done()
}
func main() {
var w sync.WaitGroup
var m sync.Mutex
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w, &m)
}
w.Wait()
fmt.Println("final value of x", x)
}
通过信道实现
//package main
//import (
// "fmt"
// "sync"
//)
//var x = 0
//func increment(wg *sync.WaitGroup, m *sync.Mutex) {
// m.Lock()
// x = x + 1
// m.Unlock()
// wg.Done()
//}
//func main() {
// var w sync.WaitGroup
// var m sync.Mutex
// for i := 0; i < 1000; i++ {
// w.Add(1)
// go increment(&w, &m)
// }
// w.Wait()
// fmt.Println("final value of x", x)
//}
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup, ch chan bool) {
ch <- true // 一开始放一个true
x = x + 1
<- ch // 执行完了把true取出来,如果没执行由于缓冲为1 就一直阻塞在这里
wg.Done()
}
func main() {
var w sync.WaitGroup // 等待协程完成
ch := make(chan bool, 1) // 定义了一个有缓冲大小唯一的信道
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w, ch)
}
w.Wait()
fmt.Println("final value of x", x)
}
利用的是缓冲信道放满了就会阻塞
Mutex vs 信道
共享内存更倾向于Mutex
传递数据倾向于信道,不涉及到数据操作,只涉及到通信