go语言单向链表 golang单向链表

【golang详解】go语言GMP(GPM)原理和调度Goroutine调度是一个很复杂的机制,下面尝试用简单的语言描述一下Goroutine调度机制,想要对其有更深入的了解可以去研读一下源码 。
首先介绍一下GMP什么意思:
G ----------- goroutine: 即Go协程 , 每个go关键字都会创建一个协程 。
M ---------- thread内核级线程 , 所有的G都要放在M上才能运行 。
P ----------- processor处理器,调度G到M上 , 其维护了一个队列 , 存储了所有需要它来调度的G 。
Goroutine 调度器P和 OS 调度器是通过 M 结合起来的,每个 M 都代表了 1 个内核线程 , OS 调度器负责把内核线程分配到 CPU 的核上执行
模型图:
避免频繁的创建、销毁线程,而是对线程的复用 。
1)work stealing机制
当本线程无可运行的G时,尝试从其他线程绑定的P偷取G,而不是销毁线程 。
2)hand off机制
当本线程M0因为G0进行系统调用阻塞时 , 线程释放绑定的P , 把P转移给其他空闲的线程执行 。进而某个空闲的M1获取P,继续执行P队列中剩下的G 。而M0由于陷入系统调用而进被阻塞,M1接替M0的工作,只要P不空闲,就可以保证充分利用CPU 。M1的来源有可能是M的缓存池,也可能是新建的 。当G0系统调用结束后,根据M0是否能获取到P,将会将G0做不同的处理:
如果有空闲的P , 则获取一个P,继续执行G0 。
如果没有空闲的P,则将G0放入全局队列 , 等待被其他的P调度 。然后M0将进入缓存池睡眠 。
如下图
GOMAXPROCS设置P的数量,最多有GOMAXPROCS个线程分布在多个CPU上同时运行
在Go中一个goroutine最多占用CPU 10ms,防止其他goroutine被饿死 。
具体可以去看另一篇文章
【Golang详解】go语言调度机制 抢占式调度
当创建一个新的G之后优先加入本地队列,如果本地队列满了 , 会将本地队列的G移动到全局队列里面,当M执行work stealing从其他P偷不到G时,它可以从全局G队列获取G 。
协程经历过程
我们创建一个协程 go func()经历过程如下图:
说明:
这里有两个存储G的队列 , 一个是局部调度器P的本地队列、一个是全局G队列 。新创建的G会先保存在P的本地队列中,如果P的本地队列已经满了就会保存在全局的队列中;处理器本地队列是一个使用数组构成的环形链表,它最多可以存储 256 个待执行任务 。
G只能运行在M中,一个M必须持有一个P,M与P是1:1的关系 。M会从P的本地队列弹出一个可执行状态的G来执行,如果P的本地队列为空,就会想其他的MP组合偷取一个可执行的G来执行;
一个M调度G执行的过程是一个循环机制;会一直从本地队列或全局队列中获取G
上面说到P的个数默认等于CPU核数,每个M必须持有一个P才可以执行G,一般情况下M的个数会略大于P的个数,这多出来的M将会在G产生系统调用时发挥作用 。类似线程池,Go也提供一个M的池子 , 需要时从池子中获?。?用完放回池子,不够用时就再创建一个 。
work-stealing调度算法:当M执行完了当前P的本地队列队列里的所有G后,P也不会就这么在那躺尸啥都不干,它会先尝试从全局队列队列寻找G来执行,如果全局队列为空,它会随机挑选另外一个P,从它的队列里中拿走一半的G到自己的队列中执行 。
如果一切正常 , 调度器会以上述的那种方式顺畅地运行,但这个世界没这么美好,总有意外发生,以下分析goroutine在两种例外情况下的行为 。
Go runtime会在下面的goroutine被阻塞的情况下运行另外一个goroutine:
用户态阻塞/唤醒
当goroutine因为channel操作或者network I/O而阻塞时(实际上golang已经用netpoller实现了goroutine网络I/O阻塞不会导致M被阻塞 , 仅阻塞G,这里仅仅是举个栗子),对应的G会被放置到某个wait队列(如channel的waitq),该G的状态由_Gruning变为_Gwaitting , 而M会跳过该G尝试获取并执行下一个G,如果此时没有可运行的G供M运行,那么M将解绑P,并进入sleep状态;当阻塞的G被另一端的G2唤醒时(比如channel的可读/写通知),G被标记为 , 尝试加入G2所在P的runnext(runnext是线程下一个需要执行的 Goroutine 。) , 然后再是P的本地队列和全局队列 。
系统调用阻塞
当M执行某一个G时候如果发生了阻塞操作 , M会阻塞,如果当前有一些G在执行,调度器会把这个线程M从P中摘除 , 然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务于这个P 。当M系统调用结束时候 , 这个G会尝试获取一个空闲的P执行 , 并放入到这个P的本地队列 。如果获取不到P,那么这个线程M变成休眠状态 , 加入到空闲线程中,然后这个G会被放入全局队列中 。
队列轮转
可见每个P维护着一个包含G的队列,不考虑G进入系统调用或IO操作的情况下,P周期性的将G调度到M中执行,执行一小段时间,将上下文保存下来,然后将G放到队列尾部,然后从队列中重新取出一个G进行调度 。
除了每个P维护的G队列以外 , 还有一个全局的队列,每个P会周期性地查看全局队列中是否有G待运行并将其调度到M中执行,全局队列中G的来源,主要有从系统调用中恢复的G 。之所以P会周期性地查看全局队列,也是为了防止全局队列中的G被饿死 。
除了每个P维护的G队列以外,还有一个全局的队列 , 每个P会周期性地查看全局队列中是否有G待运行并将其调度到M中执行,全局队列中G的来源,主要有从系统调用中恢复的G 。之所以P会周期性地查看全局队列,也是为了防止全局队列中的G被饿死 。
M0
M0是启动程序后的编号为0的主线程,这个M对应的实例会在全局变量rutime.m0中,不需要在heap上分配,M0负责执行初始化操作和启动第一个G,在之后M0就和其他的M一样了
G0
G0是每次启动一个M都会第一个创建的goroutine,G0仅用于负责调度G,G0不指向任何可执行的函数 , 每个M都会有一个自己的G0,在调度或系统调用时会使用G0的栈空间,全局变量的G0是M0的G0
一个G由于调度被中断,此后如何恢复?
中断的时候将寄存器里的栈信息,保存到自己的G对象里面 。当再次轮到自己执行时,将自己保存的栈信息复制到寄存器里面 , 这样就接着上次之后运行了 。
我这里只是根据自己的理解进行了简单的介绍,想要详细了解有关GMP的底层原理可以去看Go调度器 G-P-M 模型的设计者的文档或直接看源码
参考:()
()
Go语言设计与实现(上)基本设计思路:
类型转换、类型断言、动态派发 。ifacego语言单向链表 , eface 。
反射对象具有go语言单向链表的方法:
编译优化:
内部实现:
实现 Context 接口有以下几个类型(空实现就忽略了):
互斥锁的控制逻辑:
设计思路:
(以上为写被读阻塞go语言单向链表,下面是读被写阻塞)
总结,读写锁的设计还是非常巧妙的:
设计思路:
WaitGroup 有三个暴露的函数:
部件:
设计思路:
结构:
Once 只暴露了一个方法:
实现:
三个关键点:
细节:
让多协程任务的开始执行时间可控(按顺序或归一) 。(Context 是控制结束时间)
设计思路: 通过一个锁和内置的 notifyList 队列实现 , Wait() 会生成票据,并将等待协程信息加入链表中,等待控制协程中发送信号通知一个(Signal())或所有(Boardcast())等待者(内部实现是通过票据通知的)来控制协程解除阻塞 。
暴露四个函数:
实现细节:
部件:
包: golang.org/x/sync/errgroup
作用:开启func() error函数签名的协程,在同 Group 下协程并发执行过程并收集首次 err 错误 。通过 Context 的传入 , 还可以控制在首次 err 出现时就终止组内各协程 。
设计思路:
结构:
暴露的方法:
实现细节:
注意问题:
包: "golang.org/x/sync/semaphore"
作用:排队借资源(如钱,有借有还)的一种场景 。此包相当于对底层信号量的一种暴露 。
设计思路:有一定数量的资源 Weight,每一个 waiter 携带一个 channel 和要借的数量 n 。通过队列排队执行借贷 。
结构:
暴露方法:
细节:
部件:
细节:
包: "golang.org/x/sync/singleflight"
作用:防击穿 。瞬时的相同请求只调用一次,response 被所有相同请求共享 。
设计思路:按请求的 key 分组(一个 *call 是一个组,用 map 映射存储组),每个组只进行一次访问,组内每个协程会获得对应结果的一个拷贝 。
结构:
逻辑:
细节:
部件:
如有错误 , 请批评指正 。
go面试题整理(附带部分自己的解答)原文:【】
如果有解答的不对的,麻烦各位在评论写出来~
go的调度原理是基于GMP模型,G代表一个goroutine,不限制数量;M=machine,代表一个线程,最大1万,所有G任务还是在M上执行;P=processor代表一个处理器,每一个允许的M都会绑定一个G,默认与逻辑CPU数量相等(通过runtime.GOMAXPROCS(runtime.NumCPU())设置) 。
go调用过程:
可以能,也可以不能 。
因为go存在不能使用==判断类型:map、slice,如果struct包含这些类型的字段,则不能比较 。
这两种类型也不能作为map的key 。
类似栈操作,后进先出 。
因为go的return是一个非原子性操作 , 比如语句return i,实际上分两步进行,即将i值存入栈中作为返回值 , 然后执行跳转,而defer的执行时机正是跳转前,所以说defer执行时还是有机会操作返回值的 。
select的case的表达式必须是一个channel类型 , 所有case都会被求值,求值顺序自上而下 , 从左至右 。如果多个case可以完成,则会随机执行一个case,如果有default分支 , 则执行default分支语句 。如果连default都没有,则select语句会一直阻塞,直到至少有一个IO操作可以进行 。
break关键字可跳出select的执行 。
goroutine管理、信息传递 。context的意思是上下文 , 在线程、协程中都有这个概念,它指的是程序单元的一个运行状态、现场、快照,包含 。context在多个goroutine中是并发安全的 。
应用场景:
例子参考:
waitgroup
channel
len:切片的长度,访问时间复杂度为O(1),go的slice底层是对数组的引用 。
cap:切片的容量,扩容是以这个值为标准 。默认扩容是2倍,当达到1024的长度后,按1.25倍 。
扩容:每次扩容slice底层都将先分配新的容量的内存空间,再将老的数组拷贝到新的内存空间,因为这个操作不是并发安全的 。所以并发进行append操作,读到内存中的老数组可能为同一个,最终导致append的数据丢失 。
共享:slice的底层是对数组的引用,因此如果两个切片引用了同一个数组片段,就会形成共享底层数组 。当sliec发生内存的重新分配(如扩容)时,会对共享进行隔断 。详细见下面例子:
make([]Type,len,cap)
map的底层是hash table(hmap类型) , 对key值进行了hash,并将结果的低八位用于确定key/value存在于哪个bucket(bmap类型) 。再将高八位与bucket的tophash进行依次比较 , 确定是否存在 。出现hash冲撞时,会通过bucket的overflow指向另一个bucket,形成一个单向链表 。每个bucket存储8个键值对 。
如果要实现map的顺序读?。?需要使用一个slice来存储map的key并按照顺序进行排序 。
利用map,如果要求并发安全,就用sync.map
要注意下set中的delete函数需要使用delete(map) 来实现 , 但是这个并不会释放内存,除非value也是一个子map 。当进行多次delete后 , 可以使用make来重建map 。
使用sync.Map来管理topic,用channel来做队列 。
参考:
多路归并法:
pre class="vditor-reset" placeholder="" contenteditable="true" spellcheck="false"p data-block="0"(1)假设有K路a href=""数据流/a , 流内部是有序的,且流间同为升序或降序;
/pp data-block="0"(2)首先读取每个流的第一个数,如果已经EOF,pass;
/pp data-block="0"(3)将有效的k(k可能小于K)个数比较,选出最小的那路mink,输出 , 读取mink的下一个;
/pp data-block="0"(4)直到所有K路都EOF 。
/p/pre
假设文件又1个G,内存只有256M,无法将1个G的文件全部读到内存进行排序 。
第一步:
可以分为10段读取,每段读取100M的数据并排序好写入硬盘 。
假设写入后的文件为A,B,C...10
第二步:
将A , B,C...10的第一个字符拿出来,对这10个字符进行排序,并将结果写入硬盘,同时记录被写入的字符的文件指针P 。
第三步:
将刚刚排序好的9个字符再加上从指针P读取到的P 1位数据进行排序,并写入硬盘 。
重复二、三步骤 。
go文件读写参考:
保证排序前两个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同的排序叫稳定排序 。
快速排序、希尔排序、堆排序、直接选择排序不是稳定的排序算法 。
基数排序、冒泡排序、直接插入排序、折半插入排序、归并排序是稳定的排序算法 。
参考:
head只请求页面的首部 。多用来判断网页是否被修改和超链接的有效性 。
get请求页面信息,并返回实例的主体 。
参考:
401:未授权的访问 。
403: 拒绝访问 。
普通的http连接是客户端连接上服务端,然后结束请求后,由客户端或者服务端进行http连接的关闭 。下次再发送请求的时候,客户端再发起一个连接,传送数据,关闭连接 。这么个流程反复 。但是一旦客户端发送connection:keep-alive头给服务端,且服务端也接受这个keep-alive的话,两边对上暗号,这个连接就可以复用了 , 一个http处理完之后,另外一个http数据直接从这个连接走了 。减少新建和断开TCP连接的消耗 。这个可以在Nginx设置 ,
这个keepalive_timout时间值意味着:一个http产生的tcp连接在传送完最后一个响应后,还需要hold住keepalive_timeout秒后,才开始关闭这个连接 。
特别注意TCP层的keep alive和http不是一个意思 。TCP的是指:tcp连接建立后 , 如果客户端很长一段时间不发送消息,当连接很久没有收到报文,tcp会主动发送一个为空的报文(侦测包)给对方,如果对方收到了并且回复了,证明对方还在 。如果对方没有报文返回,重试多次之后则确认连接丢失 , 断开连接 。
tcp的keep alive可通过
net.ipv4.tcp_keepalive_intvl = 75// 当探测没有确认时,重新发送探测的频度 。缺省是75秒 。
net.ipv4.tcp_keepalive_probes = 9 //在认定连接失效之前,发送多少个TCP的keepalive探测包 。缺省值是9 。这个值乘以tcp_keepalive_intvl之后决定了,一个连接发送了keepalive之后可以有多少时间没有回应
net.ipv4.tcp_keepalive_time = 7200 //当keepalive起用的时候,TCP发送keepalive消息的频度 。缺省是2小时 。一般设置为30分钟1800
修改:
可以
tcp是面向连接的,upd是无连接状态的 。
udp相比tcp没有建立连接的过程,所以更快 , 同时也更安全,不容易被攻击 。upd没有阻塞控制,因此出现网络阻塞不会使源主机的发送效率降低 。upd支持一对多,多对多等,tcp是点对点传输 。tcp首部开销20字节 , udp8字节 。
udp使用场景:视频通话、im聊天等 。
time-wait表示客户端等待服务端返回关闭信息的状态 , closed_wait表示服务端得知客户端想要关闭连接 , 进入半关闭状态并返回一段TCP报文 。
time-wait作用:
解决办法:
close_wait:
被动关闭 , 通常是由于客户端忘记关闭tcp连接导致 。
根据业务来啊~
重要指标是cardinality(不重复数量),这个数量/总行数如果过?。ㄇ鹘?)代表索引基本没意义,比如sex性别这种 。
另外查询不要使用select *,根据select的条件 where条件做组合索引 , 尽量实现覆盖索引,避免回表 。
僵尸进程:
即子进程先于父进程退出后 , 子进程的PCB需要其父进程释放 , 但是父进程并没有释放子进程的PCB,这样的子进程就称为僵尸进程,僵尸进程实际上是一个已经死掉的进程 。
孤儿进程:
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程 。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作 。
子进程死亡需要父进程来处理,那么意味着正常的进程应该是子进程先于父进程死亡 。当父进程先于子进程死亡时,子进程死亡时没父进程处理,这个死亡的子进程就是孤儿进程 。
但孤儿进程与僵尸进程不同的是,由于父进程已经死亡,系统会帮助父进程回收处理孤儿进程 。所以孤儿进程实际上是不占用资源的,因为它终究是被系统回收了 。不会像僵尸进程那样占用ID,损害运行系统 。
原文链接:
产生死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个进程使用 。
(2) 请求与保持条件:一个进程因请求资源而阻塞时 , 对已获得的资源保持不放 。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺 。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系 。
避免方法:
端口占用:lsof -i:端口号或者 nestat
cpu、内存占用:top
发送信号:kill -l 列出所有信号,然后用 kill [信号变化] [进程号]来执行 。如kill -9 453 。强制杀死453进程
git log:查看提交记录
git diff :查看变更记录
git merge:目标分支改变,而源分支保持原样 。优点:保留提交历史,保留分支结构 。但会有大量的merge记录
git rebase:将修改拼接到最新 , 复杂的记录变得优雅,单个操作变得(revert)很简单;缺点:
git revert:反做指定版本,会新生成一个版本
git reset:重置到某个版本,中间版本全部丢失
etcd、Consul
pprof
节省空间(非叶子节点不存储数据,相对b tree的优势),减少I/O次数(节省的空间全部存指针地址,让树变的矮胖),范围查找方便(相对hash的优势) 。
explain
其他的见:
runtime2.go 中关于 p 的定义: 其中 runnext 指针决定了下一个要运行的 g,根据英文的注释大致意思是说:
所以当设置 runtime.GOMAXPROCS(1) 时,此时只有一个 P,创建的 g 依次加入 P, 当最后一个即 i==9 时,加入的最后 一个 g 将会继承当前主 goroutinue 的剩余时间片继续执行,所以会先输出 9,之后再依次执行 P 队列中其它的 g 。
方法一:
方法二:
[图片上传失败...(image-4ef445-1594976286098)]
方法1:to_days,返回给的日期从0开始算的天数 。
方法2:data_add 。向日期添加指定时间间隔
[图片上传失败...(image-b67b10-1594976286098)]
Go语言list(列表)2021-11-10
列表是一种非连续的存储容器,有多个节点组成,节点通过一些变量记录彼此之间的关系
单链表和双链表就是列表的两种方法 。
原理:A、B、C三个人,B懂A的电话 , C懂B的电话只是单方知道号码,这样就形成了一个单链表结构 。
如果C把自己的号码给B,B把自己的号码给A,因为是双方都知道对方的号码,这样就形成了一个双链表结构
如果B换号码了,他需要通知AC,把自己的号码删了,这个过程就是列表的删除操作 。
在Go语言中,列表使用 container/list 包来实现,内部的实现原理是双链表,列表能够高效地进行任意位置的元素插入和删除操作 。
列表初始化的两种办法
列表没有给出具体的元素类型的限制,所以列表的元素可以是任意类型的 ,
例如给列表中放入了一个 interface{} 类型的值 , 取出值后,如果要将 interface{} 转换为其他类型将会发生宕机 。
双链表支持从队列前方或后方插入元素,分别对应的方法是 PushFront 和 PushBack 。
列表插入函数的返回值会提供一个 *list.Element 结构 , 这个结构记录着列表元素的值以及与其他节点之间的关系等信息,从列表中删除元素时,需要用到这个结构进行快速删除 。
遍历完也能看到最后的结果
学习地址:
怎么判断一个单向链表是否有回环1, 最简单的方法, 用一个指针遍历链表, 每遇到一个节点就把他的内存地址(java中可以用object.hashcode())做为key放在一个hashtable中. 这样当hashtable中出现重复key的时候说明此链表上有环. 这个方法的时间复杂度为O(n), 空间同样为O(n).
2, 使用反转指针的方法, 每过一个节点就把该节点的指针反向:
Boolean reverse(Node *head) {
Node *curr = head;
Node *next = head-next;
curr-next = NULL;
while(next!=NULL) {
if(next == head) { /* go back to the head of the list, so there is a loop */
next-next = curr;
return TRUE;
}
Node *temp = curr;
curr = next;
next = next-next;
curr-next = temp;
}
/* at the end of list, so there is no loop, let's reverse the list back */
next = curr-next;
curr -next = NULL;
while(next!=NULL) {
Node *temp = curr;
curr = next;
next = next-next;
curr-next = temp;
}
return FALSE;
}
看上去这是一种奇怪的方法: 当有环的时候反转next指针会最终走到链表头部; 当没有环的时候反转next指针会破坏链表结构(使链表反向), 所以需要最后把链表再反向一次. 这种方法的空间复杂度是O(1), 实事上我们使用了3个额外指针;而时间复杂度是O(n), 我们最多2次遍历整个链表(当链表中没有环的时候).
这个方法的最大缺点是在多线程情况下不安全, 当多个线程都在读这个链表的时候, 检查环的线程会改变链表的状态, 虽然最后我们恢复了链表本身的结构, 但是不能保证其他线程能得到正确的结果.
3, 这是一般面试官所预期的答案: 快指针和慢指针
Boolean has_loop(Node *head) {
Node *pf = head; /* fast pointer */
Node *ps = head; /* slow pointer */
while(true) {
if(pfpf-next)
pf = pf-next-next;
else
return FALSE;
ps = ps-next;
if(ps == pf)
return TRUE;
}
}
需要说明的是, 当慢指针(ps)进入环之后, 最多会走n-1步就能和快指针(pf)相遇, 其中n是环的长度. 也就是说快指针在环能不会跳过慢指针, 这个性质可以简单的用归纳法来证明. (1)当ps在环中位置i, 而pf在环中位置i-1, 则在下一个iteration, ps会和pf在i 1相遇.
(2)当ps在环中位置i, 而pf在环中位置i-2, 则在下一个iteration, ps在i 1, pf在i, 于是在下一个iteration ps和pf会相遇在i 2位置
(3)和上面推理过程类似, 当ps在i, pf在i 1, 则他们会经过n-1个iteration在i n-1的位置相遇. 于是慢指针的步数不会超过n-1.
扩展:
这个问题还有一些扩展, 例如, 如何找到环的开始节点? 如何解开这个环? 这些问题的本质就是如何找到有"回边"的那个节点.
我们可以利用方法3的一个变形来解决这个问题:
Boolean has_loop(Node *head) {
Node *pf = head; /* fast pointer */
Node *ps = head; /* slow pointer */
while(true) {/* step 1, is there a loop? */
if(pfpf-next)
pf = pf-next-next;
else
return FALSE;
ps = ps-next;
if(ps == pf)
break;
}
/* step 2, how long is the loop */
int i = 0;
do {
ps = ps-next;
pf = pf-next-next;
i;
} while(ps!=pf)
/* step 3, use 2 addtional pointers with distance i to break the loop */
ps = head;
pf = head;
int j;
for(j=0; ji; j) { pf = pf-next; }
j = 0;
while(ps!=pf) {
ps = ps-next;
pf = pf-next;
j;
}
【go语言单向链表 golang单向链表】printf("loop begins at position %d, node address is %x", j, ps);
/*step 4, break the loop*/
for(j=0; j=i; j) { ps = ps-next; } //step i-1 in the loop from loop beginning
ps-next = NULL; // break the loop
return TRUE;
go语言单向链表的介绍就聊到这里吧,感谢你花时间阅读本站内容,更多关于golang单向链表、go语言单向链表的信息别忘了在本站进行查找喔 。

    推荐阅读