第六篇:并发编程_小猿取经-程序员宅基地

技术标签: go系列教程  golang  

第六篇:并发编程

在这里插入图片描述

##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会报错,然后被我关掉
}

感觉很棒有没有?!!!

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

在这里插入图片描述

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_46032351/article/details/103702196

智能推荐

java怎么打开_java开不了怎么办?java怎么打开?_Hasaki酱的博客-程序员宅基地

我们在打开java软件的时候,总是提示打不开,这可急坏了小伙伴,软件打不开,就无法写程序了,那么接下来,我们就来给大家讲解一下java开不了的解决方法。1、先保证正确安装了JAVA环境。2、再打开“控制面板”中的“Java”组件设置程序。3、切换到“JAVA控制面板”中的“安全”选项卡,并将“安全级别”设置到最低级。4、然后再单击对话框右下角的“编辑站点列表”按钮。5、在打开的“例外站点列表”对话...

oracle分组后合并其中一个字段 (2)_weixin_30835923的博客-程序员宅基地

1、SELECT wmsys.wm_concat(t.org) orgs, t.area_nameFROM (SELECT concat(concat(b.abbreviation, '-'), b.org_name) org,a.area_name area_nameFROM t_organization bleft join t_area_store aon a.store_code =...

ToolTip功能_weixin_30268071的博客-程序员宅基地

直接看效果图:使用普通的HTML属性alt只能简单地提示一些文本,如上这些复杂的功能就需要用JavaScript实现。详细代码如下:Code&lt;!DOCTYPEhtmlPUBLIC"-//W3C//DTDXHTML1.0Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"...

linux efi不要boot目录,在单引导硬件上将哪些命令将Ubuntu BIOS安装转换为EFI/UEFI而不使用boot-repair?..._weixin_28909289的博客-程序员宅基地

问题描述意外BIOS模式安装我有英特尔64位硬件和UEFI设置实用程序。 Ubuntu 14.04.1 LTS作为唯一的操作系统安装在唯一的驱动器上。无意中Ubuntu安装在BIOS /CSM /legacy模式下。转换为UEFI在稍后了解UEFI时,目标是通过EFI /UEFI将此现有Ubuntu安装更改为(更快)启动。我仍然想要某种 – 显示2秒 – 启动菜单,允许我进入UEFI设置实用程序。...

Python基础(二)--- IDEA中整合Python和MySQL,使用Python进行SQL操作_xcvbxv01的博客-程序员宅基地_idea pymysql

一、Python操作MySQL----------------------------------------------------- 1.安装MySQL 2.安装mysql的python模块 a.下载并安装PyMySQL-master.zip https://github.com/PyMySQL/PyMySQL 3.解压,在解压目...

oracle启动 例程,Oracle 启动的三个步骤_weixin_28890941的博客-程序员宅基地

Oracle 数据库的启动需要三个步骤,当我们直接输入Startup进行启动时,实际上数据库已经隐含的将三个步骤一起执行。而实际上,Oracle的启动过程包括了启动例程,装载数据库,打开数据库。每完成一个步骤,数据库就进入了一个特定的环境,以保证数据库进入了某种一致性的状态。本文即是对这三个步骤中需要打开的文件,以及各个步骤执行后的数据库状态进行简单的实验。我们在每一个步骤执行时,通过alert_...

随便推点

AlexNet,ResNet34,SqueezeNet模型的实现_aift的专栏-程序员宅基地

文章目录概要__init__.pybasic_module.py具体模型定义alexnet.pyresnet34.pysqueezenet.pyReferences概要如果对这几种基础模型不太了解,请先参考博客。首先来看程序文件的组织结构:├── checkpoints/├── data/│ ├── __init__.py│ ├── dataset.py│ └── ge...

css3 滑动验证,Vue 实现拖动滑块验证功能(只有css+js没有后台验证步骤)_weixin_42322219的博客-程序员宅基地

vue验证滑块功能,在生活中很多地方都可以见到,那么使用起来非常方便,基于vue如何实现滑块验证呢?下面通过代码给大家讲解。效果图如下所示:拖动前拖动后代码引用的css与js都是线上的将代码全部复制到一个html中可以直接打开,极其简单。来分析一下代码底色div上放了一个变色div再放一个提示字的div最后加一个滑块div给滑块div绑定鼠标移动事件.drag {border-radius:30p...

Mysql 插入记录时检查记录是否已经存在,存在则更新,不存在则插入记录SQL_dfafa30201020的博客-程序员宅基地

Mysql 插入记录时检查记录是否已经存在,存在则更新,不存在则插入记录SQL我们在开发数据库相关的逻辑过程中, 经常检查表中是否已经存在这样的一条记录, 如果存在则更新或者不做操作, 如果没有存在记录,则需要插入一条新的记录。这样的逻辑固然可以通过两条sql语句完成。SELECT COUNT(*) FROM x...

Shell脚本 awk实现查看IP连接数_沐沐.-程序员宅基地_shell 查看连接数

一.简介处理文本,是awk的强项了。 无论性能已经速度都是让人惊叹!二.使用适用:centos6、7+语言:英文注意:无cat awk_ip.shawk 'BEGIN{ while("netstat -an"|getline){ if( $5 ~ /[1-255]/) { split($5,t1,":"); tarr[t1[1]]++; } } for(k in t

运算器和控制器在计算机的作用,运算器和控制器合称为什么_weixin_28871821的博客-程序员宅基地

在计算机中,运算器和控制器合称为“中央处理器”。中央处理器(CPU)是计算机中负责读取指令,对指令译码并执行指令的核心部件;其主要包括两个部分,即控制器、运算器,其中还包括高速缓冲存储器及实现它们之间联系的数据、控制的总线。在计算机中,运算器和控制器合称为“中央处理器”。中央处理器(CPU,Central Processing Unit)作为计算机系统的运算和控制核心,是信息处理、程序运行的最终执...

javascript 学习随笔3_weixin_30325971的博客-程序员宅基地

&lt;html&gt;&lt;head&gt;&lt;script type="text/javascript"&gt;function startTime(){var today=new Date()//得到当前时间var h=today.getHours()var m=today.getMinutes()var s=today.getSeconds()//...

推荐文章

热门文章

相关标签