《并发概念计算机.docx》由会员分享,可在线阅读,更多相关《并发概念计算机.docx(38页珍藏版)》请在课桌文档上搜索。
1、并发概念(计算机)一.串行程序、并发程序、并行程序串行程序:指只能被顺序执行的指令列表。并发程序:并发程序属于程序,其内部是由多个部分(串行程序)组合在一起,这些部分组合在一起构成了一个整体,叫做并发程序。也叫单元并行程序:指可以在并行的硬件上(CPU,服务器),运行的并发程序。二、并发系统、并行系统并发系统:程序和程序之间是通过协议协商一致后形成通信。并发系统就是多个并发程序间构成的,看作是一个系统。并行系统:指并发系统以并行的方式存在。每个并发系统中的程序,很有可能会部署在多个并行硬件上(服务器)同时运行,也叫做分布式系统。三、并发程序内部交互1 .什么是并发程序的内部交互?并发程序中存在
2、多个串行程序,这些串行程序之间可能会存在数据交互的需求。比如多个串行程序对一个共享资源进行访问(数据库,消息队列);又或者在它们之间传递一些数据。在这些需求的环境下,协调它们的执行,就要涉及到同步。2 .同步的作用避免在并发访问共享资源时发生的冲突,可以有条不紊的传递数据。3 .同步的原则当程序要访问一个共享资源时,就必须请求该共享资源并获取对应的访问权;反之如果程序不再需要一个共享资源,就要放弃该资源的访问权。(释放资源,Close()而应该等到其释放资源后在进行访问(锁)。结论:一个共享资源在同一时刻内,只能被一个程序占用。同步会导致系统资源的浪费,耦合性太强。所以有了异步通信的思想。4
3、.异步通信发送方可以不加延迟的将消息发送出去,接收方也不会造成发送方的等待。(解耦)数据会被临时存储在一个通道缓存(IPC)的通道中。通道缓存是一种特殊的共享资源,可以被多个程序使用。接收方准备就绪后无需知道发送方,直接从通道缓存中获取数据。多线程编程概念(计算机)一、什么是多线程编程POSIX:可移植性的UniX操作系统接口。NPTL:LinUX操作系统的最新线程库。多线程编程是一种比多进程编程更灵活,更高效的编程方法。在Linux系统中提供了一个以POSIX为标准定义的线程(简称:POSIX线程)为中心的各种系统调用。POSIX也是go并发编程在Iinux系统下真正使用的内核接口,也就是N
4、PTL本地POSIX线程库。二、什么是线程线程可以视为进程中的控制流,一个进程中至少要包含一个线程。因为一个进程中至少要一个控制流持续运行。所以第一个线程会随着其进程的启动而创建,这个线程也被称之为主线程。一个进程中可以存在多个线程,这些线程是由当前进程中存在的线程创建出来的,而创建的方法就是系统调用。更准确的说是调用pthread_create函数。拥有多个线程的进程可以并发执行多个任务,并且即使某个线程被阻塞也不会影响到进程中其它任务的执行。线程会大大提升程序的响应效率和吞吐量线程不能独立于进程之上,线程的生命周期不能超过其所在进程的生命周期。三、如何实现一个线程通过系统调用来控制线程。线
5、程与进程的父子家族关系树结构不同,线程之间的关系都是平等的。它们之间不存在层级关系,任何线程都可以对同一进程中的其它线程进行有效的管理。其中管理分为5种:pthread_create创建线程pthread_cancel终止线程pthread_join连接已经终止线程pthread_detach分离线程pthread_exit彻底退出主线程3-1V线程TID标识和进程一样,线程也有属于自己的ID,叫做TIDo但与进程不同的是,线程ID在系统内中并不唯一,只会在其当前进程下唯一。不过Iinux系统则确保了每个线程在系统内ID的唯一性。目当某个线程不复存在后,其TID可以被其它线程复用。线程ID由操
6、作系统内核进行分配和维护。四、线程运行时发生异常会怎么样线程异常后会强制调用pthread,cancel函数来终止线程,然后调用detach函数分离线程。根据不同的异常情况会进入僵死状态和终止状态两种。僵死状态就是还会保留一些程序所需的资源,不会删掉线程上的全部数据。如果不再需要僵死线程,就会调用PthreadJoin终止掉僵死线程。五、如何在两个线程间共享数据一个进程中的所有线程都拥有自己的线程栈,并以此来存储线程私有的数据栈。这些线程的线程栈包含在其所属进程的虚拟内存空间当中。一个进程中的很多资源都会被其所有线程共享,这些被线程共享资源包括(在当前进程的虚拟内存中存储的):数据段、代码段、
7、堆、栈、信号,以及当前进程支持的文件描述符。正因如此,同一个进程中的多个线程运行的肯定是一个程序。只不过具体的流控方式会有所不同。另外,创建一个线程也不会像创建进程那样费劲,因为线程需要的所有代码、数据段、资源都在其进程中存储不需要被复制就能使用。操作系统内核提供了若干系统调用以便应用程序能够管理当前进程中的所有线程,还可以通过相应的系统功能协调这些线程的运行。六、线程状态线程状态分为:1 .就绪状态2 .运行状态3 .阻塞状态4 .睡眠状态当调用pthread_create函数时线程会进入就绪状态。当线程获得运行时机被CPU激活运行之后会进入运行状态。在线程运行中如果出现了阻塞等待,就会进入
8、睡眠状态,阻塞解除后会重新进入就绪状态。在线程运行中如果出现了return语句,或者退出线程的语句,根据当时不同的线程执行环境会进入僵尸状态和终止状态。不管是僵尸状态还是终止状态最终都会被回收。七、线程调度在线程的生命周期中,操作系统内核对线程的调用是非常核心的部分。正是因为有了调度器的实时调度和切换,才给众多线程一种并行运行的幻觉。调度器会把事件划分成极小的时间片,并把这些时间片分配给不同的线程,以使众多线程都有机会在CPU上运行。一个线程什么时候能够获得CPU时间,以及在CPU上运行多久,都是调度器的工作范畴。调度器:最大程度保证多核CPU之间的平衡运行。7-1.什么是线程调度,什么是线程
9、切换线程调度(也称线程间的上下文切换),线程的执行总是趋向于CPU受限或者I/O受限。也就是说线程的调度只分为两类:一些线程需要花费一定的时间使用CPU进行计算,另一些线程会花费一些时间等待相对较慢的I/O操作完成。调度器会依据它对线程的趋向性的猜测把他们进行分类,并让I/O受限的线程具有更高的动态优先级以及优先使用CPUo调度器会认为I/O操作往往会花费更长的时间,所以应该让它们尽早执行。这也是为了让众多线程运行的更加高效。在人决定下一个要敲击的按键、磁盘在磁道中定位簇或者网卡从网络中接收数据帧的时候,CPU可以腾出手来位其它线程服务。7-2.线程的静态、动态优先级调用线程的动态优先级是可以
10、被调度器实时调整的,而与之相对应的线程的静态优先级只能由程序指定。如果应用程序没有指定一个线程的静态优先级,那么默认为Oo调度器不会改变线程的静态优先级。线程的动态优先级就是调度器在其静态优先级的基础上调整得出的,动态优先级决定了线程的运行顺序。而线程的静态优先级决定了线程单次在CPU上运行的最长时间,也就是调度器分配给它的时间片的大小。所有等待使用CPU的线程会按照动态优先级从高到低的进行顺序排序,并依序放到与该CPU对应的运行队列当中。因此,下一个运行的线程总是动态优先级最高的那一个。7-3V线程的优先级队列每一个CPU的运行队列中都包含两个优先级阵列。其中一个用于存放正在等待运行的线程,
11、暂时称之为(激活的优先级阵列)。另一个则用于存放正在等待的运行的线程,暂时称为(过期的优先级阵歹I)。下一个运行的线程总是会从激活的优先级队列中选出。如果CPU发现某个线程占用了CPU很久的时间,并且激活的优先级队列中还有优先级与他相同的线程在等待运行,那么调度器就会让那个等待的线程在CPU上运行,而被换下的则进入过期的优先级阵列。当激活的优先级阵列中没有等待运行的线程时,调度器会把这两个优先级阵列的身份互换,之前的激活阵列变成过期阵列,现在的过期阵列变成激活阵列。如此,放入过期的优先级阵列的线程就又有机会运行了。当然,线程并不是总会处于运行和就绪状态。他还有可能会进入阻塞睡眠状态,处于睡眠状
12、态的线程是不能够被调度和运行的。它们会从两个阵列中移除。七、线程模型线程的实现模型分为三个,分别是:用户级线程模型、内核级线程模型、两级线程模型。它们之间最大的差异就在于线程与内核实体(内核调度实体KES)之间的对应关系。内核调度实体就是被内核调度器所调用的实体对象。也被称为内核级线程,是操作系统得最小运行单元。1 .用户级线程模型(M:1):此模型下的线程是由用户级别的线程库全权管理的。线程库并不是内核的一部分,而只是存储在进程的用户空间之中,这些线程的存在对于内核而言是无法感知的。用户级线程并不是内核的调度器的调度对象。对线程的各种管理和协调完全是用户及程序的自主行为,与内核无关。应用程序
13、在对线程进行创建、终止、切换或同步等操作的时候,并不需要让CPU从用户态转变成内核态。2 .内核级线程模型Q:1):该模型下的线程是由内核负责管理的,它门是内核的一部分。应用程序对线程的创建、终止和同步都必须通过内核提供的系统调用来完成(PthreaCL*)。进程中的每一个线程都会与一个KES(内核调度实体)相对应。也就是说,内核可以分别为每一个线程进行调度。由此,内核级线程模型也被称为Ll的线程实现。一对一线程实现消除了多对一线程实现的很多弊端。可以真正的实现线程的并发运行。3 .两级线程模型(M:N):两级线程模型的目标是取前两种模型的精华,去其糟粕,也成为多对多的(M:N)线程实现。与其
14、他模型相比,两级线程模型提供了更多的灵活性。在此模型下一个进程可以与多个KSE(内核调度实体)相关联,这与内核级线程模型相似,不同点在于进程中的线程并不与KES对应,这些应用程序线程可以映射到同一个已经关联的KES上。实现了两级线程模型的线程库,会通过操作系统内核创建多个内核级线程。然后它会通过这些内核级线程对应用程序线程进行调度。大多数此类线程库都可以将这些应用程序线程动态地与内核级线程关联。这样的设计显然使线程的管理工作更加复杂,因为这需要线程库与内核级线程共同努力和协作才能正确、有效的运行。八、线程同步线程同步的目的就是为了更好地协同工作或者维持数据的一致性。1 .共享数据的一致性一个进
15、程所拥有的相当一部分虚拟内存地址都可以被进程中所有线程共享,所以这些共享数据大多是以内存的空间作为载体。如果两个线程同时读取同一块共享内存但获取到的数据却不相同,那么程序很有可能就会出现某种错误。这是因为,共享数据的一致性往往代表着某种约定,而只有在该约定成立的前提下,多线程程序中的各个线程才能够使相应的流程执行正确。换句话说,如果操作的共享数据的结果,总是与约定的结果相同,就说明共享数据的一致性得到了保证。实际上,保证共享数据一致性的最简单最彻底的方法就是使该数据成为一个不变量。例如:常量就是不变量,它不可能被改变,也就不可能出现不一致的情况。因此无论当前程序中有多少个可能访问常量的线程,都
16、不需要采取任何措施。但是程序中不可能都是常量,我们需要通过额外的手段保证被多个线程共享的变量的一致性,这样就有了临界区的概念。2 .临界区临界区是只能被串行化访问或执行的某个资源或者代码,也被称为串行区域。保证临界区有效的最佳方式就是利用同步机制。在针对多线程程序的同步机制中包含了很多同步方法,包括原子性操作和互斥量,以及条件变量。3 .互斥量在同一时刻,只允许一个线程处于临界区之内的约束,称之为互斥(mutex)o每个线程在进入临界区之前,都必须先锁定某个对象,只有成功锁定对象的线程才会允许进入临界区,否则就会阻塞,这种对象被称为互斥对象或互斥量。互斥量有两种状态,既锁定状态、未锁定状态。互
17、斥量每次只能锁定一次,处于已锁定状态的互斥量不能被再次锁定。除非它已经解锁,否则任何!线程都不能对它进行二次加锁。如果对一个已锁定的互斥量进行加锁操作,那么这个操作必定会失败。成功锁定互斥量的线程会成为该互斥量的所有者,只有互斥量的所有者才能对其进行解锁。从这个角度讲,多个线程对同一个互斥量的争相锁定也可以看作是对互斥量的释放。当线程离开临界区的时候,必须要对相应的互斥量进行解锁。这样其它想进入该临界区而被阻塞的线程才会被唤醒,并且有机会再次尝试锁定该互斥量。在这些线程中只有一个线程会成功锁定该互斥量。注意:对同一个互斥量的锁定与解锁应该成队出现(defer)。4 .线程安全性,怎么实现线程安
18、全性?如果有一个代码块,它可以被多个线程并发执行,且总能够产生预期的结果,那么该代码块就是线程安全的。如果代码块对共享数据进行了更新操作,那么这个代码块就是非线程安全的。但是如果该代码块处于临界区中,那么这个代码块就是线程安全的。但是,为了实现线程安全,把所有的代码都置于临界区中虽然可行,但也是一种最低效的方法。可以仔细从函数体中查找出操作共享数据的代码并用互斥量将它们保护起来。还可以将这写代码从函数体中分离出来,然后再将它们聚集在一起成为一个函数或者一个结构体。为这个函数或结构体实现线程安全性。GMP一、早期的GMP调度器1 .当内核线程获取go的协程时,会经过调度器访问go的全局协程队列。
19、go的全局协程队列是由锁来进行保护的。当拿到队列的锁之后,会执行队列中的首个goroutine协程,其余的goroutine依次向前挪动一位。2 .内核执行完goroutine协程任务之后,会释放锁,并把这个goroutine还回全局协程队列,只不过这时候会把goroutine放在队尾。早期调度器的弊端:1.创建、销毁、调度G都需要每个内核线程M获取锁,形成了激烈的锁竞争。2 .内核M转移goroutine,会造成延迟和额外的系统负载。3 .系统调用(CPU在多个M之间切换)导致频繁的线程阻塞和取消阻塞操作,增加了系统开销。二、GMP模型简介GMP是三种元素的缩写。M与P是相互引用的关系,P与
20、G是一对多的调用关系。G:goroutine协程,go中的代码片段,协程程序。P:ProCeSSOr处理器,一个P代表执行了一个G。代码片段中所必需的资源(上下文环境)M:Machine,一个M代表一个内核线程,或者称为工作线程。简单来说,一个协程(G)的执行需要P(处理器)和M(内核)的支持,一个M在与P关联之后,就形成了一个有效的协程(G)运行环境(内核线程+上下文环境)。每个P都会包含一个可运行的协程(G)队列(runq)。该队列中的协程(G)会被依次传递给本地的P关联M,并获得运行。2-1V详解M1 .什么情况下会建立一个M一个M代表一个内核线程。大多数情况下,创建M都是由于没有足够的
21、M来关联P,并运行其中可运行的Go在系统执行系统监控和垃圾回收等任务的时候,也会创建M。M是一个结构体。2 .M与P和G是如何关联的M是一个结构体,这个结构体中包含了关联P,G的字段。typemstructgo*gcurg*gPpuintptr当前与M关联的Pnetppuintptr潜在关联的Pspinningbool表示M是否正在寻找可以运行的G。在寻找过程中M会处于自旋状态。Go在运行时可以讲一个M和一个G锁在一起,一旦锁定,这个M就只能运行这个G。Iockedg *g表示与M锁定的G3 .M是什么时候被创建的如何创建的?在M被创建之后,GO系统会先对它进行一番初始化,其中包括对自身所持有
22、的栈空间以及信号处理方面的初始化。M在创建之初,M会被加入全局的M列表中。这时它的起始函数和预联的P也会被设置。当运行时,系统会为这个M专门创建一个新的内核线程并与之关联。如果M就做好了执行G的准备。起始函数只有当M执行系统监控和垃圾回收任务的时候才会被设置。全局M队列在运行时系统需要的时候,会通过全局M队列,获取到所有M的信息,同时防止M被当成垃圾回收掉。4 .空闲M,M停止会如何?运行中的M有时也会被停止,比如在执行垃圾回收任务的过程中。运行时系统在停止M的时候,会把它放入调度器的空闲M列表。这很重要,因为在需要一个未被使用的M时,调度器会先尝试从空间的M中获取。M是否空闲,仅以它是否存在
23、于调度器的空闲M队列表中为依据。5 .能否设置M的最大数量?单个G。程序所使用的M的最大数量是可以设置的,G。程序运行的时候会先启动一个引导程序,这个引导程序会为其建立必要的环境。在初始化调度器的时候,它会对M的最大数量进行初始设置,这个初始值是IooO0,也就是说一个Go程序最多可以使用IOooO个M。2-2V详解P1.什么是PP是能够在M中运行的关键。GO的调度器会适时地让P与不同的M建立或者断开关联,以使P中那些可运行的G能够及时获得运行时机,这与操作系统内核在CPU之上实时的切换不同进程或线程的情形类似。P的最大数量实际上是对程序中并发运行的G的规模的一种限制。P的数量即为可运行G的队
24、列的数量。一个G在被启用,会先追加到某个P的可运行队列中,以等待运行。一个P只有与M关联在一起,才会使其可以运行G。2. P的数量,如何改变P的数量在g。程序启动之初,引导程序会在初始化调度器时,对P的最大数量进行设置。这里的默认值会与当前CPU的总核心数相同。一旦发现环境变量comaxprocs的值大于o,引导程序就会认为我们想要对P的最大数量进行设置。它会先检查一下此值的有效性;如果不大于预设的硬性上限值(256),就会认为是有效的,否则就会被这个硬性上限值取代。第一种方法:调用函数procs把想要设定的数量作为参数传入。第二种方法:在Go程序运行前设置Gomaxprocs的环境变量值。3
25、. P脱离运行状态虽然Go并未对何时调用PROCS函数作限制,但是该函数调用的执行会暂时让所有的P都脱离运行状态,并试图阻止任何用户级别G的运行。只有在新的P最大数量设定完成之后,调度器才会陆续恢复它们。这对于程序的性能是非常大的消耗。所以,P的数量最好在init()函数里设置。实际上多数情况下不改变也没什么。4. P的空闲列表与空闲的M列表类型,Go中也存在一个空闲的P列表。当一个P不再与任何M关联的时候,调度器就会把它放入该列表;而当调度器需要一个空闲的P关联某个M时,就会从此列表中取出一。进入空闲P列表的前提是,该P中可运行的G必须为空。5. P的运行状态Pidle:此状态表明当前P未与
26、任何M存在关联。Prunnig:此状态表明当前P正在与某个M关联。Psyscall:此状态表名当前P中的运行的G正在进行系统调用。Pgcstop:此状态表明调度器需要停止调度。开始垃圾回收。pdead:此状态表明当前P已经不会再被使用。如果Go程序运行的过程中,通过调用PROCS函数减少了P的最大数量,那么多余的P就会被运行时系统置于此状态。P在创建之初是pgcstop,但是这并不意味着要垃圾回收。当P进行初始化之后会进入pidleo6. P的自由调度列表,如何提高G的复用率每个P除了有一个可运行的G队列外,还有一个自由G列表。这个列表中包含了一些已经运行完成的Go当go语句要启用一个G的时候
27、,调度器会先试图从相应P的自由G列表中获取一个现成的G,来封装这个g。语句携带的函数。如果自由G列表中的G太少,调度器会从可运行G列表中转移一部分到自由列表中。如此,只有自由G列表中的G也没了的时候,调度器才会创建新的G,最大可能的提高G的复用率。2-3V详解G1 .什么是G,goroutine的调度原理一个G就是一个goroutine,编程人员使用go,gofunc向系统中提交并发任务。g。的编译器会把提交的g。语句变成内部函数newproc的调用,并把go函数以及参数都作为参数传递给这个函数。调度器在接收到newproc这样一个调用后,会先检查go函数的合法性,然后试图从本地的自由G列表与
28、运行G列表中获取一个可用的Go如果没有,则新建一个G。与M和P相同,G也有一个全局列表。新建的G会第一时间加入全局G列表。这个列表记录着全局中所有的G指针。在初始化完成后,这个G会被存储到本地P的runnext字段中,该字段用于存放新的G,处于这个字段的G会被更早地运行。如果这时的runnext字段已经有了一个G,那么这个G就会被“踢到”该P的可运行G队列的末尾。如果P的可运行队列满了,那么就只能追加到调度器的可运行G队列中等待运行。2 .G的运行状态Gidle:表示G刚被分配,还没有初始化Grunnable:表示当前G正在可运行队列中等待运行。Grunning:表示当前G正在运行。GsysC
29、aII:表示当前G正在执行某个系统调用。Gwaiting:表示当前G正在阻塞。Gdead:表示当前G正在闲置。Gcopystack:表示当前G的栈区正在移动。移动的原因可能是栈的扩展或者收缩。在运行时调度器用一个G封装goroutine函数时,会先对这个G进行初始化,一旦G准备就绪,其状态就会被设置成Grunnableo一个G真正被使用一定是在GrUnnabIe之后。三、GMP调度模型的策略Go语言中的调度,是用来调度操作系统内核之外的程序。调度的对象是MPG实例。1 .调度器的基本结构调度器有自己的数据结构,形成此结构的主要目的就是更加方便的管理和调度各个核心元素的实例,空闲M列表,空间P列
30、表,可运行G队列和自由G队列。还有一些其它的重要字段:2 .重要字段gcwaitinguint32表示是否需要因一些任务而停止调度。stopwaitint32表示需要停止但仍未停止的P的数量stopnodenote用于实现与stopwait相关的事件通知机制sysmonwaituint32表示在停止调度期间系统监控任务是否在等待sysmonnotenote用于实现与SySmOnWait相关的事件通知机制在g。调度器执行工作中,一些任务在执行前是需要暂停调度的,例如垃圾回收任务中的某些子任务,又比如发起运行时恐慌的PaniC任务。3 .gcwaitingstopwaitstopnote都是串行运
31、行时任务执行前后的辅助协调手段。GcwaitingzStopwaitzStopnote:该字段的值用于表示是否需要停止调度,在停止调度时,该值被设置为1;在恢复调度时,该值被设定为0。当一些调度任务在执行时只要发现gcwaiting的值为1,就会把当前P的状态设置为pgstop,然后逐渐递减stopwait字段的值,当stopwait字段的值递减为0时,就说明所有的P的状态都是pgstop,这时就可以唤醒停止调度的任务了。4 .字段Sysmonwait和Sysmonnote针对的是系统监测任务。sysmonwaitSysmonnote在停止调度任务执行之前,系统监测任务也需要暂停。Sysmon
32、wait就是表示是否已经暂停,0表示未暂停,1表示已经暂停。系统监测任务是持续执行的,处于无限的循环当中。每次调度器执行任务之初,系统监测程序都会先检查调度情况,一旦发现调度停止gcwaiting字段不=0,或者P都已经处于闲置状态。就会把Sysmonwait的字段设置=1,并利用Sysmonnote停止调度器自身。在恢复时会将Sysmonwait设置=0,SySmonnote用来恢复系统监测任务。2 .调度策略全力查找:调度器会为P来寻找可运行的G,如果没有找到就会进入全力查找的状态,从各处搜索可以运行的G。如果当前P中实在找不到,就会通过workstealing从别的P偷一个。全力搜索的范
33、围(了解即可)1.获取已经终结准备回收的G3 .从本地P的可运行G列表中获取4 .从全局所有P的可运行G列表中获取5 .从调度器的可运行G列表中获取6 .从网络I/Onetpoller获取G如果经历这些步骤还是没有获取到G,就会停止掉当前的M。当之后的某一个时刻出现G之后,M才会被唤醒。偷取机制:当当前M关联的P中没有可运行的G时,会尝试从其它M关联的P中偷取一个G放到当前M下运行,而不是销毁线程。分离机制:当当前M线程因为G进行系统调用发生阻塞时,该M线程会释放绑定的P,并把整个关联的P转移给其它空闲的M线程进行关联运行。并行限制:设置GOMAXPROCSP的数量,最多有GoMAXPROCS
34、个线程分布在多个CPU上同时运行。3.抢占抢占也叫系统监测,由SySmOn函数实现。主要监测以下任务:1.在需要时抢夺符合条件的P和G,抢占模式2 .在需要时进行强制GC,清扫堆3 .在需要时打印调度器的跟踪信息抢占P和G的途径有两个,首先是通过网络I/O轮询其获取可运行的Go其次是从调度器那里抢夺符合条件的P和Go抢占P过程:在抢占P的流程中,全局P列表中的所有P都会被检查。程序会先查看P的状态,如果为系统调用和运行状态,程序就会对P进行下一步检查,调度器会检查它的调度计数是否同步。P的调度计数由它的Schedick字段存储o只要它的可运行G队列中的某个G被取出运行了,该字段值就会递增。同样
35、的在调度器中也会持有一个调度计数的备份,判断这个备份与P中的Schedick字段值是否相同。如果不同,就会忽略对这个P的进行一步检查。如果相同,就判断距离上次同步该P的调度时间是否满足IOms,如果超过,救说明该P的G已经运行了太久,需要停止并把运行机会让给其它的G。总结:1 .首先从网络I/O中抢到要执行的Go2 .然后调度器会监测每一个P中的Schedick字段,然后与调度器的备份数据比较,如果相同。就会继续判断距离上一次同步该P的时间是否超过IOms,如果超时就会抢占一个新的P来运行。四、gofunc()都经历了哪些过程1 ,创建一个gofunc()后,会把go语句编程对内部函数newp
36、roc的调用,并把g。函数以及其参数都作为参数传递给这个newproc函数。2 .当系统接收到这样一个函数调用后,会检查go函数以及其参数的合法性。与M和P相同,运行时系统也会持有一个G的全局列表,新建的G会在第一时间被加入该列表。这个全局列表的作用就是集中存放当前运行时系统中的所有G指针。3然后初始化go函数变成一个G,设置该G的状态和IDo当初始化结束后就会将G存储到本地的P中等待运行。4.如果当前没有跟M建立管理的P,则会放入全局队列中等待出现与M相关联的P出现。5,当M执行某一个G时发生了阻塞操作,M会阻塞,如果当前有一些G正在运行,就会把这个M中的P剔除。然后再创建一个新的M,或者全
37、局M列表中拿到一个空闲的M来服务这个Po五、调度器的生命周期,m,gMO1 .启动进程后的执行的第一个线程,也是编号为O的主线程。2 .在全局变量runtime.m中,不需要在heap上分配。3 .负责执行初始化操作和启动第一个G的4 .当启动第一个G之后,MO就和其它M一样了。GO每次启动一个M,都会立刻创建第一个goroutine,就是gg仅用于负责调度其它Gg本身不指向任何可指向的函数。每个M都会有一个自己的g0o在调度或系统调用时,会使用M切换到g来进行调度。MO的g放在全局空间六、Netpoller网络I/O轮询器,在操作系统提供的异步I/O基础组件之上,实现Go自己的阻塞式I/O而
38、编写的子程序。当一个G试图在一个网络连接上进行读写操作的时候,底层程序就会开始为此准备,这时这个G就会进入阻塞状态。一旦G准备就绪,就会让netpoller立即通知为此等待的G,返回给它相应的事件。因此,从netpoller处获取G的意思,就是获取那些已经接收到通知的G,此时这个G就可以进行网络读写操作了,调度器会让它进入GrUnnabie准备状态,并等待运行。七、M自旋在GMP中,M自旋是代表M的一种工作状态。M处于自旋状态,意味着M还没有找到要执行的Go这种状态需要循环持续监听是否有G出现在全局队列。如果M停止,就会退出自旋状态。一般情况下,调度器中至少要保证一个M处于自旋。如果发现调度器
39、中没有自旋M了,就会创建一个新的M来启动G0当创建或者恢复启动M时,M会自动进入自旋状态。Coroutine一、goroutine什么时候会发生阻塞?场景1:由于原子、互斥量或通道操作调用导致Goroutine阻塞,调度器将把当前阻塞的Goroutine场景2:由于网络请求和IO操作导致Goroutine阻塞,这种阻塞的情况下,我们的G和M又会怎么做呢?Go程序提供了网络轮询器(NetPoIIer)来处理网络请求和IO操作的问题,其后台通过kqueue(MacOS),epoll(Linux)gJciocp(Windows)来实现IO多路复用。场景3:当调用一些系统方法的时候,如果系统方法调用的
40、时候发生阻塞,这种情况下,网络轮询器(NetPoIIer)无法使用,而进行系统调用的Goroutine将阻塞当前Mo场景4:如果在Goroutine去执行一个sleep操作,导致M被阻塞了。场景5:Channel只写没有接收的时候会出现阻塞,代码层阻塞。系统调用阻塞当M因为系统调用而阻塞,G进入系统调用的时候,调度器会把该M和P分离开。这时,如果这个P的可运行G队列中还有未被运行的G,那么调度器就会找到一个空闲的M,或创建一个新的M,并与该P关联,以满足这些G的运行需要。因此M的数量一般都要比P多。而那个阻塞的G系统调用,会与运行它的M锁住,直到阻塞结束,才会释放锁。这时将M重新归入空闲的M队
41、列,等待下一次的调用。执行等待操作进入阻塞如果goroutine的go代码中包含对channel通道值得等待操作,那么在执行到对应代码得时候,这个G就会进入阻塞状态。需要等待从通道类型值中接收数据。此外,操作定时器,SIeeP函数也会造成G得等待。在事件到来之前一直会处于阻塞。直到事件到来之后,G才会被唤醒,并转换至等待运行得状态。二、gor。Utme有几种状态?自旋状态是什么?goroutine的状态就是G的状态,分为:初始化、等待运行、正在运行、系统调用、阻塞、闲置、栈移动(回收)状态。goroutine的自旋是M内核线程的自旋。在M的结构体中有一个spinning的字段,该字段用来表示M
42、是否正在寻找可运行的G(goroutine)0在寻找过程中M会处于自旋状态,直到找到G。三、每一个线程占用多少内存线程占用几KB四、如果1个goroutine一直占用着资源(出现阻塞),GMP模型怎么解决这个问题?如果一个G一直占用资源超过IOms就会进入抢占模式,抢占模式下会把这个G交给其它的P来运行。如果这个G在其它的P还是处于阻塞状态就会与M内核级线程锁住一直等待激活事件发生。其余的P正常运行不会阻塞的G。Channel一、介绍一下有缓冲channel和无缓冲1 .创建语法不同,有缓冲的Channel会带有一个缓冲容量。2 .无缓冲是同步读写,有写就必须有读,否则阻塞。3 .有缓冲的异步
43、读写,写与读可以分开,当缓冲区写满了,才会阻塞。如果缓冲区中的数据一直有人读取就不会阻塞。二、Channel的底层实现,channel的数据结构是什么?Channel的结构体中定义了有缓冲和无缓冲channel的实现字段。typehchanstructqcountuint队列数据总的数据数量dataqsizuint环形队列的数据大小bufer指向dataqsiz元素类型大小的数组elemsizeuintl6closeduint32elemtype*-typ/元素类型senduint/发送数据时的游标recvuint/接收数据时的游标recvqwaitq/接收而阻塞的等待队列sendqwaitq
44、/发送而阻塞的等待队列lockmutex/保护hchan所有字段的锁Channel的数据结构是一个环形队列,由qcount和elemsize分别指定了队列的容量和当前使用量。dataqsize是队列的大小。如果是有缓冲的ChanneI,则缓冲区是在HChan结构体中分配,如果是不带缓冲的ChanneI,环形队列的SiZe=0。recvq和sendq两个双向链表,recvq等待读channel,sendq等待写ChanneI。如果一个Channel发生阻塞,它就被挂在recvq或者sendq队列中。2-1写channel写channel是通过end函数1、锁定整个通道结构。2、确定写入。尝试re
45、cvq从等待队列中等待goroutine,然后将元素直接写入goroutineo3、如果ecvq为EmPty,则确定缓冲区是否可用。如果可用,从当前goroutine复制数据到缓冲区。4、如果缓冲区已满,则要写入的元素将保存在当前正在执行的goroutine的结构中,并且当前goroutine将在sendq中排队并从运行时挂起。5、写入完成释放锁。2-2读channel读channel是通过ecv1、先获取Channel全局锁2、尝试sendq从等待队列中获取等待的goroutine,3、如有等待的goroutine,没有缓冲区,取出goroutine并读取数据,然后唤醒这个goroutine
46、,结束读取释放锁。4、如有等待的goroutine,且有缓冲区(此时缓冲区已满),从缓冲区队首取出数据,再从Sendq取出一个goroutine,将goroutine中的数据存入buf队尾,结束读取释放锁。5、如没有等待的goroutine,且缓冲区有数据,直接读取缓冲区数据,结束读取释放锁。6、如没有等待的goroutine,且没有缓冲区或缓冲区为空,将当前的goroutine加入recvq排队,进入睡眠,等待被写goroutine唤醒。结束读取释放锁。三、channel是否是线程安全的从理论上来说channel的写入和读取都是原子性的操作,保证线程安全。但是看了代码之后发现,写入队列是加上一个互斥锁。所以线程安全肯定是保证的,但是还是要靠锁来确保原子性。