ab 性能测试,goroutine协程,Goroutine协程之间的数据传输,golang单例模式,PTZ,搞笑代码注释,佛祖保佑 永无BUG,Ubuntu20.04开机启动指定程序vscode,开机自动登录

ab 性能测试

ab -c 1000 -n 5000 -k http://192.168.1.113/v1/ptz/getPTZPosition 

# -c 并发一次发出多个请求的次数
# -n 请求要执行的请求数
# -k 使用HTTP KeepAlive功能
# ApachBench bench:工作台 benchmark:基准 benchmarking:v.评估,检测;n.确定基准点,标杆管理

goroutine协程

http://mp.weixin.qq.com/mp/homepage?__biz=Mzg5NjIwNzIxNQ==&hid=6&sn=5f344340cd22eb2148795fb2bfe5caf0&scene=18#wechat_redirect

1.协程是怎样的存在?

https://mp.weixin.qq.com/s?__biz=Mzg5NjIwNzIxNQ==&mid=2247484260&idx=1&sn=30cb86b3ff1e8d8aad56ea193ed05ee8&scene=19#wechat_redirect

线程是进程中的执行体,拥有一个执行入口,以及从进程虚拟地址空间中分配的栈,包括用户栈和内核栈。

操作系统会记录线程控制信息,而线程获得CPU时间片以后才可以执行,CPU这里栈指针、指令指针、栈基等寄存器,都要切换到对应的线程;

如果线程自己又创建几个执行体,给它们各自指定执行入口,申请一些内存分给他们用做执行栈,那么线程就可以按需调度这几个执行体了,为了实现这些执行体的切换,线程也需要记录他们的控制信息,包括标识符id、执行栈位置、执行入口、执行现场等,

线程可以选择一个执行体来执行,此时CPU中指令指针就会指向这个执行体的执行入口栈基栈指针寄存器也会指向线程给它分配的执行栈,要切换执行体时,需要先保存当前执行体的执行现场,然后切换到另一个执行体,通过同样的方式可以恢复到之前的执行体,这样就可以从上次中断的地方继续执行,这些由线程创建的执行体就是所谓的“协程”,因为用户程序不能操作内核空间,所以只能给协程分配用户栈,而操作系统对协程一无所知,协程又被称为“用户态线程”。

协程的思想很早就被提出来了,1963年梅尔文·康威,最初是为了解决编译器实现中的问题,后来出现了很多实现方式,例如windows中的纤程,lua中的coroutine,可无论被赋予什么样的名字,有这怎样的用法,在创建协程时,都要指定执行入口,底层都会分配协程执行栈和控制信息,否则又该如何实现用户态调度?而让出执行权时,也都要保存执行现场,不然如何能够从中断处恢复执行?协程思想的关键在于控制流的“主动让出”和"恢复",每个协程拥有自己的执行栈,可以保存自己的执行现场,所以可以由用户程序,按需创建协程,协程“主动让出”执行权时,会保存执行现场,然后切换到其他协程,协程恢复执行时,会根据之前保存的执行现场,恢复到中断前的状态继续执行,这样就通过协程即轻量又灵活的由用户态进行调度的多任务模型。

即便如此,协程依然风平浪静很多年,直到高并发成为主流趋势,瞬间抵达的海量请求让多进程模型下内存资源捉襟见肘,让多线程模型下内核(态)用户(态)两头忙,却依然疲于应对,协程这种灵活、轻量的用户态调度模型便受到了广泛的关注,而真正让协程大放异彩的是在IO多路复用中的应用,二者的结合助力协程成为炙手可热的高并发解决方案。

总结:

  • 线程是进程中的执行体,拥有一个执行入口,以及从进程虚拟地址空间中分配的栈,包括用户栈和内核栈。
  • 操作系统会记录线程控制信息,而线程获得CPU时间片以后才可以执行CPU这里栈指针、指令指针、栈基等寄存器,都要切换到对应的线程
  • 如果线程创建几个执行体,指定执行入口,申请内存作为执行栈,线程按需调度这几个执行体;这些执行体的切换,线程需要记录它们控制信息,包括标识符id,执行栈入口,执行入口,执行现场等。
  • 线程选择一个执行体来执行,此时CPU中指令指针就会指向这个执行体的执行入口,栈基和栈指针寄存器指向线程给它分配的执行栈;切换执行体时,虚线保存执行提的执行现场,然后切换到另一个执行体,通过同样的方式可以恢复到之前的执行体,也可以从上次中断的地方继续执行。

栈基:堆区和栈区的内存分配都是先从基地址开始分配,并在内存释放后指针再次回到基地址,动态数据区主要分为heap与stack,其中的堆区和栈区分别具有基地址。

https://www.cnblogs.com/miajun/p/5245047.html

寄存器:用来暂存指令、数据和地址,寄存器内的数据可用于执行算术及逻辑运算,存于寄存器的地址,可用来指向内存的某个位置;拥有非常高的读写速度,在寄存器之间的数据传送非常快。

康威定律——这个50年前就被提出的微服务概念,你知多少?

http://www.dockone.io/article/2691

在文章中,Mike Amundesn总结了一些核心观点:

  • 第一定律:企业沟通方式会通过系统设计表达出来
  • 第二定律:再多的时间也没办法让任务完美至极,但总有时间能将它完成
  • 第三定律:线型系统和线型组织架构间有潜在的异质同态特性
  • 第四定律:大系统比小系统更适用于任务分解

在应用项目后期加大人员的投资,会更加拖慢它的速度。——Fred Brooks(1975)

团队中微服务的理念应是Inter-Operate,而不是Integrate ,Inter-Operate是指定义系统边界和接口,并为整个团队提供完整的堆栈,实现完全的自制。如此就能降低系统间的依赖性,减少通信成本。

openresty:通过使用 Lua 扩展 NGINX 的可扩展 Web 平台,快速构造出足以胜任 10K 以上并发连接响应的超高性能 Web 应用系统。

swoole:PHP 协程框架,Swoole 使 PHP 开发人员可以编写高性能高并发的 TCP、UDP、Unix Socket、HTTP、 WebSocket 等服务,让 PHP 不再局限于 Web 领域。Swoole4 协程的成熟将 PHP 带入了前所未有的时期, 为性能的提升提供了独一无二的可能性。Swoole 可以广泛应用于互联网、移动通信、云计算、 网络游戏、物联网(IOT)、车联网、智能家居等领域。使用 PHP + Swoole 可以使企业 IT 研发团队的效率大大提升,更加专注于开发创新产品。

2.协程和IO多路复用

  • 通过操作系统记录的进程控制信息,可以找到打开文件描述符表进程打开的文件、创建的socket等,都会记录到这张表里。
  • socket的所有操作都有操作系统来提供,也就是要通过系统调用来完成,每创建一个socket,就会在打开文件描述符表中,对应增加一条记录,而返回给应用程序的只有一个socket描述符,用于识别不同的socket,而每个TCP socket在创建时,操作系统都会为它分配一个读缓冲区和一个写缓冲区要获得响应数据,就是要从读缓冲区拷贝过来,同样的要通过socket发送数据,也要先把数据拷贝到写缓冲区才行,所以问题出现了,用户需要读缓冲区数据,读缓冲区未必有数据,想发送数据的时候,写缓冲区里也未必有空间

第一种办法(阻塞式IO):乖乖的让出CPU,进入等待队列里等socket就绪后,再次获得时间片就可以继续执行了,这就是“阻塞式IO”。

使用阻塞式IO,要处理一个socket就要占用一个线程,等这个socket处理完才能接手下一个,这在高并发场景下会加剧调度开销

第二种方法(非阻塞式IO):不让出CPU,但是需要频繁的检查socket是否就绪了,这是一种“忙等待” 的方式,很难把握轮询的间隔时间,容易造成空耗CPU,加剧响应延迟

第三种办法(IO多路复用):由操作系统提供支持把需要等待的socket加入到监听集合,这样就可以通过一次系统调用,同时监听多个socket,有socket调用了就可以逐个处理了,即不会因为某个socket而阻塞,也不会陷入“忙等待”之中。

Linux中提供了三种IO多路复用实现方式:

方式一:select,我们可以设置要等待的描述符,也可以设置等待超时时间,如果有准备好的fd或达到指定超时时间,select就会返回

从函数签名来看,它支持监听可读、可写、异常三类事件,因为fd_set是个usigned long型数组,共16个元素,每一位对应一个fd,最多可以监听1024个,这就有点少了,而且每次调用select都要传递所有监听集合,这就需要频繁的从用户态到内核态拷贝数据,除此之外,即便有fd就绪了,也需要遍历整个监听集合,来判断那个fd是可操作的,这些都会影响性能。

方式二:poll,虽然支持的fd数目,等于最多可打开的文件描述符个数,但另外两个问题依然存在。

方式三:epoll,提供3个接口,epoll_create1用于创建epoll,并获取一个句柄,epoll_ctl用于添加或删除fd与对应的事件信息,除了指定fd和要监听的事件类型,还可以传入一个event.data,通常会按需定义一个数据结构,用于处理对应的fd,可以看到每次都只需传入要操作的一个fd,无需传入所有监听集合,而且只需要注册这一次,通过epoll_wait得到的fd集合都是已经就绪的逐个处理即可,无需遍历整个监听集合。通过IO多路复用线程再也不用为等待某一个socket,而阻塞或空耗CPU,并发处理能力因而大幅提升。

但并非没有问题,例如一个socket可读了,但是这回只读到了半条请求,也就是说需要再次等待这个socket可读,在处理下一个socket之前需要记录下这个socket的处理状态,下次一这个socket可读时,也需要恢复上次保存的现场,才好继续处理,也就是说,在IO多路复用中实现业务逻辑时,我们需要随着事件的等待和就绪,而频繁的保存和恢复现场,这并不符合常规的开发习惯,如果业务逻辑简单还好,业务复杂的业务场景就悲剧了。

既然业务处理过程中要等待事件时,需要保存现场并切换到下一个就绪的fd,而事件就绪时,又需要恢复现场继续处理,那岂不是很适合使用协程?在IO多路复用这里,事件循环依然存在,依然要在循环中逐个处理就绪的fd,但处理过程却不是围绕具体业务,而是面向协程调度,如果是用于监听端口的fd就绪了,就建立连接创建一个新的fd,交给一个协程来负责,协程执行入口就指向业务处理函数入口,业务处理时需要等待时就注册IO事件,然后让出(yield),这样执行权就会回到切换到该协程的地方继续执行,如果是其他等待IO事件的fd就绪了,只需恢复关联的协程即可,协程拥有自己的栈,要保存和恢复现场都很容易实现,这样IO多路复用这一层的事件循环,就和具体业务逻辑解耦了,可以把read、write、connect等可能会发生等待的函数包装一下,在其中实现IO事件注册与主动让出,这样在业务逻辑层面就可以使用这些包装函数,按照常规的顺序编程方式,来实现业务逻辑了,这些包装函数,在需要等待时会注册IO事件,然后让出协程,这样我们在实现业务逻辑时,就完全不用关心保存与恢复现场的问题了。

总结

  • 通过操作系统记录的进程控制信息,可以找到打开文件描述符表进程打开的文件、创建的socket等,都会记录到这张表里。
  • socket的所有操作都有操作系统来提供,也就是要通过系统调用来完成,每创建一个socket,就会在打开文件描述符表中,对应增加一条记录,而返回给应用程序的只有一个socket描述符,用于识别不同的socket,而每个TCP socket在创建时,操作系统都会为它分配一个读缓冲区和一个写缓冲区要获得响应数据,就是要从读缓冲区拷贝过来,同样的要通过socket发送数据,也要先把数据拷贝到写缓冲区才行,所以问题出现了,用户需要读缓冲区数据,读缓冲区未必有数据,想发送数据的时候,写缓冲区里也未必有空间
  • 阻塞式IO,让出CPU,加剧调度开销;非阻塞式IO,不让出CPU,忙等待,空耗CPU;
  • IO多路复用,操作系统提供支持把需要等待的socket加入到监听集合,这样就可以通过一次系统调用,同时监听多个socket,有socket调用了就可以逐个处理了;select,poll,epoll是Linux的内核函数;
    • select方式,设置要等待的描述符,也可以设置等待超时时间,如果有准备好的fd或达到指定超时时间,select就会返回;能监听1024个;
    • poll,支持的fd数目等于最多可打开的文件描述符个数;
    • epoll(event poll),epoll_create1用于创建epoll,并获取一个句柄;epoll_ctl用于添加或删除fd与对应的事件信息,传入一个event.data用于处理对应的fd每次都只需传入要操作的一个fd,无需传入所有监听集合,而且只需要注册这一次;通过epoll_wait得到的fd集合都是已经就绪的 逐个处理即可,无需遍历整个监听集合。

协程和IO多路复用的结合,不仅保存了IO多路复用的高并发性能,还解放了业务逻辑的实现。

3.你好 goroutine~

一个helloworld程序,编译后成为一个可执行文件,执行时可执行文件被加载到内存,对于进程虚拟地址空间中的代码段,我们感兴趣的是程序入口,不是我们熟悉的main.main,不同平台为程序入口不同(rt0_amd64_windows, rt0_amd64_linux),在进行一系列检查与初始化等准备工作后,会以runtime.main为执行入口创建main goroutinemain goroutine执行起来以后才会调用我们编写的main.main

数据段,这里有几个重要的全局变量不得不提,我们知道go语言中协程对应的数据结构是runtime.g,工作线程对应的是数据结构runtime.m,而全局变量g0,就是主协程对应的g,与其他协程有所不同,它的协程栈实际上是在主协程栈上分配的,全局变量m0就是主线程对应的m,g0持有m0的指针,m0页记录着g0的指针,而且一开始m0上执行的协程正式g0,m0和g0就这样联系了起来,全局变量allgs记录所有的g,allm自然用户记录所有的m。

最初go语言的调度模型里只有M和G,所以待执行的G排排坐等在一处,每个M来这里获取一个G时都要加锁,多个M分担着多个G的执行任务,就会因频繁加锁解锁而发生等待,影响程序并发性能,所有后来在M和G之外又引入了P。

P对应的数据结构是runtime.p,它有一个本地队列runq,这样只要把一个P关联到一个M,这个M就可以从P这个直接获取待执行的G,不用每次都和众多M从一个全局队列中争抢任务了,也就是说,虽然P有一个本地runq,但是依然有一个全局runq,它保存在全局变量sched中,这个全局变量sched代表的是调度器,对应的数据结构是runtime.schedt,这里记录着所有空闲的m和空闲的p等等,许多和调度相关的内容,其中就有一个全局的runq,如果p的本地队列已经满了,那么等待执行的G就会被放到这个全局队列里,而M会先从关联P持有的本地队列runq中获取待执行G,没有的话再到调度器持有的全局队列这里领取一些任务,如果这里也没有了,就会从别的P那里分担一些P过来,同G和M一样,也有一个全局变量allp用于保存所有的P,在程序初始化过程中会进行调度器初始化,这时会按照GOMAXPROCS这个环境变量,决定创建多少个P,保存在全局变量allp中,并且把第一个P(allp[0])与m0关联起来,简单来说,G、M和P就是这样的合作关系。

在main goroutine创建之前,G、M、P的情况是这样的,main goroutine创建之后被加入到当前P的本地队列中,然后通过mstart函数开启调度循环,这个mstart函数是所有工作线程的入口,主要就是调用schedule函数,也是就执行调度循环,其实对于一个活跃的m而言,不是在执行某个G,就是在执行调度程序获取某个G。

队列里只有main goroutine等待执行,所有m0切换到main goroutine执行入口自然是runtime.main,它会做很多事情,包括创建监控线程、进行包初始化等等,其中也包括调用我们熟悉的main.main,终于可以输出helloworld了,值得一提的是,在main.main返回之后,runtime.main会调用exit()函数结束进程;

如果在main.main中不直接输出,而是通过一个协程来输出,那么到main.main被调用执行时,就会创建一个新的goroutine,我们把它记为“hello goroutine”,我们通过go关键字创建协程,会被编译器转换为newproc函数调用,main goroutine也是由newproc函数创建的,创建goroutine时我们只负责指定入口、参数, 而newproc会给goroutine构造一个栈桢,目的是让协程任务结束后,返回到goexit函数中,进行协程资源回收处理等工作,这很合理,一个协程任务完成后,是该放到空闲G队列里备用还是该释放,总归要有个出路,回到这里,如果我们设置GOMAXPROCS只创建一个P,新创建的hello goroutine被添加到当前P的本地runq,然后main.main就结束了,再然后exit()函数被调用,进程就结束了,所有hello goroutine它没能执行,问题就在于main.main返回后,exit函数就会被调用,直接把进程结束掉,没给hello goroutine空出调度执行的时间,所以要让hello goroutine执行就要在main.main返回之前拖延下时间,如果使用time.Sleep(),实际上会调用gopark函数,把当前协程的状态从_Grunning修改为_Gwaiting,然后main goroutine不会回到当前P的runq中,而是在time中等待,继而调用schedule()进行调度,hello goroutine得以执行,等到sleep的事件到达后,timer会把main goroutine重新置为_Grunnale状态,放回到P的runq中,再然后,main.main结束,exit得以调用进程退出,这是只有一个P的情况。

如果创建多个P,hello goroutine创建之后,虽然默认会添加到当前P的本地队列里,但是在有空闲P的情况下,就可以启动新的线程关联到这个空闲的P,并把hello goroutine放到它的本地队列中了,同样的可以使用time.Sleep,或者是等待一个Channel,又或者是使用WaitGroup,反正只要main.main不马上返回,hello goroutine就有时间得以执行了。

GM模型和GMP模型

GMP是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统,区别于操作系统调度OS线程。

  1. G就是goroutine,里面除了存放goroutine信息外,还有与所在P的绑定等信息;
  2. M(machine)是go运行时(runtime)对操作系统内核线程的虚拟,M与内核线程一般是一一映射的关系,一个goroutine最终是要放在M上执行;
  3. P管理一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针、堆栈地址以及地质边界),P会对自己的goroutine队列做一些调度 (比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等),当自己的队列(本地队列)消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里分担(抢)一些任务,P与M也是一一对应关系。

GMP关系:P管理着一组G挂载在M上运行。 当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。

P的个数是通过runtime.GOMAXPROCS设定(最大256),Go1.5版本之后默认为物理线程数。 在并发量大的时候会增加一些P和M,但不会太多,切换太频繁的话得不偿失。

单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。 其一大特点是goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。

4.goroutine的创建、让出与恢复

package main
//hellogoroutine
//多了一个参数
func hello(name string){
    println("Hello,"name)
}
func main(){
    name:="goroutine"
    go hello(name)
}

main goroutine执行起来会创建一个hellogoroutine,而创建的任务就交由newproc函数 func newproc(siz int32,fn *funcval)来负责,我们通过函数栈桢来看一下newproc函数的调用过程,main函数栈桢自然分配在main goroutine的协程栈中,还记得go语言函数栈桢布局吧,call指令入栈的返回地址之后,是调用者栈基,然后是局部变量区间,以及调用其他函数时传递返回值和参数的区间,main函数这里有一个局部变量name,接下来要调用newproc函数,从newproc签名来看要接收两个参数,第一个是传递给协程入口函数的参数占多少字节,第二个是协程入口函数对应的funcval指针,所以在参数空间这里要入栈两个参数,由右至左,先入栈fn也就是协程入口函数hello对应的funcval指针,再入栈参数大小,一个string类型的参数64位下占16字节,实际上这个要传给协程入口函数的参数,也会被放到第二个参数之后,所有局部变量那么要拷贝到这里,这样就好像给newproc函数传递了三个参数一样,而这下面就是Call指令入栈的返回地址,再然后就是newproc函数栈桢了,而newproc这里主要做的就是切换到g0栈去调用newproc1函数。

至于为什么要切换到g0栈?简单来说是因为g0的栈空间,它大呀!因为runtime中很多函数都有no-split (不分裂)标记,意味着这个函数不支持栈增长,也就是说编译器不会在这个函数中 插入栈增长相关的检测代码,协程栈本来就比线程栈小的多,这些个函数自己要消耗栈空间却又不支持栈增长,那在普通协程上执行它们,万一栈溢出了就不好了,而g0的栈直接分配在线程栈上,栈空间足够大,所以直接切换到g0栈来执行这些函数,就不用担心栈溢出的问题了,

newproc1(fn,argp,siz,gp,pc)

再来看它调用newproc1时,都传递了些什么参数,fn和siz不用多说就是newproc自己接收的那两个参数,argp赋值时是用参数fn的地址加上一个指针大小, 正好是fn=&hello funcval siz=16,所以argp用来定位到协程入口函数的参数,第四个参数gp是当前协程的指针,我们的例子中,newproc函数执行在main goroutine中,所以gp就是main goroutine的g指针,最后一个参数pc,在newproc函数中调用getcallerpc()得到的是newproc函数调用结束后的返回地址, 也就是 return addr,调用newproc函数时,由call指令入栈的那个返回地址。

与newproc1而言,他的任务是创建一个协程,而目前已经知道新建协程入口在哪里、参数在哪儿、占多少字节、还知道它的父协程是谁,以及创建完协程后要返回到哪里去。newproc1首先通过acquirem禁止当前m被抢占,为什么不能抢占,因为接下来要执行的程序中,可能会把当前p保存到局部变量中,若此时m被抢占,p关联到别的m,等到再次恢复时,继续使用这个局部变量里保存的p就会造成问题,所以为了保持数据一致性,会暂时禁止m被抢占,继续看,接下来会尝试获取一个空闲的G gfget(p),如果当前P和调度器中都没有空闲的G,就创建一个,并添加到全局变量allg是中,我们依然把这个新创建的协程,记为hello goroutine,此时它的状态是_Gdead,而且已然拥有自己的协程栈,接下来如果协程入口有参数,就把参数移动到协程栈上,对hello goroutine而言,就要把这里的参数拷贝到这里,再然后就有点意思了,接下来会把goexit函数的地址加1,压入协程栈,再把hello goroutine对应的g这里,startpc置为协程入口函数起始地址,gopc置为父协程调用newproc后的返回地址,g.sched这个结构体用于保存现场,此时,会把g.sched.sp置为协程栈指针,g.sched.pc指向协程入口函数的起始地址,现在我们来看hello goroutine的协程栈,参数空间、返回地址,下面是hello函数栈桢,这岂不是就像在goexit函数中,调用了协程入口函数hello,并且传递了用户传递的参数,而指令指针刚刚跳转到hello函数的入口处, 却还没有开始执行时的状态,所以经过这一通伪装待到这个协程得到调度执行的时候,通过g.sched恢复现场,就会从hello函数入口处开始执行了,而hello函数结束后便会返回到goexit函数中,执行协程资源回收等收尾工作, 这样协程该如何出厂和如何收场都有了着落,甚是巧妙。

不过,这还没完,newproc1还会给新建的goroutine赋予一个唯一id,给g.goid赋值前,会把协程的状态置为_Grunnable,这个状态也意味着,这个G可以进到run queue里了,所以接下来会调用runqput,把这个G放到当前P的本地队列中,然后判断如果当前有空闲的P,而且没有处于spining状态的M,也就是说所有M都在忙,同时主协程已经开始执行了,那么就调用wakep函数,启用一个m并把它置为spining状态,最后与一开始的acquirem相呼应,会调用releasem允许当前m被抢占,话说这一边spining状态的m启动后,忙不迭地执行调度循环寻找任务,从本地runq,到全局runq,再到其他p的runq,只为找到个待执行的G,却也抵不过另一边main goroutine早早的就结束了进程。

上一次我们使用time.sleep拖延了一下main.main返回的时间,这一次我们通过等待一个channel看一下协程如何让出又是怎样恢复的,channel对应的数据结构是runtime.hchan,里面有channel缓冲区地址、大小、读写下标,也记录着元素类型、大小及channel是否已关闭,还记录着等待channel的那些G的读队列与写队列,自然也少不了保护channel并发操作安全的一把锁,我们这个例子中创建的是无缓冲的channel,<-chan 会被编译器转换成runtime.chanrecv1函数调用, 而它实际上会调用runtime.chanrecv函数,ch没有缓冲区,目前也没个那个G等在sendq中,简言之没有数据可读,所以当前goroutine会阻塞在ch这里等待数据,当前goroutine自然是main goroutine,而channel的读等待队列是一个sudog类型的链表,链表项会记录那个G等在这里,读到数据后要放到哪里等等,然后chanrecv通过gopark函数(runtime.gopark)使当前goroutine挂起,让出cpu,gopark首先会调用acquirem禁止当前m被抢占,然后把main goroutine的状态从_Grunning修改为 _Gwaiting,main goroutine不再是执行中状态了,接下来调用releasem解除m的抢占禁令,最后调用mcall(park_m),它主要负责保存当前协程的执行现场,然后切换到g0栈,调用由mcall的参数传入的这个函数,对应到这里就是park_m函数,park_m会根据g0找到当前m,把m.curg置为nil,所以当前m正在执行的G,变不再是main goroutine了,最后会调用schedule()寻求下一个待执行的g,经过上面这一番折腾, hello goroutine无论是得到当前m的调度,还是被其它m抢了去,总之是可以执行了,等到hello goroutine完成工作,关闭main goroutine等待的channel时,不只会修改channel的closed状态,还会处理等待队列里的这些G,我们这个例子,只在读队列里面有一个main goroutine在等待,所以会把它接收的数据置为nil,并最终调用goready函数结束这个G的等待状态,而goready函数会切换到g0栈,并执行runtime.ready函数,目前待ready的协程自然是main goroutine,此时它的状态是 _Gwaiting,接下来会被修改为 _Grunnable,表示它又可以被调度执行了,然后,他会被放到当前p的本地runq中,同时协程创建时一样,接下来也会检查是否有空闲的P,并且没有spinning状态的m,是的话,也会调用wakep(),启动新的m,接下来hello goroutine结束,main goroutine得到调度执行,最终结束进程,这样看来time.sleep和channel,底层都会调用gopark来实现协程让出,都会使用goready把协程恢复到runnable状态,放回到runq中。

这一次,我们了解了协程创建的基本步骤,以及协程让出与恢复到可运行状态的大致过程。

7.46

5.协程让出、抢占、监控和调度

我们已经知道协程执行time.Sleep的时候,状态会从 _Grunning变成 _Gwaiting,并进入到对应的timer中进行等待,而timer中有一个回调函数,在指定时间到达后调用这个回调函数,把等在这里的协程恢复到 _Grunnable状态,并放到runq中,那谁负责定时器到达时触发定时器注册的回调函数呢?其实每个P都持有一个最小堆,存储在p.timers中,用于管理自己的timer,堆顶timer就是接下来要触发的那一个,而每次调度时schedule()都会调用checkTimer,检查并执行已经到时间的那些timer,不过这还不够稳妥,如果所有M都在忙不能及时触发调度的话,可能会导致timer执行时间发生较大的偏差,所以还会通过监控线程sysmon来增加一层保障。

在介绍HelloGoroutine的执行过程时,我们提过监控线程是由main goroutine创建的,这个监控线程与GMP中的工作线程不同,并不需要依赖P,也不由GMP模型调度,它会重复执行一系列任务,只不过会视情况调整自己的休眠时间,其中一项任务便是保障timer正常执行,监控线程检测到接下来有timer要执行时,不仅会调整休眠时间,还会在空不出M时创建新的工作线程,以保障timer可以顺利执行。

当协程等待一个channel时,其状态也会从 _Grunning变成 _Gwaiting,并进入到对应channel的读队列和写队列中等待, 如果协程需要等待IO事件,就也需要让出,以epoll为例,若IO事件尚未就绪,需要注册要等待的IO时间到监听队列中,而每个监听对象都可以关联一个event data,所以就在这里记录是哪个协程在等待,等到事件就绪是再把它恢复到runq中即可,不过timer有设置好的触发事件when,等待的channel可读、可写或关闭了,也自会通知到相关协程,而获取就绪的IO事件需要主动轮询,所以为了降低IO延迟,需要时不时的轮询一下,也就是执行netpoll,实际上监控线程、调度器、GC等工作过程都会按需执行netpoll,全局变量sched中会记录上次netpoll执行的时间lastpoll,监控线程检测到距离上次轮询已经超过了10ms,就会再执行一次netpoll。

上面说的无一例外都是协程会主动让出的情况,那要是一个协程不会等待timer、channel或者IO事件,就不用让出了吗?那必须不能啊!否则调度器不就成了摆设,那怎么让那些不用等待的协程“让出”呢,这就是监控线程的另一个工作任务了,那就是本着公平调度的原则,对运行过程较长的P,实行“抢占”操作,就是告诉那些运行时间超过特定阀值(10ms)的G该让一让了,怎么知道运行的G时间过长了呢,P里面有一个schedtick字段,每当调度执行一个新的G并且不继承上个G的时间片时,就会把它自增1,而这个p.sysmontick中schedwhen记录的是上一次调度的时间,监控线程如果检测到sysmontick.schedtick与p.schedtick不相等,说明这个P→发生了新的调度,就会同步这里的调度次数,并更新这个调度时间,若二者相等就说明,自schedwhen这个时间点之后,这个P并未发生新的调度,或者即使发生了新的调度也沿用了之前G的时间片,所以可以通过当前时间与schedwhen的差值来判断当前P上的G是否运行时间过长了,那如果真的运行时间过长了,要怎么通知它让出呢?

这就不得不提到栈增长了,除了对协程栈没什么消耗的函数调用, GO语言编译器都会在函数头部插入栈增长相关代码,实际上编译器插入的栈增长代码一共有3种,
如果栈帧比较小,这个SP表示当前协程栈使用到了什么位置,stackguard0是栈空间下界,所以当协程栈的消耗达到或超过这个位置时,就需要栈增长了;
如果栈帧大小处在 _StackSmall和 _stackBig之间,超出的部分大于StackSmall就要进行栈增长了,而对于栈帧大小超过 _StackBig函数,插入的代码就又有所不同了;
判断是否需要栈增长的方式,本质上与第二种相同,而我们要关注的是这里的stackPreempt,它是和协程调度相关的重要标识,当runtime希望某个协程让出CPU时,
就会把它的stackguard0赋值为stackPreempt,这是一个非常大的值,真正的栈指针不可能指向这个位置,所以可以安全的用作特殊标识,正因为stackPreempt的值足够大,
所以这两段代码中判断结果也都会为true,进而调转到morestack处,最终会调用runtime.newstack函数,它实际负责栈增长工作,不过它在栈增长之前,
会先判断stackguard0是否等于stackPreempt,等于的话就不进行栈增长,而是执行一次协程调度,所以协程不主动让出时也可以设置stackPreempt标识,通知它让出,
不过这种调度的方式缺陷,就是过于依赖栈增长代码,如果来个空的for循环,因为与栈增长无关,监控线程等也无法通过,设置stackPreempt标识来实现抢占,所以最终导致程序卡死。
这种方式在1.14中得到了解决,因为它实现了异步抢占,具体实现在不同平台中不尽相同,例如在unix系统中会向协程关联的M发送信号(sigPreempt),接下来目标线程会被信号中断,
转去执行runtime.sighandler,在sighandler函数中检测到信号为sigPreempt后,就会调用runtime.doSigPreempt函数,他会向当前被打断的协程上下文中注入一个异步抢占函数调用,
处理完信号后,sighandler返回,被中断的协程得到恢复立刻执行被注入的异步抢占函数,该函数最终会执行runtime中的调度逻辑,这不就让出了吗。
所以在1.14版本中这段代码执行起来就不会卡死了,而监控线程的抢占方式又多了一种异步抢占。
其实为了充分利用cpu,监控线程还会抢占在系统调用中的P,因为一个协程要切换到系统调用就要切换到go栈,在系统调用还未执行完之前,这个M和这个G算是抱团了,不能被分开也就用不到P,
所以在陷入系统调用之前,当前M会让出P,解除m.p与P的强关联,只在m.oldp中记录这个P,P的数目毕竟有限,如果其他协程在等待执行,那么放任P如此闲置就着实浪费了,
还是把它关联到其他M,继续工作比较划算,不过如果当前M从系统调用中恢复,会先检查之前的P是否被占用,没有的话就继续使用,否则再去申请一个没申请到的话,就当前的G放到全局runq中去,
然后当前线程就睡眠了。
让出、抢占之后,M也不能闲着得找到下一个待执行的G来运行,这就是schedule()的职责了。

9.51

channel

select

https://blog.csdn.net/qq_25870633/article/details/83339538

https://blog.csdn.net/ma2595162349/article/details/112911710

https://blog.csdn.net/u011957758/article/details/82230316

select关键字可以监听channel上的数据流动,

time.Sleep()

goroutine调度器

  • 线程池目的是线程可以复用。
  • 对象池是对象用完丢进池中,以便复用,那么池一开始是没有东西的。(先无后创建,用完就放池里面。)

goroutine实现高并发

goroutine协程与多线程的区别

https://blog.csdn.net/qq_44205272/article/details/111500905

从使用上讲

  1. 协程比线程更轻量,可以创建十万、百万不用担心资源问题;
  2. goroutine和chan搭配使用,实现多线程、高并发更方便;
  3. go的并发使用方便,但多线程还是要小心不要产生数据竞态,以及共享锁、互斥锁的选择问题、并发操作的数据同步问题(多核心,不用的cpu缓存操作,同步到内存使其他协程感知)

总结:轻量,goroutine搭配chan实现高并发方便,避免数据竞态、共享锁互斥锁的选择问题、并发操作数据同步问题。

从实现上讲

  1. 从资源上讲,线程的栈内存大小一般是固定的2MB,虽然这个数值可以设置,但是太大浪费,太小了又不够用。
    而goroutine栈内存是可变的初始一般为2kb,随着需求可以扩大达到1GB。所有goroutine十分的轻量级,且能满足不同的需求。
  2. 从调度上讲,线程的调度由OS的内核完成,线程的切换需要CPU寄存器和内存的数据交换,从而切换不同的线程上下文,其触发方式为CPU时钟。
    而goroutine的调度则比较轻量级,由go自身的调度器完成;只关心当前go程序内协程的调度;触发方式为go内部的事件,time.Sleep() 通道阻塞,互斥量操作等。
  3. 协程同线程的关系,有些类似于线程通进程的关系。多个协程绑定到同一个线程上,按照一定的调度算法执行。

Goroutine协程之间的数据传输

  1. 加锁
  2. channel

https://blog.csdn.net/shaoduo/article/details/92829719

golang单例模式

单例模式

package main

import (
  "fmt"
  "sync"
)

// 单例模式
func main() {
  // SingletonPattern()
  // SingletonPattern2()
  SingletonPattern3()
}

// 问题,获取web配置,没有必要开辟多个内存
type WebConfig struct {
  Port int
}

func GetConfig() *WebConfig {
  return &WebConfig{Port: 1111}
}
func SingletonPattern() {
  c := GetConfig()
  c2 := GetConfig()
  c3 := GetConfig()
  // 值时相等的,但是每一个都分配了内存,内存地址不同
  fmt.Println(c == c2, c2 == c3)
  fmt.Println("c addr", &c, "c1 addr", &c2, "c3 addr", &c3)
  fmt.Println("c addr", &c.Port, "c1 addr", &c2.Port, "c3 addr", &c3.Port)
  /*
      false false
    c addr 0xc00000e028 c1 addr 0xc00000e030 c3 addr 0xc00000e038
    c addr 0xc0000140e0 c1 addr 0xc0000140e8 c3 addr 0xc0000140f0
  */
}

// 单例写法1
var cc *WebConfig
var mux sync.Mutex

func GetConfig2() *WebConfig {
  mux.Lock()
  defer mux.Unlock()
  if cc == nil {
    cc = &WebConfig{Port: 1111}
  }
  return cc
}
func SingletonPattern2() {
  c1 := GetConfig2()
  c2 := GetConfig2()
  fmt.Println(c1 == c2)
}

// 单例写法2

func GetConfig3() *WebConfig {
  return &WebConfig{Port: 1111}
}
func SingletonPattern3init() *WebConfig {
  var once sync.Once
  once.Do(func() {
    cc = GetConfig3()
  })
  return cc
}

func SingletonPattern3() {
  c1 := cc
  c2 := cc
  fmt.Println(c1 == c2)
}

PTZ

P---Pan,中文含义为adj.全景的 v.(移动摄像头)追拍 n.平底锅,延伸意:水平,全景,代表云台水平方向移动控制;

T---Tilt,中文含义为n.倾斜,垂直,代表云台垂直方向移动控制;

Z---Zoom,中文含义是可变焦距镜头,代表镜头变焦控制。

变焦镜头:在一定范围内可以变换焦距、从而得到不同宽窄的视场角,不同大小的影像和不同景物范围的照相机镜头。变焦镜头在不改变拍摄距离的情况下,可以通过变动焦距来改变拍摄范围。

linux根据进程名获取PID并结束

https://www.cnblogs.com/linbky/p/11599150.html

ps aux | grep "/home/neuron" | grep -v grep |cut -c 0-5 | xargs kill -9 \r

搞笑代码注释,佛祖保佑 永无BUG

https://www.cnblogs.com/wangjunwei/p/5222445.html

Ubuntu20.04开机启动指定程序vscode,开机自动登录

https://www.jianshu.com/p/e88cd2fa5a3a

vim .bash_profile
#添加code

code

posted @ 2022-04-28 17:05  凌易说-lingyisay  阅读(205)  评论(0编辑  收藏  举报