第七章:并发编程
目录一、并发与并行
并发:同一时间段内,多个任务在执行(单个cpu,执行多个任务)。
并行:同一时刻,多个任务在执行(多个cpu的支持)。
二、Go协程(Goroutine)
1 Go协程介绍
单线程下实现并发,也就是协程。其实是多个线程下的多个协程。
- 相比线程而言,go协程的成本极低。堆栈大小只有若干kb(2kb),并且可以根据需求进行增减。而线程是必须指定堆栈大小的,其堆栈是固定不变的。
- Go协程会复用数量更少的OS线程,即便程序可能有数以千计的Go协程,也可能只有一个线程。
- Go协程使用信道(channel)来进行通讯。信道用于防止多个协程访问共享内存时发生竞态条件(Race Condition)。信道可以看作是Go协程之间通信的管道。
goroutine协程 ----> 2kb大小
线程 ----> 几M
go推崇用信道通信,而不推崇用共享变量通信,因为使用共享变量通讯,会涉及到锁,有锁就会有死锁。
2 启动Go协程
go的主线程不会等待Goroutine执行完毕,和python不同。go没有python中的join、守护线程之类的方式。
func test(){
fmt.Println("go go go")
}
func main(){
fmt.Println("主线程开始执行...")
go test()
fmt.Println("主线程结束执行...")
}
// 打印结果
主线程开始执行... // go的主线程不会等待Goroutine执行完毕,和python不同
主线程结束执行...
// 睡一会
import (
"fmt"
"time"
)
func test(){
fmt.Println("go go go")
}
func main(){
fmt.Println("主线程开始执行...")
go test()
time.Sleep(1 * time.Second) // 暂停几秒
fmt.Println("主线程结束执行...")
}
// 打印结果
主线程开始执行...
go go go
主线程结束执行...
go语言中使用匿名函数开协程。go 关键字开启goroutine,一个goroutine只占2kb。
func main(){
fmt.Println("主线程开始执行...")
for i := 0; i < 10; i++{
// 匿名函数
go func() {
fmt.Println("go go go")
}()
}
fmt.Println("主线程结束执行...")
}
// 打印结果【循环了10次,但只打印了5次。。。】
主线程开始执行...
go go go
go go go
主线程结束执行...
go go go
go go go
go go go
3 GMP调度模型
- G:Goroutine;
- M:当成是操作系统真正的线程,实际上是用户线程;
- P:Processor,现在版本默认时cpu核数;
python中,开的线程开出用户线程,用户线程跟操作系统是1:1的对应关系。
某些语言,用户线程和操作系统是n:1的关系。
go语言,用户线程和操作系统是n:m的关系。
简单参考下两图:
// 演示
func main() {
//设置P的大小,认为是cpu核数即可
runtime.GOMAXPROCS(1)
fmt.Println("主线程开始执行")
go func() {
for { // 死循环
fmt.Println("我是死循环...")
}
}()
time.Sleep(10*time.Second)
fmt.Println("主线程结束执行")
}
三、信道(Channel)
1 信道使用
不同Goroutine之间通讯,通过channel实现。
func main(){
// 1 定义信道,不同协程之间通讯
var c chan int
// 2 信道的零值(引用类型,空值为nil,当做参数传递时,不需要取地址,改的就是原来的,需要初始化再使用)
fmt.Println(c) // <nil>
// 3 信道初始化
c = make(chan int) // 后面的数字暂时先不关注
// 4 信道的存值和取值
c <- 1 // 信道存值
// c = 12 // 报错,类型不匹配。chan int不等于int
a := <-c // 信道取值,取出来是一个int
// 5 信道默认不管放值还是取值,都是阻塞的。
// 操作
// 由于c是引用类型,所以直接传递,就会修改原值
go test(c)
a := <-c // 阻塞【不但实现了两条协程之间通信,还实现了等待协程执行结束】
}
func test(a chan int){
fmt.Println("go go go")
time.Sleep(1 * time.Second)
// 往信道中放一个值
a <- 100 // 阻塞
}
总结:信道的存值和取值默认是阻塞的。这个就相当于是pv操作一样。
信道小例子:
package main
// 程序有一个数中,每一位的平方和与立方和,然后把平方和与立方和相加并打印出来
import (
"fmt"
"time"
)
func calcSquares(number int, squareop chan int) {
sum := 0 //总和
for number != 0 {
digit := number % 10 //589对10取余数,9 8 5
sum += digit * digit //sum=9*9 8*8 5*5
number /= 10 //num=58 5 0
}
squareop <- sum // 程序阻塞
}
func calcCubes(number int, cubeop chan int) {
sum := 0
for number != 0 {
digit := number % 10
sum += digit * digit * digit
number /= 10
}
cubeop <- sum // 程序阻塞
}
func main() {
startTime := time.Now().Unix()
number := 589
sqrch := make(chan int)
cubech := make(chan int)
go calcSquares(number, sqrch) // 开设两个goroutine,时间减少
go calcCubes(number, cubech)
squares, cubes := <-sqrch, <-cubech // 程序阻塞
fmt.Println("Final output", squares + cubes)
endTime := time.Now().Unix()
fmt.Println("程序执行:", endTime - startTime, "秒")
}
// 执行结果
Final output 1536
程序执行: 0 秒 // 真的强
多线程应用场景:原来线性执行的东西,让它们并发执行起来。
2 死锁现象
信道的死锁现象,默认都是阻塞的,一旦有一个人放没有人取,或者有人取没有人放,就会出现死锁现象。
// 死锁演示
func main(){
var c chan int = make(chan int)
// 放了没有人取
// c <- 1
// 取没有人放
<-c
}
// 打印提示死锁
3 单向信道【了解】
简单来说,就是控制在一个goroutine,只负责读,或者只负责写。
func sendData(sendch chan<- int) { // 定义一个
sendch <- 10 // 写数据
}
func main() {
//sendch := make(chan<- int) //定义了一个只写信道
sendch := make(chan int) //定义了一个可读可写信道
go sendData(sendch) //传到函数中转成只写信道,在goroutine中,只负责写,不能往外读,主协程读
fmt.Println(<-sendch) //只写信道一旦读,就有问题
}
4 关闭信道
关闭信道之后,不能在进行读写。
func main(){
sendch := make(chan int)
close(sendch) // 关闭信道
}
5 循环信道
package main
import "fmt"
func producer(chnl chan int) {
for i := 0; i < 10; i++ {
chnl <- i
}
close(chnl)
}
func main() {
ch := make(chan int)
go producer(ch)
for v := range ch {
fmt.Println("Received ",v)
}
}
// 打印结果
Received 0
Received 1
Received 2
Received 3
Received 4
Received 5
Received 6
Received 7
Received 8
Received 9
四、缓冲信道
1 缓冲信道
信道默认是阻塞的,缓冲信道只有放满了或者取不出去了,才会阻塞。【可以定义一个缓冲长度】。无缓冲信道,数字其实是0。【其实,我个人感觉数字默认是1,建议书写的时候用缓冲信道,并指定大小即可】【这里其实就是操作系统中pv操作】
// 缓冲信道
var c chan int =make(chan int,6) //无缓冲信道数字是0
c<-1
c<-2
c<-3
c<-4
c<-5
c<-6
//c<-7 //死锁
<-c
<-c
<-c
<-c
<-c
<-c
死锁现象,只会出现在一个goroutine中,如果是多个goroutine,会一直进行阻塞,不会出现死锁现象。
信道的长度和容量:
len(c) // 目前放了多少
cap(c) // 最多可以放多少
2 WaitGroup
WaitGroup等待所有goroutine执行完成。之前使用缓冲信道来实现。
func process1(i int,wg *sync.WaitGroup) {
fmt.Println("started Goroutine ", i)
time.Sleep(2 * time.Second)
fmt.Printf("Goroutine %d ended\n", i)
//一旦有一个完成,减一
wg.Done()
}
func main() {
var wg sync.WaitGroup //没有初始化,值类型,当做参数传递,需要取地址
//fmt.Println(wg)
for i:=0;i<10;i++ {
wg.Add(1) //启动一个goroutine,add加1
go process1(i,&wg)
}
wg.Wait() // 一直阻塞在这,直到调用了10个done,计数器减到零
}
// 执行结果
started Goroutine 0
started Goroutine 3
started Goroutine 1
started Goroutine 9
started Goroutine 4
started Goroutine 5
started Goroutine 6
started Goroutine 7
started Goroutine 8
started Goroutine 2
Goroutine 8 ended
Goroutine 7 ended
Goroutine 6 ended
Goroutine 5 ended
Goroutine 4 ended
Goroutine 3 ended
Goroutine 0 ended
Goroutine 1 ended
Goroutine 9 ended
Goroutine 2 ended
补充:可以使用缓冲信道,来实现上述效果。
五、select
select用于在多发送/接受信道操作中进行选择,select语句会一直阻塞,直到发送/接受操作准备就绪。如果有多个信道操作准备完毕,select会随机选取其中之一执行。该语法与switch类似,所有不同的是,这里的每个case语句都是信道操作。
// 方式一: 信道中谁先回来,就执行谁,其他的丢弃【可以做爬虫使用】,简单来说,就是挑最快的去执行即可。【找最快的线路】
package main
import (
"fmt"
"time"
)
func server1(ch chan string) {
time.Sleep(3 * 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)
// select
select {
case s1 := <-output1:
fmt.Println(s1)
case s2 := <-output2:
fmt.Println(s2)
}
}
// 执行结果
from server2
// 方式二: 不是一直卡住,而是如果卡住,执行其他的事
func process(ch chan string) {
time.Sleep(10500 * time.Millisecond)
ch <- "process successful"
}
func main() {
ch := make(chan string)
go process(ch)
for { // 死循环
time.Sleep(1000 * time.Millisecond)
select {
case v := <-ch:
fmt.Println("received value: ", v)
return
default: // 如果不写default,会出现死锁【只有一个goroutine】
// 可以干其他事,模拟非阻塞式io【逻辑自写】
fmt.Println("no value received")
}
}
}
六、mutex
使用锁的场景:多个goroutine需要访问同一些资源,也就是临界区的概念。由此出现了并发安全的概念,并发安全,需要加锁处理。【修改共享资源的代码叫做临界区。】
如果在任意时刻只允许一个 Go 协程访问临界区,那么就可以避免竞态条件。而使用 Mutex 可以达到这个目的。【python中通过队列来实现通讯】
mutex.Lock() // 加锁
x = x + 1
mutex.Unlock() // 解锁
var x = 0 //全局,各个goroutine都可以拿到并且操作
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 // 是个值类型,函数传递需要传地址
fmt.Println(m)
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w,&m)
}
w.Wait() // 等待所有goroutine执行完毕
fmt.Println("final value of x", x)
}
// >>>>>>>>>>>>>>>>>>>通过缓冲信道来实现>>>>>>>>>>>>>>>>>>>>>>
var x = 0
func increment(wg *sync.WaitGroup, ch chan bool) {
ch <- true // 缓冲信道放满了,就会阻塞
x = x + 1
<- ch
wg.Done()
}
func main() {
var w sync.WaitGroup
ch := make(chan bool, 1) //定义了一个有缓存大小为1的信道
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w, ch)
}
w.Wait()
fmt.Println("final value of x", x)
}
不同goroutine之间传递数据:①共享变量 ② 信道
- 如果是修改共享变量,建议加锁;
- 如果是协程之间通讯,建议用信道;
七、异常处理
1 defer:延迟执行,并且程序出现严重错误也会执行
// 注册一下,并不执行,等main函数执行完了以后,从下往上执行defer定义的东西
func main() {
defer fmt.Println("我是最后执行的")
defer fmt.Println("我是倒数第二个执行")
fmt.Println("我先执行")
}
// 输出结果
我先执行
我是倒数第二个执行
我是最后执行的
案例使用:打开文件等
// 伪代码
f := open()
defer f.close() // 不管如何,都会执行这里
操作文件
// f.close() 可以不用写
2 panic:主动抛出异常。【python中的raise】
panic("我出错了") // python中的raise
// 举例子
func f1(){
fmt.Println("f1 f1")
}
func f2(){
fmt.Println("f2 f2")
panic("主动抛出错误")
}
func f3(){
fmt.Println("f3 f3")
}
func main() {
f1()
f2() // 抛出错误
f3() // 不会被执行掉
}
// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
// 所以我们需要对f2跑出来的错误进行异常捕获,让程序继续运行
func f1(){
fmt.Println("f1 f1")
}
func f2(){
defer func(){ // 这个匿名函数永远会执行
recover() // 恢复错误,继续执行
}()
fmt.Println("f2 f2")
panic("主动抛出错误")
}
func f3(){
fmt.Println("f3 f3")
}
func main() {
f1()
f2() // 抛出错误
f3()
}
3 recover:恢复程序,继续执行。【其实就是python中except中的东西】。如果没有错误,执行recover会返回nil,如果有错误,执行recover返回错误信息。
// 固定写法
func f1(){
fmt.Println("f1 f1")
}
func f2(){
defer func(){ // 这个匿名函数永远会执行
if error:=recover(); error!=nil{
// 表示出错了,打印一下错误信息,程序恢复了,继续执行
fmt.Println(error)
}
fmt.Println("我永远会执行,无论是否出错...")
}()
fmt.Println("f2 f2")
panic("主动抛出错误")
}
func f3(){
fmt.Println("f3 f3")
}
func main() {
f1()
f2() // 抛出错误
f3() // 不会被执行掉
}
# python中的写法
try:
可能出现的代码
expect Exception as e:
print(e)
finally:
无论是否出错都会执行
// go中的写法【如果觉得谁出错,就写在谁的上面即可】
defer func(){ // 这个匿名函数永远会执行
if error:=recover(); error!=nil{
// 表示出错了,打印一下错误信息,程序恢复了,继续执行【python中except中的东西】
fmt.Println(error)
}
// 此处相当于python中的finally,无论是否出错都会执行
}()
go的错误处理:
func main() {
f, err := os.Open("/test.txt")
if err != nil {
fmt.Println(err)
return // 退出main主函数
}
fmt.Println(f.Name(), "opened successfully")
}
标签:Println,第七章,fmt,编程,并发,信道,func,go,执行
From: https://www.cnblogs.com/yangyi215/p/17284387.html