第六篇:并发编程

第六篇:并发编程

在这里插入图片描述

##6.1 go并发设计模式

”敌人的高并发要来了,python抗住“

“老板,扛不住了,宕机了!”

“你们卷铺盖回家吧!go程序员在哪里!!!”

go语言因为Goroutine才与众不同。

goroutine是用来实现go程序并发的,是go最重要的一部分。市面上很多人会把goroutine叫做go的协程,我们这里不叫它协程,依旧用goroutine去描述它。因为用协程或者线程去描述goroutine是不合适的,goroutine是线程和协程的一种复合使用。在我们下面讲解go并发设计模式的时候,你们就会发现我为什么这么说。

在讲go并发设计模式前,我们先来看两个概念,内核级线程、用户级线程

内核级线程:

内核级线程是操作系统执行程序的最小单元。就是说啊,cpu会在内核级线程之间切换并执行,如果有多个cpu,那就是多个cpu在这些线程之间切换并执行。我cpu去执行的就是内核级线程。

用户级线程:

用户级线程说的简单直白一点,就是用户自己规定的一个代码块。

在这里插入图片描述

理解完这两个概念之后,我们先来看看协程、多线程的设计模式,最后比较着来学习go并发设计模式

1、协程设计模式

在这里插入图片描述
这个模式的优点在于开销小,切换效率快。但有也有两个很致命的问题,一、就是不能实现并行,二、就是一旦阻塞整个线程都会被阻塞。python里的gevent就是这个模式

2、多线程设计模式

在这里插入图片描述

这个模型优点在于简单,和并行。缺点在于:一、切换是由操作系统调度的,效率比较低,二、操作系统开一个线程至少需要2M空间,资源成本很大

每个用户级线程的运行状态,运行结果都是存在内核级线程上的。

重点来了,go的并发设计模式结合了协程和线程,并对其弊端进行了优化,以至于很牛逼。

那具体牛逼在哪里,我们来研究研究goroutine是底层怎么玩的:

3、go并发设计模式

在这里插入图片描述

字母解释:

1、G:G表示一个goroutine

2、P:P是介于内核级线程M和用户级线程G之间的调度器

3、M:M是内核级线程,真正执行程序的是M

在这里插入图片描述

并发过程解释:

1、每创建一个goroutine,会优先加入到p的local队列里,等待被执行,如果local队列都满了,那么就会加入到global全局队列里。

2、然后P会和M绑定,M去循环着执行local队列里的G。

3、一旦G出现了用户态阻塞,G就会被拿出,放进等待队列里,M继续执行下一个G,这个是不是就类似协程了,阻塞我就跳过执行下一个。G阻塞完成后,会被继续加入到loacl队列里,等待下次被执行

4、一旦G出现了系统调用阻塞(如磁盘读写),整条M也会被阻塞,这时候G也同样会被拿出,放进等待队列。那么P就会和M解绑,然后去找其它空闲的M绑定,如果没有空闲的M,就会新建一个M然后绑定,继续执行下一个G。这是不是就解决了协程里一旦阻塞整条线程就会被阻塞的问题啊?

5、整个go并发运行的时候,会有多个P/M同时运行,这就是多线程。

6、如果,我某一个P里的G运行完了,那么它会去global队列里拿G,如果global队列里没有,它就会随机的去其它p里拿出一半的G来运行

细节:

1、正常一个系统线程占8M空间,会保存着程序栈,记录着运行信息、保存着运行结果等,但实际上我们用不到这么大空间,对于一般程序,8M很大,那对于深度递归这样的程序,8M不算大。

goroutine不让内核级线程去记录这些信息了,内核级线程只管运行其它啥都不管。这些信息由用户级线程就是G去保存,初始空间只有2KB,然后按需扩展,实际需要多大给多大。这就为开启更多的goroutine提供了资源支持。

有了如此强大的go并发模型,我们用起来就很简单了。
在这里插入图片描述

6.2 创建go并发

1、创建goroutine:

格式如下:

go 函数名(参数)

或者用匿名函数创建:

go func(形参){ 函数体 }(实参)

func test1(){
    fmt.Println("我是test1")
}
func test2(){
    fmt.Println("我是test2")
}

func main(){
    for i:=0;i<10;i++{     //创建20个goroutine,10个test1和10个test2
        go test1()
        go test2()
    }
    
    var input string       //用来接收用户输入,阻塞主线程
    fmt.Scanln(&input)
    
}

这时候我们就创建了20个并发任务

2、GOMAXPROCS()

我们回过去看上面的go并发模型图,里面有调度器P,我P的个数可以设置的。

我设置1个P是不是意味着我最后只有1个cpu来运行我的程序。我设置4个P,是不是意味着我用时可以有4个cpu来执行我的程序。

但P的数量是越多越好吗?显然不是啊,我就4个cpu,开5个p,是不是总归有一个P不会被执行啊?显然没意义。这个go语言呐也给你做了封装,p的最大数量,和你cpu核数相同,你设置为10,源码里也会给你改成4。

我们来看看如何设置:

//很简单,就一句话
runtime.GOMAXPROCS(P的数量)
//我们可以通过NumCPU()来获取当前计算机的cpu核数
runtime.GOMAXPROCS(runtime.NumCPU())     //go默认用的最大cpu数

在这里插入图片描述

6.3 通道 channel

我们正常多线程,比如Python,是怎么交换数据的?是不是用的共享内存的方式啊?

我每个线程都可以访问到同一个数据,并对它进行修改。为了保证数据修改的正确性,我们是不是用互斥锁来解决这个问题的?问题是结局了,但是!造成了性能的下降!!

goroutine另辟蹊径,它不用共享内存的方式来共享数据,它用通道的方式来共享数据。

什么是通道:

通道有2头,一头放数据,一头拿数据,先进先出。

怎么用通道:

6.3.1 通道的定义

在这里插入图片描述

我们使用make关键字来创建一个通道

mychan := make(chan 类型)     //chan:表示通道  类型:用来指定通道里数据的类型

6.3.2 收发数据

我们用<- 来收发数据,很简单也很形象

发数据:

mychan <- 数据

收数据:

数据 <- mychan

实例:

func main(){
	mychan := make(chan string)
    
    //循环着放数据
	go func() {
        for {
            var msg string
            fmt.Scanln(&msg)
            mychan <- msg
        }
	}()
    
    //循环着取数据
    for{
        msg := <- mychan
    	fmt.Println(msg)
        if msg == "exit"{     //如果输入“exit”我就退出程序
            return
        }
    }
}

###6.3.3 非缓冲管道

如上这种管道我并没有指定它的容量,意味着它容量为0,容量为0,意味着不能存数据!

我擦,不能存数据是什么鬼?那我上面代码里mychan <- msg这TM是在干嘛?

我负责任的告诉你,它没有存,只是阻塞着,等在那里!等谁啊?当然是等接受者。一旦有接受者出现<- mychan,那么这个管道就被打通,发送者发数据,接受者接数据,数据不在管道停留。

这样的管道叫做非缓冲管道,它有两种情况会发生阻塞:

1、发送者没有匹配到接受者

2、接收者没有匹配到发送者

在这里插入图片描述

6.3.4 缓冲管道

非缓冲管道容量为0,缓冲管道显然就是容量不为0的,意味着就是可以保存数据的。

我们用make的第二个参数去设置容量

mychan := make(chan int,3)   //容量为3的管道

容量为3意味着什么呀?

mychan <- 1     //不会阻塞
mychan <- 2     //不会阻塞
mychan <- 3     //不会阻塞
mychan <- 4     //嘿呀,塞住了,需要接受者,不然我一直塞着

6.3.5 单向管道

单向通道很好理解,就是要么只能读,要么只能写的通道。

我是不是可以像定义双向通道一样去定义单向通道啊?

mychanin := make(chan<- int)       //只写通道
mychanout := make(<-chan int)      //只读通道

但是!!!你这么玩?疯了吗?这个通道有啥意义啊?

单向通道的意义在于把双向通道拆成单向通道,用来限制某个函数的操作

mychan := make(chan int,2)
mychan2 := make(chan int,2)

//类型转换,双向通道变成单向(理论上,但这么玩有坑)
inout_to_in := chan<- int(mychan)     //单向写 ---这货正常
inout_to_out := <-chan int(mychan)    //单向读 ---这货就是死狗,坑就在这里
inout_to_out := (<-chan int)(mychan)  //得加括号,不然程序会把<-和chan分开

//这么玩最靠谱,用赋值的方式进行通道转换
var inout_to_in chan<- int = mychan
var inout_to_out <-chan int = mychan

fmt.Println(inout_to_in == mychan)      //true
fmt.Println(inout_to_out == mychan)     //true    证明他们操作的都是同一个通道

fmt.Println(inout_to_out == mychan2)    //false

在这里插入图片描述

6.3.6 关闭通道

纳尼?为什么要关闭通道?

通道是一直存在的,不会被回收。某些情况,我不需要再使用它了,就把它关掉,等待垃圾回收。释放内存资源。

请看玩法:

func main(){
	mychan := make(chan int)
    
	go func() {
        defer close(mychan)                   //关了它
        
		for i:=0;i<3;i++{
			mychan<-i
		}
	}()

	fmt.Println(<-mychan)
	fmt.Println(<-mychan)
	fmt.Println(<-mychan)
}

注意点:

1、重复关闭,会引发panic恐慌

func main(){
	mychan := make(chan int)
    
	go func() {
        defer close(mychan)                   //嘣!  panic: close of closed channel
        defer close(mychan)                   //关了它
        
		for i:=0;i<3;i++{
			mychan<-i
		}
	}()

	fmt.Println(<-mychan)
	fmt.Println(<-mychan)
	fmt.Println(<-mychan)
}

2、向已经关闭的通道发送数据,会引发panic

func main(){
	mychan := make(chan int,3)
	
	close(mychan) 
	mychan<-1                                //嘣!  panic: send on closed channel

}

3、接收已关闭的通道,返回缓存值或者0值

func main(){
	mychan := make(chan int,2)
	mychan<-1
	mychan<-2
	close(mychan)
	fmt.Println(<-mychan)                     // 1
	fmt.Println(<-mychan)                     // 2
	fmt.Println(<-mychan)                     // 0    值取完了,所以返回int的0值
}

其实接收通道数据时,会有2个值:

value,isopen := <-mychan
fmt.Println(value)                            //通道里取的值
fmt.Println(isopen)                           //true-表示通道打开,false-表示通道关闭

重要的事来的!

接收已关闭的通道不会报错,但是!!发送数据给已关闭的通道会报错,所以!我们关闭通道最好是放在数据发送端!!!!!!

在这里插入图片描述

6.3.7 通道的多路复用

什么是通道的多路复用

很好理解嘛,一堆通道一起玩嘛

为什么要有多路复用

多路复用是一堆通道一起玩,那我们来看,通道一个一个的玩儿会有什么问题

func main(){
    mychan1 := make(chan int,2)
    mychan2 := make(chan int,2)
    mychan3 := make(chan int,2)
    
    go func(){
        for {
            a := <-mychan1              
            fmt.Println(a)
            //问题在这里在这里!!看过来!
            //如果不接收到mychan1的数据,会一直阻塞着,以至于后面的通道也没法正常接收数据

            b := <-mychan2
            fmt.Println(b)

            c := <-mychan3
            fmt.Println(c)
        }
    }()
    
    mychan3 <- 3
    mychan3 <- 3
    mychan2 <- 2
    mychan2 <- 2
    time.Sleep(5*time.Second)              //睡5秒,我再往通道1放值
    mychan1 <- 1
    mychan1 <- 1
    
    var input string                       //阻塞住主程序
	fmt.Scanln(&input)
     
}

怎么用多路复用:

格式如下:

select{
    case 通道操作1:
    	代码..
    case 通道操作2:
        代码..
    ...
}

修改上面代码:

func main(){
    mychan1 := make(chan int,2)
    mychan2 := make(chan int,2)
    mychan3 := make(chan int,2)
    
    go func(){
        for {
            select{
                //这时候不会因为mychan1的阻塞而阻塞mychan2和mychan3
                //select类似switch
                case a := <-mychan1:
                    fmt.Println(a)
                case b := <-mychan2:
                    fmt.Println(b)
                case c := <-mychan3:
                    fmt.Println(c)
        	}
        }      
    }()
    
    mychan3 <- 3
    mychan3 <- 3
    mychan2 <- 2
    mychan2 <- 2
    time.Sleep(5*time.Second)              //睡5秒,我再往通道1放值
    mychan1 <- 1
    mychan1 <- 1
    
    var input string                       //阻塞住主程序
	fmt.Scanln(&input)
     
}

6.4 死锁错误

在这里插入图片描述

在go里面有一种现象叫做死锁!这是个很严重的问题,会造成程序直接崩溃。

死锁错误的原因:

所有的goroutine都处于阻塞状态,就会造成死锁错误!

所有的goroutine都阻塞住了,程序压根儿没法运行了,不崩溃才怪!

案例1:

func main(){
	mychan1 := make(chan int)
	mychan1<-1                        //阻塞
    go func() {
		fmt.Println(<-mychan1)
	}()
}

这里有2个goroutine,住程序阻塞的时候第二个goroutine还没来得及开启,程序无法继续运行,崩溃!

案例2:

func main(){
	mychan1 := make(chan int,3)
	go func() {
		for i:=0;i<3;i++{
			mychan1<-i
		}
	}()

	for {             
        //前3次正常运行,第四次阻塞,此时go开启的goroutine已经结束,当前只有1个主程序在运行,并阻塞,所			以死锁发生
		fmt.Println(<-mychan1)
	}
}

在编写程序的时候要时刻提防死锁的发生!否则,轻则功能无法实现,重则,程序崩溃,你被开除!

在这里插入图片描述

6.5 Lazy生成器

我有一堆数据需要处理。

那么有两个途径:1、把所有的数据都读到内存,一起处理;2、读一个处理一个

这两种方法相比较而言:

第一种:简单,但内存消耗大

第二种:略复杂,内存消耗少

举个例子,我有1000个int64的数字,一共8000B,约为7.8KB。问题来了,我现在需要求和。

第一种方法,我至少需要7.8KB的空间来存数字。

第二种方法,我只需要大约8B的空间

go里实现生成器实际上需要一个2kb的goroutine,所以方法二需要大约2KB空间

结论:如果数据量小于2KB,适合一次性读取;如果数据量大于2KB,适合生成器

生成器怎么玩:

在这里插入图片描述

func num1to1000 () chan int {
    mychan := make(chan int,1)
    
    go func(){
        for i:=1;i<1001;i++{
            mychan <- i
    	}
    }()

    return mychan
}


func main(){
    mychan := num1to1000()
    fmt.Println(<- mychan)       //  1
    fmt.Println(<- mychan)       //  2
    fmt.Println(<- mychan)       //  3
}

一个简单的生成器就这么完成了!

但是!但是!!这么写有问题!!

1、生成器没有把数据全部生产完,这个goroutine会一直存在

2、生成器没有把数据全部生产完,关闭通道会panic

问题的关键点:

有个机制可以关闭通道,并终止goroutine
在这里插入图片描述

实现方法多种多样,不要拘泥于细节,充分结合自己所学知识:

我从painc入手,它panic了,我就recover,并退出goroutine

func num1to1000 () chan int {
    mychan := make(chan int,1)
    
    go func(){
    	defer func(){
            recover()
            runtime.Goexit()
        }()
        for i:=1;i<1001;i++{
            mychan <- i
    	}
    }()

    return mychan
}


func main(){
    mychan := num1to1000()
    fmt.Println(<- mychan)       //  1
    fmt.Println(<- mychan)       //  2
    fmt.Println(<- mychan)       //  3
    close(mychan)                //我关掉通道,goroutine会报错,然后被我关掉
}

感觉很棒有没有?!!!

其它方法都蛮繁琐的,我就不介绍了!交给你们自己去想了

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值