
正文
go语言多线程遍历目录 go语言多线程怎么做的
提示:扫一扫查出行【扫一扫了解最新限行尾号】
复制提示
golang多线程简单逻辑
实现指定个核心最大化使用,比如核心总数减一。
必要的库。
要使用的cpu数量,建议不全使用。
建立管道。
声明使用的cpu数。
建立互斥关系,本例中主要为了实现所有线程执行完后再执行后续程序。
创建cpu数减1个线程
后面每个任务结束时要done一个wg,这里根据具体情况加,是循环就在每个循环里加,保证后面能全部done即可
没有缓冲的、阻塞式的往管道传递字符串。
Wait是等所有线程都执行完,即增加的数字被全done掉。
关闭管道。
假设已有的函数是ReadLogs,在它的基础上加个Wg加函数名的新函数,我觉得这种方式不改变原有的,比较舒服。
大意是:循环从管道读取字符串,读不到了就跳出循环。
每个ReadLogs()之后加一个wg.Done(),相当于计数减一。
ReadLogs()就是要执行的任务,不再解释。
就是开指定个线程。
管道阻塞传值。
wg同步。
WgReadLogs循环接收。
相关问答
Q1: 彻底理解Golang Map
本文目录如下,阅读本文后,将一网打尽下面Golang Map相关面试题
Go中的map是一个指针,占用8个字节,指向hmap结构体; 源码 src/runtime/map.go 中可以看到map的底层结构
每个map的底层结构是hmap,hmap包含若干个结构为bmap的bucket数组。每个bucket底层都采用链表结构。接下来,我们来详细看下map的结构
bmap 就是我们常说的“桶”,一个桶里面会最多装 8 个 key,这些 key 之所以会落入同一个桶,是因为它们经过哈希计算后,哈希结果是“一类”的,关于key的定位我们在map的查询和插入中详细说明。在桶内,又会根据 key 计算出来的 hash 值的高 8 位来决定 key 到底落入桶内的哪个位置(一个桶内最多有8个位置)。
bucket内存数据结构可视化如下:
注意到 key 和 value 是各自放在一起的,并不是 key/value/key/value/... 这样的形式。源码里说明这样的好处是在某些情况下可以省略掉 padding字段,节省内存空间。
当 map 的 key 和 value 都不是指针,并且 size 都小于 128 字节的情况下,会把 bmap 标记为不含指针,这样可以避免 gc 时扫描整个 hmap。但是,我们看 bmap 其实有一个 overflow 的字段,是指针类型的,破坏了 bmap 不含指针的设想,这时会把 overflow 移动到 extra 字段来。
map是个指针,底层指向hmap,所以是个引用类型
golang 有三个常用的高级类型 slice 、map、channel, 它们都是 引用类型 ,当引用类型作为函数参数时,可能会修改原内容数据。
golang 中没有引用传递,只有值和指针传递。所以 map 作为函数实参传递时本质上也是值传递,只不过因为 map 底层数据结构是通过指针指向实际的元素存储空间,在被调函数中修改 map,对调用者同样可见,所以 map 作为函数实参传递时表现出了引用传递的效果。
因此,传递 map 时,如果想修改map的内容而不是map本身,函数形参无需使用指针
map 底层数据结构是通过指针指向实际的元素 存储空间 ,这种情况下,对其中一个map的更改,会影响到其他map
map 在没有被修改的情况下,使用 range 多次遍历 map 时输出的 key 和 value 的顺序可能不同。这是 Go 语言的设计者们有意为之,在每次 range 时的顺序被随机化,旨在提示开发者们,Go 底层实现并不保证 map 遍历顺序稳定,请大家不要依赖 range 遍历结果顺序。
map 本身是无序的,且遍历时顺序还会被随机化,如果想顺序遍历 map,需要对 map key 先排序,再按照 key 的顺序遍历 map。
map默认是并发不安全的,原因如下:
Go 官方在经过了长时间的讨论后,认为 Go map 更应适配典型使用场景(不需要从多个 goroutine 中进行安全访问),而不是为了小部分情况(并发访问),导致大部分程序付出加锁代价(性能),决定了不支持。
场景: 2个协程同时读和写,以下程序会出现致命错误:fatal error: concurrent map writes
如果想实现map线程安全,有两种方式:
方式一:使用读写锁 map + sync.RWMutex
方式二:使用golang提供的 sync.Map
sync.map是用读写分离实现的,其思想是空间换时间。和map+RWLock的实现方式相比,它做了一些优化:可以无锁访问read map,而且会优先操作read map,倘若只操作read map就可以满足要求(增删改查遍历),那就不用去操作write map(它的读写都要加锁),所以在某些特定场景中它发生锁竞争的频率会远远小于map+RWLock的实现方式。
golang中map是一个kv对集合。底层使用hash table,用链表来解决冲突 ,出现冲突时,不是每一个key都申请一个结构通过链表串起来,而是以bmap为最小粒度挂载,一个bmap可以放8个kv。在哈希函数的选择上,会在程序启动时,检测 cpu 是否支持 aes,如果支持,则使用 aes hash,否则使用 memhash。
map有3钟初始化方式,一般通过make方式创建
map的创建通过生成汇编码可以知道,make创建map时调用的底层函数是 runtime.makemap 。如果你的map初始容量小于等于8会发现走的是 runtime.fastrand 是因为容量小于8时不需要生成多个桶,一个桶的容量就可以满足
makemap函数会通过 fastrand 创建一个随机的哈希种子,然后根据传入的 hint 计算出需要的最小需要的桶的数量,最后再使用 makeBucketArray 创建用于保存桶的数组,这个方法其实就是根据传入的 B 计算出的需要创建的桶数量在内存中分配一片连续的空间用于存储数据,在创建桶的过程中还会额外创建一些用于保存溢出数据的桶,数量是 2^(B-4) 个。初始化完成返回hmap指针。
找到一个 B,使得 map 的装载因子在正常范围内
Go 语言中读取 map 有两种语法:带 comma 和 不带 comma。当要查询的 key 不在 map 里,带 comma 的用法会返回一个 bool 型变量提示 key 是否在 map 中;而不带 comma 的语句则会返回一个 value 类型的零值。如果 value 是 int 型就会返回 0,如果 value 是 string 类型,就会返回空字符串。
map的查找通过生成汇编码可以知道,根据 key 的不同类型,编译器会将查找函数用更具体的函数替换,以优化效率:
函数首先会检查 map 的标志位 flags。如果 flags 的写标志位此时被置 1 了,说明有其他协程在执行“写”操作,进而导致程序 panic。这也说明了 map 对协程是不安全的。
key经过哈希函数计算后,得到的哈希值如下(主流64位机下共 64 个 bit 位):
m: 桶的个数
从buckets 通过 hash m 得到对应的bucket,如果bucket正在扩容,并且没有扩容完成,则从oldbuckets得到对应的bucket
计算hash所在桶编号:
用上一步哈希值最后的 5 个 bit 位,也就是 01010 ,值为 10,也就是 10 号桶(范围是0~31号桶)
计算hash所在的槽位:
用上一步哈希值哈希值的高8个bit 位,也就是 10010111 ,转化为十进制,也就是151,在 10 号 bucket 中寻找** tophash 值(HOB hash)为 151* 的 槽位**,即为key所在位置,找到了 2 号槽位,这样整个查找过程就结束了。
如果在 bucket 中没找到,并且 overflow 不为空,还要继续去 overflow bucket 中寻找,直到找到或是所有的 key 槽位都找遍了,包括所有的 overflow bucket。
通过上面找到了对应的槽位,这里我们再详细分析下key/value值是如何获取的:
bucket 里 key 的起始地址就是 unsafe.Pointer(b)+dataOffset。第 i 个 key 的地址就要在此基础上跨过 i 个 key 的大小;而我们又知道,value 的地址是在所有 key 之后,因此第 i 个 value 的地址还需要加上所有 key 的偏移。
通过汇编语言可以看到,向 map 中插入或者修改 key,最终调用的是 mapassign 函数。
实际上插入或修改 key 的语法是一样的,只不过前者操作的 key 在 map 中不存在,而后者操作的 key 存在 map 中。
mapassign 有一个系列的函数,根据 key 类型的不同,编译器会将其优化为相应的“快速函数”。
我们只用研究最一般的赋值函数 mapassign 。
map的赋值会附带着map的扩容和迁移,map的扩容只是将底层数组扩大了一倍,并没有进行数据的转移,数据的转移是在扩容后逐步进行的,在迁移的过程中每进行一次赋值(access或者delete)会至少做一次迁移工作。
1.判断map是否为nil
每一次进行赋值/删除操作时,只要oldbuckets != nil 则认为正在扩容,会做一次迁移工作,下面会详细说下迁移过程
根据上面查找过程,查找key所在位置,如果找到则更新,没找到则找空位插入即可
经过前面迭代寻找动作,若没有找到可插入的位置,意味着需要扩容进行插入,下面会详细说下扩容过程
通过汇编语言可以看到,向 map 中删除 key,最终调用的是 mapdelete 函数
删除的逻辑相对比较简单,大多函数在赋值操作中已经用到过,核心还是找到 key 的具体位置。寻找过程都是类似的,在 bucket 中挨个 cell 寻找。找到对应位置后,对 key 或者 value 进行“清零”操作,将 count 值减 1,将对应位置的 tophash 值置成 Empty
再来说触发 map 扩容的时机:在向 map 插入新 key 的时候,会进行条件检测,符合下面这 2 个条件,就会触发扩容:
1、装载因子超过阈值
源码里定义的阈值是 6.5 (loadFactorNum/loadFactorDen),是经过测试后取出的一个比较合理的因子
我们知道,每个 bucket 有 8 个空位,在没有溢出,且所有的桶都装满了的情况下,装载因子算出来的结果是 8。因此当装载因子超过 6.5 时,表明很多 bucket 都快要装满了,查找效率和插入效率都变低了。在这个时候进行扩容是有必要的。
对于条件 1,元素太多,而 bucket 数量太少,很简单:将 B 加 1,bucket 最大数量( 2^B )直接变成原来 bucket 数量的 2 倍。于是,就有新老 bucket 了。注意,这时候元素都在老 bucket 里,还没迁移到新的 bucket 来。新 bucket 只是最大数量变为原来最大数量的 2 倍( 2^B * 2 ) 。
2、overflow 的 bucket 数量过多
在装载因子比较小的情况下,这时候 map 的查找和插入效率也很低,而第 1 点识别不出来这种情况。表面现象就是计算装载因子的分子比较小,即 map 里元素总数少,但是 bucket 数量多(真实分配的 bucket 数量多,包括大量的 overflow bucket)
不难想像造成这种情况的原因:不停地插入、删除元素。先插入很多元素,导致创建了很多 bucket,但是装载因子达不到第 1 点的临界值,未触发扩容来缓解这种情况。之后,删除元素降低元素总数量,再插入很多元素,导致创建很多的 overflow bucket,但就是不会触发第 1 点的规定,你能拿我怎么办?overflow bucket 数量太多,导致 key 会很分散,查找插入效率低得吓人,因此出台第 2 点规定。这就像是一座空城,房子很多,但是住户很少,都分散了,找起人来很困难
对于条件 2,其实元素没那么多,但是 overflow bucket 数特别多,说明很多 bucket 都没装满。解决办法就是开辟一个新 bucket 空间,将老 bucket 中的元素移动到新 bucket,使得同一个 bucket 中的 key 排列地更紧密。这样,原来,在 overflow bucket 中的 key 可以移动到 bucket 中来。结果是节省空间,提高 bucket 利用率,map 的查找和插入效率自然就会提升。
由于 map 扩容需要将原有的 key/value 重新搬迁到新的内存地址,如果有大量的 key/value 需要搬迁,会非常影响性能。因此 Go map 的扩容采取了一种称为“渐进式”的方式,原有的 key 并不会一次性搬迁完毕,每次最多只会搬迁 2 个 bucket。
上面说的 hashGrow() 函数实际上并没有真正地“搬迁”,它只是分配好了新的 buckets,并将老的 buckets 挂到了 oldbuckets 字段上。真正搬迁 buckets 的动作在 growWork() 函数中,而调用 growWork() 函数的动作是在 mapassign 和 mapdelete 函数中。也就是插入或修改、删除 key 的时候,都会尝试进行搬迁 buckets 的工作。先检查 oldbuckets 是否搬迁完毕,具体来说就是检查 oldbuckets 是否为 nil。
如果未迁移完毕,赋值/删除的时候,扩容完毕后(预分配内存),不会马上就进行迁移。而是采取 增量扩容 的方式,当有访问到具体 bukcet 时,才会逐渐的进行迁移(将 oldbucket 迁移到 bucket)
nevacuate 标识的是当前的进度,如果都搬迁完,应该和2^B的长度是一样的
在evacuate 方法实现是把这个位置对应的bucket,以及其冲突链上的数据都转移到新的buckets上。
转移的判断直接通过tophash 就可以,判断tophash中第一个hash值即可
遍历的过程,就是按顺序遍历 bucket,同时按顺序遍历 bucket 中的 key。
map遍历是无序的,如果想实现有序遍历,可以先对key进行排序
为什么遍历 map 是无序的?
如果发生过迁移,key 的位置发生了重大的变化,有些 key 飞上高枝,有些 key 则原地不动。这样,遍历 map 的结果就不可能按原来的顺序了。
如果就一个写死的 map,不会向 map 进行插入删除的操作,按理说每次遍历这样的 map 都会返回一个固定顺序的 key/value 序列吧。但是 Go 杜绝了这种做法,因为这样会给新手程序员带来误解,以为这是一定会发生的事情,在某些情况下,可能会酿成大错。
Go 做得更绝,当我们在遍历 map 时,并不是固定地从 0 号 bucket 开始遍历,每次都是从一个**随机值序号的 bucket 开始遍历,并且是从这个 bucket 的一个 随机序号的 cell **开始遍历。这样,即使你是一个写死的 map,仅仅只是遍历它,也不太可能会返回一个固定序列的 key/value 对了。
Q2: Golang的调度模型
Go有四大核心模块,基本全部体现在runtime,有调度系统、GC、goroutine、channel,那么深入理解其中的精髓可以帮助我们理解Go这一门语言!
参考: 调度系统设计精要
下面是我用Go语言简单写的一个调度器,大家可以看看设计思路,以及存在的问题!
1、测试条件,调度器只启动两个线程,然后一个线程主要是负责循环的添加任务,一个线程循环的去执行任务
2、测试条件,调度器启动三个线程,然后两个线程去执行任务,一个添加任务
3、继续测试,启动十个线程,一个添加任务,九个执行任务
4、我们添加一些阻塞的任务
执行可以看到完全不可用
1、 可以看到随着M的不断的增加,可以发现执行任务的数量也不断的减少,原因是什么呢?有兴趣的同学可以加一个pprof可以看看,其实大量的在等待锁的过程!
2、如果我的M运行了类似于Sleep操作的方法如何解决了,我的调度器还能支撑这个量级的调度吗?
关于pprof如何使用:在代码头部加一个这个代码:
我们查看一下 go tool pprof main/prof.pporf
可以看到真正执行代码的时间只有 0.17s + 0.02s 其他时间都被阻塞掉了!
1、GM模型中的所有G都是放入到一个queue,那么导致所有的M取执行任务时都会去竞争锁,我们插入G也会去竞争锁,所以解决这种问题一般就是减少对单一资源的竞争,那就是桶化,其实就是每个线程都分配一个队列
2、GM模型中没有任务状态,只有runnable,假如任务遇到阻塞,完全可以把任务挂起再唤醒
这里其实会遇到一个问题,假如要分配很多个线程,那么此时随着线程的增加,也会造成队列的增加,其实也会造成调度器的压力,因为它需要遍历全部线程的队列去分配任务以及后续会讲到的窃取任务!
因为我们知道CPU的最大并行度其实取决于CPU的核数,也就是我们没必要为每个线程都去分配一个队列,因为就算是给他们分配了,他们自己去那执行调度,其实也会出现大量阻塞,原因就是CPU调度不过来这些线程!
Go里面是只分配了CPU个数的队列,这里就是P这个概念,你可以理解为P其实是真正的资源分配器,M很轻只是执行程序,所有的资源内存都维护在P上!M只有绑定P才能执行任务(强制的)!
这样做的好处:
1、首先调度程序其实就是调度不同状态的任务,go里面为Go标记了不同的状态,其实大概就是分为:runnable,running,block等,所以如何充分调度不同状态的G成了问题,那么关于阻塞的G如何解决,其实可以很好的解决G调度的问题!
上面这些情况其实就分为:
2、用户态阻塞,一般Go里面依靠 gopark 函数去实现,大体的代码逻辑基本上和go的调度绑定死了
源码在:
3、其实对于netpool 这种nio模型,其实内核调用是非阻塞的,所以go开辟了一个网络轮训器队列,来存放这些被阻塞的g,等待内核被唤醒!那么什么时候会被唤醒了,其实就是需要等待调度器去调度了!
4、如果是内核态阻塞了(内核态阻塞一般都会将线程挂起,线程需要等待被唤醒),我们此时P只能放弃此线程的权利,然后再找一个新的线程去运行P!
关于着新线程:找有没有idle的线程,没有就会创建一个新的线程!
关于当内核被唤醒后的操作:因为GPM模型所以需要找到个P绑定,所以G会去尝试找一个可用的P,如果没有可用的P,G会标记为runnable放到全局队列中!
5、其实了解上面大致其实就了解了Go的基本调度模型
答案文章里慢慢品味!
如果某个 G 执行时间过长,其他的 G 如何才能被正常的调度? 这便涉及到有关调度的两个理念:协作式调度与抢占式调度。协作式和抢占式这两个理念解释起来很简单: 协作式调度依靠被调度方主动弃权;抢占式调度则依靠调度器强制将被调度方被动中断。
例如下面的代码,我本地的版本是 go1.13.5
执行: GOMAXPROCS=1 配置全局只能有一个P
可以看到main函数无法执行!也就是那个go 空转抢占了整个程序
备注:
但是假如我换为用 1.14+版本执行,有兴趣的话可以使用我的docker镜像,直接可以拉取: fanhaodong/golang:1.15.11 和 fanhaodong/golang:1.13.5
首先我们知道G/M/P,G可能和M也可能和P解除绑定,那么关于数据变量放在哪哇!其实这个就是逃逸分析!
输出可以看到其实没有发生逃逸,那是因为 demo被拷贝它自己的栈空间内
备注:
-gcflags"-N -l -m" 其中 -N禁用优化-l禁止内联优化,-m打印逃逸信息
那么继续改成这个
可以看到发现 demo对象其实被逃逸到了堆上!这就是不会出现类似于G如果被别的M执行,其实不会出现内存分配位置的问题!
所以可以看到demo其实是copy到了堆上!这就是g逃逸的问题,和for循环一样的
执行可以发现,其实x已经逃逸到了堆上,所以你所有的g都引用的一个对象,如何解决了
如何解决了,其实很简单
也谈goroutine调度器
图解Go运行时调度器
Go语言回顾:从Go 1.0到Go 1.13
Go语言原本
调度系统设计精要
Scalable Go Scheduler Design Doc
Q3: go语言语法(基础语法篇)
import "workname/packetfolder"
导入多个包
方法调用 包名.函数//不是函数或结构体所处文件或文件夹名
packagename.Func()
前面加个点表示省略调用,那么调用该模块里面的函数,可以不用写模块名称了:
当导入一个包时,该包下的文件里所有init()函数都会被执行,然而,有些时候我们并不需要把整个包都导入进来,仅仅是是希望它执行init()函数而已。下划线的作用仅仅是为了调用init()函数,所以无法通过包名来调用包中的其他函数
import _ package
变量声明必须要使用否则会报错。
全局变量运行声明但不使用。
func 函数名 (参数1,参数2,...) (返回值a 类型a, 返回值b 类型b,...)
func 函数名 (参数1,参数2,...) (返回值类型1, 返回值类型2,...)
func (this *结构体名) 函数名(参数 string) (返回值类型1, 返回值类型2){}
使用大小来区分函数可见性
大写是public类型
小写是private类型
func prifunc int{}
func pubfunc int{}
声明静态变量
const value int
定义变量
var value int
声明一般类型、接口和结构体
声明函数
func function () int{}
go里面所有的空值对应如下
通道类型
内建函数 new 用来分配内存,它的第一个参数是一个类型,不是一个值,它的返回值是一个指向新分配类型零值的指针
func new(Type) *Type
[这位博主有非常详细的分析]
Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可。
goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。
同一个程序中的所有 goroutine 共享同一个地址空间。
语法格式如下:
通道(channel)是用来传递数据的一个数据结构。
通道的声明
通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 - 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。
[这里有比较详细的用例]
go里面的空接口可以指代任何类型(无论是变量还是函数)
声明空接口
go里面的的强制类型转换语法为:
int(data)
如果是接口类型的强制转成其他类型的语法为:
go里面的强制转换是将值复制过去,所以在数据量的时候有比较高的运行代价
go语言多线程遍历目录的介绍就聊到这里吧,感谢你花时间阅读本站内容,更多关于go语言多线程怎么做的、go语言多线程遍历目录的信息别忘了在本站进行查找喔。






