-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.json
1 lines (1 loc) · 180 KB
/
index.json
1
[{"categories":null,"contents":"","date":"Jun 24","permalink":"https://hearecho.github.io/projects/spider/","tags":null,"title":"Spider"},{"categories":null,"contents":"","date":"Jun 24","permalink":"https://hearecho.github.io/projects/web/","tags":null,"title":"Go-Web-Template"},{"categories":null,"contents":"\t双指针经常在数组和链表中使用。二分查找、滑动窗口、快慢指针都是双指针的变形用法。\n二分查找 二分查找,主要是在有序数组中进行查找符合目标值,一般中等难度的问题都不会简单的让查找某个确定的值,而是进行变相的询问,例如询问左边界问题,左边界问题也是用的最多的一种方式。\n34. 在排序数组中查找元素的第一个和最后一个位置 这道题可以使用调用两次最左位置,或者是一次最左,一次最右。两次调用最左位置,就是查找$target$和$target+1$的最左位置,这样就可以获得目标值的最左位置和最有位置。当然最右位置等于$target+1$最左位置-1。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 func searchRange(nums []int, target int) []int { // 时间复杂度的要求所以肯定是使用二分查找方法 // 我们需要查找的是上边界以及下边界 if len(nums) == 0 { return []int{-1,-1} } l := binarySearch(nums, target) h := binarySearch(nums, target + 1) if l \u0026lt; len(nums) \u0026amp;\u0026amp; nums[l] == target { return []int{l, h-1} } return []int{-1,-1} } func binarySearch(nums []int, target int) int { l,h := 0, len(nums) for l\u0026lt;h { // 由于是查找边界,所以不能是 mid := l + (h-l)/2 if nums[mid] \u0026gt;= target { h = mid } else { l = mid + 1 } } return l } 354. 俄罗斯套娃信封问题 这道题可以转换为最长子序列问题,最长子学列问题通常使用动态规划数组,在这里我们使用二分查找的方式来解决最长子序列问题。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 func maxEnvelopes(envelopes [][]int) int { // 排序之后转换为寻找最长子序列问题 \tsort.Slice(envelopes, func(i, j int) bool { if envelopes[i][0] == envelopes[j][0] { return envelopes[i][1] \u0026gt; envelopes[j][1] } return envelopes[i][0] \u0026lt; envelopes[j][0] }) n := len(envelopes) heights := make([]int, n) for i := 0; i \u0026lt; n; i++ { heights[i] = envelopes[i][1] } //求heights里面的最长上升子学列 \tvar lengthOfLTS func() int // \tlengthOfLTS = func() int { // 洗牌算法 \tpiles := 0 top := make([]int, n) for i := 0; i \u0026lt; n; i++ { // 当前要处理的扑克牌 \tpoker := heights[i] // piles表示此时的牌堆 \t// 而left就是当前要处理的牌要插入的位置,如果位置大于piles \t// 说明是放在牌堆大小扩充了 \tleft, right := 0, piles for left \u0026lt; right { mid := left + (right-left)/2 if top[mid] \u0026gt;= poker { right = mid } else { left = mid + 1 } } if left == piles { piles++ } top[left] = poker } return piles } return lengthOfLTS() } 875. 爱吃香蕉的珂珂 这道题是二分搜索的应用,将问题抽象为二分搜索。即我们要寻找一个$x$,使得$f(x)$满足目标$target$。\n例如该题中:$x$为吃香蕉的速度,$f(x)$为判断以$x$速度吃香蕉是否可以在警卫回来之前吃完($target$)。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 func minEatingSpeed(piles []int, h int) int { // 首先确定速度k的范围 \t// k最小值为1, 最大值当没办法进行确定的时候,就可以选择值的最大可能性 \t// 二分法寻找左边界问题 \tl, r := 1, 100000001 sort2.Ints(piles) var f func(x int) bool // f(x) \tf = func(x int) bool { use := 0 for i := 0; i \u0026lt; len(piles); i++ { use += piles[i] / x if piles[i]%x != 0 { use++ } } return use \u0026lt;= h } for l \u0026lt; r { mid := l + (r-l)/2 if f(mid) { // 可以吃完 \tr = mid } else { // 不可以吃完 \tl = mid + 1 } } return l } 滑动窗口 滑动窗口算法是双指针的一种变种,或者说是应用方法。我们一般考虑的就是窗口的两个端点,可以看作两个指针。一般应用的题型的数据结构是字符串或者是数组。而且滑动窗口算法一般都可以使用暴力法来进行解决。\n滑动窗口一般处理都是连续问题,连续子数组或者是子串问题。因为非连续问题不可能存在于一个窗口中,那个时候可能使用动态规划可能会好一点。从类型上来说,滑动窗口一般分为两种类型:\n 固定窗口大小,然后找到符合条件的结果 非固定窗口大小,找到满足符合条件的结果。 固定窗口大小 对于固定窗口大小,我们第一步就需要初始化窗口,即构建一个最开始窗口。\n 初始化l为0,初始化r使得r-l+1为窗口大小。 同时移动l、r,来保证窗口的移动。 判断窗口内部是否满足题目的条件,如果满足则更新或者直接结束,不满足则继续进行循环。 例题 438. 找到字符串中所有字母异位词 - 力扣(LeetCode) (leetcode-cn.com)\n该题就是固定窗口大小,而窗口大小并不是直接给出,而是根据匹配字符串的长度来确定窗口长度大小。使用哈希表的目的是为了判断是否满足题目中的条件,而答题流程则是固定滑动窗口答题流程。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 func findAnagrams(s, p string) (ans []int) { sLen, pLen := len(s), len(p) if sLen \u0026lt; pLen { return } var sCount, pCount [26]int // 这部分就是初始化滑动窗口 \tfor i, ch := range p { sCount[s[i]-\u0026#39;a\u0026#39;]++ pCount[ch-\u0026#39;a\u0026#39;]++ } // 初始化之后就需要进行一次判断 \tif sCount == pCount { ans = append(ans, 0) } for i, ch := range s[:sLen-pLen] { // 进入滑动窗口 即r的移动 \tsCount[ch-\u0026#39;a\u0026#39;]-- // 从滑动窗口中取出 即l的移动 \tsCount[s[i+pLen]-\u0026#39;a\u0026#39;]++ // 进行是否满足条件的判断 \tif sCount == pCount { ans = append(ans, i+1) } } return } 可变滑动窗口 对于可变滑动窗口,由于窗口大小是未知的,所以初始化的时候窗口大小为1\n 初始化l、r为0 移动r,来保证窗口的扩张。 判断窗口内部是否满足题目的条件,如果满足则更新或者直接结束,不满足则可以通过和条件对比来增加l或者是r。 例题 713. 乘积小于K的子数组 - 力扣(LeetCode) (leetcode-cn.com)\n该题就很明显可以使用滑动窗口,并且窗口大小是可变的。而需要满足的条件就是乘积小于K。之后每次满足条件之后更新参数即可,并根据乘积值来更新窗口的大小。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func numSubarrayProductLessThanK(nums []int, k int) int { // 乘积小于k // 连续的子数组 // 严格小于 等于不行 // 正整数数组,即 k\u0026lt;=1的话就直接返回空数组 res := 0 if k \u0026lt;= 1{ return res } mul := 1 l,r := 0,0 // 滑动窗口 窗口长度的变化 // 如果一个窗口的乘积小于k 因为都是正整数 所以整个区间内部的小窗口都会小于k for r \u0026lt; len(nums) { mul *= nums[r] r++ for mul \u0026gt;= k { mul /= nums[l] l++ } res += (r-l) } return res } 快慢指针 快慢指针一般用于在链表中寻找中点,或者是链表环问题。一般是利用快指针会比慢指针走的距离多一倍。\n142. 环形链表 II 相比环形链表该题需要找到环的入口,判断环形链表则是使用快指针慢指针走的位置是存在倍数关系的。而判断环的入口则是使用了两者之间走过的距离差是环的长度整数倍的数学关系。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func detectCycle(head *ListNode) *ListNode { // 找到环入口节点 slow, fast := head, head for fast != nil \u0026amp;\u0026amp; fast.Next != nil { slow = slow.Next fast = fast.Next.Next if fast == slow { break } } if fast == nil || fast.Next == nil { return nil } // 相等证明有环 fast = head for slow != fast { slow = slow.Next fast = fast.Next } return fast } 876. 链表的中间结点 该题是快慢指针最简单的运用,即快指针比慢指针走的距离的二倍。\n1 2 3 4 5 6 7 8 func findMid(head *ListNode) *ListNode{ slow,fast := head,head for fast!=nil || fast.Next != nil { slow = slow.Next fast = fast.Next.Next } return slow } ","date":"Apr 13","permalink":"https://hearecho.github.io/post/%E5%88%B7%E9%A2%98%E7%AC%94%E8%AE%B0-%E5%8F%8C%E6%8C%87%E9%92%88/","tags":["面试","算法","刷题笔记"],"title":"刷题笔记 双指针"},{"categories":null,"contents":"网络层相关 传输层相关 1. 既然IP层会分片, 为什么TCP还要分段? IP分片的原因:\n受到传输链路MTU的影响,而且因为数据链路层的MTU在传输过程中会发生改变,所以说在传输的过程中经过某个路由器之后也会进行分片。MTU默认值为1500byte\nTCP分片的原因:\nTCP分片的主要原因是因为受到网络层数据段的影响。即MSS大小的限制。MSS大小限制为1460,即1500 - 20(IP头) -20(TCP头)。\nTCP还要分段的原因:\n如果是TCP不分段,\n其他 ","date":"Apr 08","permalink":"https://hearecho.github.io/post/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/","tags":["面试","计算机网络"],"title":"计算机网络"},{"categories":null,"contents":"Go 垃圾收集 Go垃圾收集使用的标记清除的方式,并且是并行GC,即和用户程序是一起运行的。标记方式使用的则是三色收集法。\n垃圾收集步骤 Go垃圾收集总共是分为四部分:\n Sweep Termination: 对未清扫的span进行清扫, 只有上一轮的GC的清扫工作完成才可以开始新一轮的GC Mark: 扫描所有根对象, 和根对象可以到达的所有对象, 标记它们不被回收 Mark Termination: 完成标记工作, 重新扫描部分根对象 Sweep: 按标记结果清扫span 在GC过程中会有两种后台任务(G), 一种是标记用的后台任务, 一种是清扫用的后台任务。清扫任务会在程序启动时就启动,但是在进入清扫阶段之后才被唤醒。其中在标记阶段开始的时候和标记阶段结束的时候需要停止用户程序即STW(Stop The World)。两次STW的作用如下:\n 第一次STW会准备根对象的扫描, 启动写屏障(Write Barrier)和辅助GC(mutator assist). 第二次STW会重新扫描部分根对象, 禁用写屏障(Write Barrier)和辅助GC(mutator assist). Gc的触发条件 源码给出的触发条件主要有三种,如果不是这三种则是一直进行GC:\n gcTriggerHeap: 当前分配的内存达到一定值就触发GC gcTriggerTime: 当一定时间没有执行过GC就触发GC gcTriggerCycle: 要求启动新一轮的GC, 已启动则跳过, 手动触发GC的runtime.GC()会使用这个条件 go源代码如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func (t gcTrigger) test() bool { if !memstats.enablegc || panicking != 0 || gcphase != _GCoff { return false } switch t.kind { case gcTriggerHeap: // Non-atomic access to heap_live for performance. If \t// we are going to trigger on this, this thread just \t// atomically wrote heap_live anyway and we\u0026#39;ll see our \t// own write. \treturn memstats.heap_live \u0026gt;= memstats.gc_trigger case gcTriggerTime: if gcpercent \u0026lt; 0 { return false } lastgc := int64(atomic.Load64(\u0026amp;memstats.last_gc_nanotime)) return lastgc != 0 \u0026amp;\u0026amp; t.now-lastgc \u0026gt; forcegcperiod case gcTriggerCycle: // t.n \u0026gt; work.cycles, but accounting for wraparound. \treturn int32(t.n-work.cycles) \u0026gt; 0 } return true } 三色标记法 三色标记法就是通过堆内存中不同状态的下的对象进行标价来为后续的清除做准备。三色的含义如下:\n 黑色: 对象在这次GC中已标记, 且这个对象包含的子对象也已标记 灰色: 对象在这次GC中已标记, 但这个对象包含的子对象未标记 白色: 对象在这次GC中未标记 三色标记的过程:\n 先将Gc root标记为灰色,加入到灰色队列中 从灰色队列中取出对象,将其标记为黑色,并将其直接引用对象标记为灰色放入到灰色队列中 重复步骤2,直到没有灰色对象的存在,此时剩余的白色对象就是需要进行回收的对象。 Gc root对象\n Fixed Roots: 特殊的扫描工作 fixedRootFinalizers: 扫描析构器队列 fixedRootFreeGStacks: 释放已中止的G的栈 Flush Cache Roots: 释放mcache中的所有span, 要求STW Data Roots: 扫描可读写的全局变量 BSS Roots: 扫描只读的全局变量 Span Roots: 扫描各个span中特殊对象(析构器列表) Stack Roots: 扫描各个G的栈 写屏障 写屏障的存在是因为go的垃圾收集过程是和用户程序并行的。并行的过程就会产生一些对象依赖上的变化,造成错误回收。例如开始扫描的时候发现根对象A和B,B拥有C的指针,GC先扫描A, 然后B把C的指针交给A, GC再扫描B, 这时C就不会被扫描到。写屏障就是为了避免这种类型的问题而存在的。\n启用了写屏障(Write Barrier)后, 当B把C的指针交给A时, GC会认为在这一轮的扫描中C的指针是存活的, 即使A可能会在稍后丢掉C, 那么C就在下一轮回收.\n也就是说,写屏障启动之后,用户程序的改动对于GC将不会造成任何影响,相互独立。写屏障只针对指针启用, 而且只在GC的标记阶段启用, 平时会直接把值写入到目标地址.\n混合写屏障会同时标记指针写入目标的\u0026quot;原指针\u0026quot;和“新指针\u0026quot;。标记原指针的原因是,,其他运行中的线程有可能会同时把这个指针的值复制到寄存器或者栈上的本地变量,因为复制指针到寄存器或者栈上的本地变量不会经过写屏障, 所以有可能会导致指针不被标记,记新指针的原因是, 其他运行中的线程有可能会转移指针的位置。混合写屏障可以让GC在并行标记结束后不需要重新扫描各个G的堆栈, 可以减少Mark Termination中的STW时间。如果只是单独的写屏障,需要排除那些不经过写屏障的读写操作。\n辅助GC 为了防止heap增速太快, 在GC执行的过程中如果同时运行的G分配了内存, 那么这个G会被要求辅助GC做一部分的工作. 在GC的过程中同时运行的G称为\u0026quot;mutator\u0026quot;, \u0026ldquo;mutator assist\u0026quot;机制就是G辅助GC做一部分工作的机制.\n辅助GC做的工作有两种类型, 一种是标记(Mark), 另一种是清扫(Sweep).\n","date":"Mar 04","permalink":"https://hearecho.github.io/post/go%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86/","tags":null,"title":"Go垃圾收集"},{"categories":null,"contents":"Go Questions 1.nil切片和空切片、零切片 nil切片指向的地址为0,而所有创建的空切片的内存地址是存在的,并且是一个固定值。\n零切片就是底层数组内部数据全是零变量。说白了就是使用make初始化之后的切片。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func compareSlice() { var s1 []int s2 := make([]int,0) s4 := make([]int,0) fmt.Printf(\u0026#34;s1 pointer:%+v, s2 pointer:%+v, s4 pointer:%+v, \\n\u0026#34;, *(*reflect.SliceHeader)(unsafe.Pointer(\u0026amp;s1)),*(*reflect.SliceHeader)(unsafe.Pointer(\u0026amp;s2)),*(*reflect.SliceHeader)(unsafe.Pointer(\u0026amp;s4))) fmt.Printf(\u0026#34;%v\\n\u0026#34;, (*(*reflect.SliceHeader)(unsafe.Pointer(\u0026amp;s1))).Data==(*(*reflect.SliceHeader)(unsafe.Pointer(\u0026amp;s2))).Data) fmt.Printf(\u0026#34;%v\\n\u0026#34;, (*(*reflect.SliceHeader)(unsafe.Pointer(\u0026amp;s2))).Data==(*(*reflect.SliceHeader)(unsafe.Pointer(\u0026amp;s4))).Data) } // 结果 /** s1 pointer:{Data:0 Len:0 Cap:0}, s2 pointer:{Data:824633999016 Len:0 Cap:0}, s4 pointer:{Data:824633999016 Len:0 Cap:0}, false true */ 2.字符串转换为byte数组,会发生内存拷贝吗? 严格来说,只要进行了类型的强制转换都会发生内存拷贝。所以说字符串转换为byte数组会发生内存拷贝。go的字符串也为不可变对象,在内存中的实现方式是一个只读的字节数组。字符串要想修改只能先转换为可写的数组,然后在转换为字符串。其数据结构如下:\n1 2 3 4 type StringHeader struct { Data uintptr Len int } 用代码展示,可以从结果上看出,不论是从字符串转换为byte数组还是从byte数组转换为字符串,均发生内存拷贝了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 func StringAndByte() { s := \u0026#34;hello\u0026#34; tempByte := []byte(s) s2 := string(tempByte) fmt.Printf(\u0026#34;s Pointer:%+v\\n\u0026#34;, *(*reflect.StringHeader)(unsafe.Pointer(\u0026amp;s))) fmt.Printf(\u0026#34;tempByte Pointer:%+v\\n\u0026#34;, *(*reflect.SliceHeader)(unsafe.Pointer(\u0026amp;tempByte))) fmt.Printf(\u0026#34;s2 Pointer:%+v\\n\u0026#34;, *(*reflect.StringHeader)(unsafe.Pointer(\u0026amp;s2))) } // 结果 /** s Pointer:{Data:12543120 Len:5} tempByte Pointer:{Data:824633999064 Len:5 Cap:32} s2 Pointer:{Data:824633999032 Len:5} */ 不过也有方法可以不用进行内存拷贝实现转换,实际上,字符串和byte数组的底层结构之间只是少了Cap字段,所以我们可以将StringHeader 的地址强转成 SliceHeader 就可以了。\n1 2 3 a :=\u0026#34;aaa\u0026#34; ssh := *(*reflect.StringHeader)(unsafe.Pointer(\u0026amp;a)) b := *(*[]byte)(unsafe.Pointer(\u0026amp;ssh)) 3.翻转含有中文、数字、英文字母的字符串 因为中文、英文、数字所占用的字节数是不相同的,所以我们不可以使用转换为byte数组来进行反转在转换,我们这个情况需要将字符串转换为[]rune,因为其表示的范围更大,rune==int32而byte==uint8。\n1 2 3 4 5 6 7 8 9 10 11 func ReverseComplexString() { reverse := func (s []rune) []rune { for i, j := 0, len(s)-1; i \u0026lt; j; i, j = i+1, j-1 { s[i], s[j] = s[j], s[i] } return s } src := \u0026#34;你好abc啊哈哈\u0026#34; dst := reverse([]rune(src)) fmt.Printf(\u0026#34;%v\\n\u0026#34;, string(dst)) } 4. 拷贝大切片一定比小切片的代价大吗? 并不是,所有切片的大小相同;三个字段(一个 uintptr,两个int)。切片中的第一个字是指向切片底层数组的指针,这是切片的存储空间,第二个字段是切片的长度,第三个字段是容量。将一个 slice 变量分配给另一个变量只会复制三个机器字。所以 拷贝大切片跟小切片的代价应该是一样的。\n拷贝就相当于是变换指针的指向,而不是将内存数据从一个地址拷贝到另一处地址,所以这个只更换指针的指向也造成了更改其中一个切片的数据,另一个切片显示的数据也会改变即为浅拷贝\n5. map不初始化使用会怎么样,slice呢? map不初始化为nil,向里面添加值会直接报错。panic: assignment to entry in nil map。但是可以进行取值,不过返回的是对应类型的零值。并且初始化和不初始化的map。长度均为0。但是slice是可以声明之后就可以使用(不是真正意义上的使用)的,可以使用append向里面添加元素(这种方式其实是返回了一个新的切片),但是不能使用直接用索引赋值的方法添加元素,不过slice声明不初始化的话其指向的底层数组地址为0,第一次添加元素之后会给出一个内存地址。\n6. map承载多大,大了之后怎么扩容? 1 2 3 4 5 6 7 8 9 // Maximum number of key/elem pairs a bucket can hold. bucketCntBits = 3 bucketCnt = 1 \u0026lt;\u0026lt; bucketCntBits // 每个桶大小为 8 // Maximum average load of a bucket that triggers growth is 6.5. // Represent as loadFactorNum/loadFactorDen, to allow integer math. loadFactorNum = 13 loadFactorDen = 2 // 溢出因子大小为 6.5 哈希表 runtime.hmap 的桶是 runtime.bmap。每一个 runtime.bmap 都能存储8个键值对,当哈希表中存储的数据过多,单个桶已经装满时就会使用 extra.nextOverflow 中桶存储溢出的数据。 而发生扩容的条件是:\n 触发 load factor 的最大值,负载因子已达到当前界限。负载因子越大,证明空间效率越高,同时发生冲突的概率也越大。 溢出桶 overflow buckets 过多。即溢出桶和全部正常桶数量的比值。比值过大就证明溢出桶过多。 而map的扩容也是分为两种情况进行扩容的,如果是负载因子达到最大值,则是直接动态扩容当前大小两倍作为新容量的大小。而如果是溢出桶过多,则是不改变大小的扩容。而扩容并不是一步到位,而是先申请扩容空间,但是不会进行初始化,而是等到有新的访问落到某个桶中,才会对这个桶进行扩容,也就是将oldbucket迁移到bucket。\n7. map的iterator是否安全?能不能一边delete一边遍历? map的iterator是不安全的,我们需要手动对其进行并发约束来使其到达在并发中的数据安全。一般是使用sync.RWMutex或者是使用channel chan。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 var myMap map[int]string func myGoRoutine(id int, numKeys int, wg *sync.WaitGroup) { defer wg.Done() for key, _ := range myMap { myMap[key] = strconv.Itoa(id) } for key, value := range myMap { fmt.Printf(\u0026#34;Goroutine #%d -\u0026gt; Key: %d, Value: %s\\n\u0026#34;, id, key, value) } } func UnSafeMap() { // Initially set some values \tmyMap = make(map[int]string) myMap[0] = \u0026#34;test\u0026#34; myMap[2] = \u0026#34;sample\u0026#34; myMap[1] = \u0026#34;GoLang is Fun!\u0026#34; // Get the number of keys \tnumKeys := len(myMap) var wg sync.WaitGroup for i := 0; i \u0026lt; 3; i++ { wg.Add(1) go myGoRoutine(i, numKeys, \u0026amp;wg) } // Blocking wait \twg.Wait() // Iterate over all keys \tfor key, value := range myMap { fmt.Printf(\u0026#34;Key: %d, Value: %s\\n\u0026#34;, key, value) } } /* 结果 Goroutine #2 -\u0026gt; Key: 0, Value: 2 Goroutine #2 -\u0026gt; Key: 2, Value: 2 Goroutine #2 -\u0026gt; Key: 1, Value: 2 Goroutine #1 -\u0026gt; Key: 0, Value: 1 Goroutine #1 -\u0026gt; Key: 2, Value: 1 Goroutine #1 -\u0026gt; Key: 1, Value: 1 Goroutine #0 -\u0026gt; Key: 0, Value: 0 Goroutine #0 -\u0026gt; Key: 2, Value: 1 Goroutine #0 -\u0026gt; Key: 1, Value: 1 Key: 0, Value: 1 Key: 2, Value: 1 Key: 1, Value: 1 */ 同时go中的map是可以一边进行操作然后一边进行遍历的。但是虽然不会出错,但是当前遍历不会受到影响。不像java他们的迭代器具有Fail-Fast性质。\n8. 怎么判断一个数组是否有序 第一种方法是实现sort.Interface的接口类型,然后直接调用sort.IsSorted()方法即可。或者直接使用sort.SliceIsSorted函数。\n1 2 3 4 5 6 func jungeSorted() { arr := []int{5,4,3,2,1} fmt.Println(sort.SliceIsSorted(arr, func(i, j int) bool { return arr[i] \u0026gt; arr[j] })) } 9. array和slice的区别 数组array是值类型的,其作为参数传递给函数,就是将数组拷贝一份,而切片slice是一个引用类型,是一个动态指向数组切片的指针,不定长。声明数组的时候,方括号内写明了数组长度或者使用...进行代替,而声名切片的时候方括号内部为空。作为函数参数时候,切片传递的是指针,所以在函数内部改动,外部切片也会发生相应的变化。\n10. json包变量不加tag会怎么样? 如果变量首字母小写,则为private。无论如何不能转,因为取不到反射信息。\n 如果变量首字母大写,则为public。\n 不加tag,可以正常转为json里的字段,json内字段名跟结构体内字段原名一致。 加了tag,从struct转json的时候,json的字段名就是tag里的字段名,原字段名已经没用。 tag信息是可以通过reflect获取的。即:\n1 type J struct { a string //小写无tag b string `json:\u0026#34;B\u0026#34;` //小写+tag C string //大写无tag D string `json:\u0026#34;DD\u0026#34; otherTag:\u0026#34;good\u0026#34;` //大写+tag}func printTag(stru interface{}) { t := reflect.TypeOf(stru).Elem() for i := 0; i \u0026lt; t.NumField(); i++ { fmt.Printf(\u0026#34;结构体内第%v个字段 %v 对应的json tag是 %v , 还有otherTag? = %v \\n\u0026#34;, i+1, t.Field(i).Name, t.Field(i).Tag.Get(\u0026#34;json\u0026#34;), t.Field(i).Tag.Get(\u0026#34;otherTag\u0026#34;)) }}func main() { j := J{ a: \u0026#34;1\u0026#34;, b: \u0026#34;2\u0026#34;, C: \u0026#34;3\u0026#34;, D: \u0026#34;4\u0026#34;, } printTag(\u0026amp;j)} printTag方法传入的是j的指针。 reflect.TypeOf(stru).Elem()获取指针指向的值对应的结构体内容。 NumField()可以获得该结构体的含有几个字段。 遍历结构体内的字段,通过t.Field(i).Tag.Get(\u0026quot;json\u0026quot;)可以获取到tag为json的字段。 如果结构体的字段有多个tag,比如叫otherTag,同样可以通过t.Field(i).Tag.Get(\u0026quot;otherTag\u0026quot;)获得 11. 深拷贝和浅拷贝 \t一般简单的拷贝,即指针指向转换,即虽然是两个切片,但是他们指向同一个底层数组,此为浅拷贝,即通过其中一个切片修改数据,另一个切片的内容也会发生变化。而深拷贝,则是进行了内存拷贝,底层数组的拷贝,两个切片指向的地址是不相同的。同时需要注意引用切片,其中的引用如果不进行递归深拷贝,则还是会出现问题。\n12. make和new的区别 \tnew可以用于任何类型的分配空间,指定内存,并返回该类型的指针。同时 new 函数会把分配的内存置为零,也就是类型的零值。\n1 // The new built-in function allocates memory. The first argument is a type,// not a value, and the value returned is a pointer to a newly// allocated zero value of that type.func new(Type) *Type make只能用于slice, map, channel的初始化。这三个刚好为引用类型,并且Unlike new, make\u0026rsquo;s return type is the same as the type of its argument, not a pointer to it.\n1 // The make built-in function allocates and initializes an object of type// slice, map, or chan (only). Like new, the first argument is a type, not a// value. Unlike new, make\u0026#39;s return type is the same as the type of its// argument, not a pointer to it. The specification of the result depends on// the type://\tSlice: The size specifies the length. The capacity of the slice is//\tequal to its length. A second integer argument may be provided to//\tspecify a different capacity; it must be no smaller than the//\tlength. For example, make([]int, 0, 10) allocates an underlying array//\tof size 10 and returns a slice of length 0 and capacity 10 that is//\tbacked by this underlying array.//\tMap: An empty map is allocated with enough space to hold the//\tspecified number of elements. The size may be omitted, in which case//\ta small starting size is allocated.//\tChannel: The channel\u0026#39;s buffer is initialized with the specified//\tbuffer capacity. If zero, or the size is omitted, the channel is//\tunbuffered.func make(t Type, size ...IntegerType) Type 13. slice,map,channel创建的时候的几个参数什么含义? \t由于切片的底层即SliceHeader的结构如下所示。而创建切片的使用make(type, len, cap),cap通常可以省略,省略情况下cap=len。因为两者之间的关系为cap\u0026gt;=len\u0026gt;=0。其中cap代表容量,len代表当前切片的长度。\n1 type SliceHeader struct {\tData uintptr\tLen int\tCap int} 而我们创建map通常使用make(map[Type]Type,size),size表示map的存储能力,可以省略。使用make(chan Type, size)创建channel而size表示的具有通道的缓冲区大小,如果不设置,则表示该通道不具有缓冲区,默认size=0。\n14 slice扩容 源代码如下:可以看出当当前的容量小于扩容之后的容量的长度的时候,并且当前的长度小于1024,则扩容为当前的两倍,否则扩容四分之一大小。\n1 func grow(s Value, extra int) (Value, int, int) {\ti0 := s.Len()\ti1 := i0 + extra\tif i1 \u0026lt; i0 {\tpanic(\u0026#34;reflect.Append: slice overflow\u0026#34;)\t}\tm := s.Cap()\tif i1 \u0026lt;= m {\treturn s.Slice(0, i1), i0, i1\t}\tif m == 0 {\tm = extra\t} else {\tfor m \u0026lt; i1 {\tif i0 \u0026lt; 1024 {\tm += m\t} else {\tm += m / 4\t}\t}\t}\tt := MakeSlice(s.Type(), i1, m)\tCopy(t, s)\treturn t, i0, i1} 15. 线程安全的map怎么实现 go里面的map并不是并发安全的,实现其安全主要有三种方法;\n 采用sync.RWMutex或者是在协程环境下使用chan\n1 type RWMap struct { // 一个读写锁保护的线程安全的map sync.RWMutex // 读写锁保护下面的map字段 m map[int]int} 简单的采用sync.RWMutex,虽然功能上能够满足,但是在性能上,由于是对整个哈希表进行加锁,所以会导致性能下降。我们可以学习java对哈希表加锁的处理方式,使用多段锁,降低锁的粒度,go中比较知名的分片map实现是orcaman/concurrent-map,其对将整个map分为n快,每个块读写操作互相不干扰。实现原理也类似,就是在一个切片中存储带有读写锁的map,然后通过计算key在哪一个分片上来进行哈希表的读写。\n1 var SHARD_COUNT = 32// 分成SHARD_COUNT个分片的maptype ConcurrentMap []*ConcurrentMapShared// 通过RWMutex保护的线程安全的分片,包含一个maptype ConcurrentMapShared struct { items map[string]interface{} sync.RWMutex // Read Write mutex, guards access to internal map.}// 创建并发mapfunc New() ConcurrentMap { m := make(ConcurrentMap, SHARD_COUNT) for i := 0; i \u0026lt; SHARD_COUNT; i++ { m[i] = \u0026amp;ConcurrentMapShared{items: make(map[string]interface{})} } return m}// 根据key计算分片索引func (m ConcurrentMap) GetShard(key string) *ConcurrentMapShared { return m[uint(fnv32(key))%uint(SHARD_COUNT)]} 内置的sync,map是一个并发安全的map,但是用的比较少,主要是用于一写多读或者是各个协程操作的key集合没有交集或者是交集很少,能够显著提升性能。\n 16. struct是否可以进行比较? 在go中数据类型可以比较与不可以比较的数据类型如下所示:\n 可比较:Integer,Floating-point,String,Boolean,Complex(复数型),Pointer,Channel,Interface,Array 不可比较:Slice,Map,Function struct是否可以比较以及比较之后的结果,主要是看其内部字段的类型,如果其内部字段类型均为可以比较的数据类型,则该struct是可以比较的,如果含有不可比较的数据类型,则struct也不可比较。并且结构体是否相等,也要看其内部的数据值。当然我们可以通过reflect.DeepEqual来实现包含不可直接比较的数据类型的结构体实例的比较。reflect.DeepEqual就是比较所有的值,即两个值深度一致。\n17. map如何实现顺序读取 \tmap一般的读写的顺序是不固定,想要实现顺序读写,需要先将key取出,然后再通过key取出value。而一般对map进行排序输出,也是通过这种方式,不过是需要对key进行排序。\n18. go中实现set \t效仿java中hashset的实现,由于map中不会存在相同的key值,所以我们可以通过map实现。当然由于map中的key必须是要由可以比较的数据类型构成,所以例如切片、哈希表、函数是不可以的。而结构体内部也必须不含有不可比较类型。数据类型如下,使用struct{}作为value的原因是因为其占用的内存大小为0。\n1 type Set map[interface{}]struct{} 19. golang中是否可以进行指针运算? \tgolang中有普通指针,unsafe.Poniter,以及uintptr。其中只有uintptr可以进行指针运算,但是go的垃圾回收机制不会将uintptr看作指针,uintptr无法持有对象,并且会被回收。但是我们可以将普通指针通过unsafe.Poniter转换为uintptr,进行运算操作之后,在转换为普通指针。所以golang严格意义上不能进行指针运算,但是可以通过转换间接完成指针运算。\n20. for select时候,如果通道已经关闭会发生什么情况,如果select中只有一个case呢? 我们将其分为几种情况进行讨论:\n 第一种情况就是for循环里面被关闭的通道。从结果上可以看出,通道关闭之后,还是会进入读取通道信息的case。这是因为通道关闭之后,只是不能再向里面写入数据,但是可以从通道中读取数据。我们可以通过在确认通道已经关闭,并且已经没有数据读出的时候,将通道置为nil,就不会再读取已经关闭的通道了。\n1 const fmat = \u0026#34;2006-01-02 15:04:05\u0026#34;func closeChannelInFor() {\tc := make(chan int)\tgo func() {\ttime.Sleep(1*time.Second)\tc \u0026lt;- 10\tclose(c)\t}()\tfor {\tselect {\tcase x, ok := \u0026lt;- c:\tfmt.Printf(\u0026#34;%v, 通道读取到:x=%v, ok=%v\\n\u0026#34;, time.Now().Format(fmat), x, ok) //if !ok { // c = nil //}\ttime.Sleep(500*time.Millisecond)\tdefault:\tfmt.Printf(\u0026#34;%v, 通道么有读取到数据进入defult \\n\u0026#34;, time.Now().Format(fmat))\ttime.Sleep(500*time.Millisecond)\t}\t}}/*结果2022-02-22 13:02:51, 通道么有读取到数据进入defult 2022-02-22 13:02:52, 通道么有读取到数据进入defult 2022-02-22 13:02:52, 通道读取到:x=10, ok=true2022-02-22 13:02:53, 通道读取到:x=0, ok=false2022-02-22 13:02:54, 通道读取到:x=0, ok=false2022-02-22 13:02:54, 通道读取到:x=0, ok=false2022-02-22 13:02:55, 通道读取到:x=0, ok=false2022-02-22 13:02:55, 通道读取到:x=0, ok=false2022-02-22 13:02:56, 通道读取到:x=0, ok=false*/ 而如果只有一个case,则还是会进入该case,但是如果将其置为nil,则会造成协程死锁。\n1 func closeChannelInForOneCase() {\tc := make(chan int)\tgo func() {\ttime.Sleep(1*time.Second)\tc \u0026lt;- 10\tclose(c)\t}()\tfor {\tselect {\tcase x, ok := \u0026lt;- c:\tfmt.Printf(\u0026#34;%v, 通道读取到:x=%v, ok=%v\\n\u0026#34;, time.Now().Format(fmat), x, ok)\tif !ok {\tc = nil\t}\ttime.Sleep(500*time.Millisecond)\t}\t}}/*结果:2022-02-22 13:14:49, 通道读取到:x=10, ok=true2022-02-22 13:14:50, 通道读取到:x=0, ok=falsefatal error: all goroutines are asleep - deadlock!*/ 所以说select中如果有某个通道有值可以读的时候,就会执行该case,但是如果没有default,则有可能造成阻塞,直到有通道可以运行。\n21. defer 的使用 defer关键字就是实现在作用域结束之后执行函数的关键字,主要作用就是在当前函数或者是方法返回之前调用一些用于收尾的函数,例如关闭文件、关闭数据库连接以及解锁资源。\n多个defer语句的执行顺序 defer语句的执行顺序和在代码中的位置相关,在函数执行语句返回之前,按照先进后出的方式执行所有的defer语句。但是如果存在panic语句是例外。panic会导致程序崩溃,但是不会影响defer语句的运行。\ndefer的值 defer修饰的语句中的值,在该语句出现的时候确定,后续的执行并不影响结束的时候语句中的值(非引用类型或者是指针)。当 defer 调用时其实会对函数中引用的外部参数进行拷贝。但是如果拷贝的指针类型,则还是会出现变化。\n22. select的使用 select能够让协程同时等待多个通道可读或者可写,在多个文件或者是通道状态改变之前,select会一致阻塞当前的协程。select可以在通道上进行非阻塞的手法操作,并且当多个通道都可以进行操作的时候,将会进行随机选择一个case进行执行。\n典型应用 超时判断,即一个case作为接收消息,另一个case为一个time.After(...)。则当第一个case在一定时间内阻塞,则将会执行另外一个case,判断超时,做出相应的处理。 判断通道是否阻塞 用于多个协程在某个协程达到退出条件的时候,退出其他所有的协程。 23. 如何从panic中恢复? \t在了解如何从panic中恢复之前,我们先了解panic的机制。panic会改变程序的控制流,调用panic之后会立刻停止执行当前函数的剩余代码,并在当前协程中递归执行调用方的defer。而recover可以中值panic造成的程序崩溃,她是一个只能在defer中发挥作用的函数,在其他作用域是不会发挥作用的。\n panci只会触发当前协程的defer。 recover只有在defer中调用才会有效。 panic允许在defer中嵌套多次使用。 所以说我们从panic中恢复的话,需要将recover语句放置在defer关键词之后。示例如下:\n1 func badCall() { panic(\u0026#34;bad end\u0026#34;)}func test() { defer func() { if e := recover(); e != nil { fmt.Printf(\u0026#34;Panicing %s\\r\\n\u0026#34;, e) } }() badCall() fmt.Printf(\u0026#34;After bad call\\r\\n\u0026#34;) // \u0026lt;-- wordt niet bereikt}func main() { fmt.Printf(\u0026#34;Calling test\\r\\n\u0026#34;) test() fmt.Printf(\u0026#34;Test completed\\r\\n\u0026#34;)}/*结果Calling testPanicing bad endTest completed*/ 24. 如何避免内存逃逸 \t内存逃逸就是值得局部变量(存在栈上)没有在栈上进行回归,而是进入到堆中,在堆中被回收,就叫做内存逃逸。所以避免内存逃逸就是避免局部变量进入到堆中。可以通过命令行go build -gcflags=-m查看内存逃逸,内存逃逸发生的原因有以下几种:\n 向chan发送指针数据。由于在编译的时候,不知道该数据会被哪个goroutine接收,所以不知道这个局部变量什么时候才能释放,所以只能放到堆中,在堆中等待被回收。 局部变量在函数调用结束后还被其他地方使用,比如函数返回局部变量指针或闭包中引用包外的值。因为变量的生命周期可能会超过函数周期,因此只能放入堆中。 在 slice 或 map 中存储指针。比如 []*string,其后面的数组可能是在栈上分配的,但其引用的值还是在堆上。 切片扩容后长度太大,导致栈空间不足,逃逸到堆上。初始化的时候是在栈上进行分配,运行时数据扩充则要在对上进行分配,但是初始化的时候不知道容量大小,则会直接在堆上进行分配。 在 interface 类型上调用方法。 在 interface 类型上调用方法时会把interface变量使用堆分配, 因为方法的真正实现只能在运行时知道。 针对发生内存逃逸的原因我们可以通过以下方式来避免内存逃逸:\n 对于小型的数据,使用传值而不是传指针,避免内存逃逸。 避免使用长度不固定的slice切片,在编译期无法确定切片长度,只能将切片使用堆分配。 interface调用方法会发生内存逃逸,在热点代码片段,谨慎使用。 25. Goroutine 泄露 goroutine泄露的原因主要集中在以下几个方面:总结来看,只要发生了阻塞,就会产生Gououtine泄露。\n goroutine中正在进行channel的读写操作,但是因为代码逻辑问题,导致一直被阻塞。 goroutine内的业务逻辑进入死循环,资源一直无法被释放。 goroutine内的业务逻辑进入长时间的等待,并且有不断新增的goroutine进入等待。 造成阻塞的原因大多可以分为:\n 向通道中发送数据,但是没有从通道中取出数据。或者是想从通道中取出数据,但是没有向通道中发送数据。 通道没有进行初始化,使用了nil通道 没有阻塞,但是一个操作等待时间比较长(例如在获取网页内容的时候,由于网速问题,并且没有设置超时,无法进行复用),就会导致goroutine的数量越来越多。 锁使用不当,加锁忘记释放锁,或者是同步锁使用 排查方法 可以使用runtime.NumGoroutine来获取Goroutine的运行数量,然后前后进行比较,就可以知道是否泄露了。\n 在业务运行场景中,一般可以直接使用PProf。\n1 import ( \u0026#34;net/http\u0026#34; _ \u0026#34;net/http/pprof\u0026#34;)http.ListenAndServe(\u0026#34;localhost:6060\u0026#34;, nil)) 26. 内存泄露问题 \t首先要说明的内存泄漏和内存逃逸是不同的概念,内存泄露是一部分内存无法得到回收。而内存逃逸只是局部变量从栈跑到堆中,但是还是会被回收,如果在这个阶段不被回收才是内存泄露。造成内存泄露的原因如下:\n 获取长字符串的一段导致长字符串未释放。 获取长切片的一段导致长切片没有释放。 在长切片中新建切片导致泄露。 goroutine泄露,这个一般是由于goroutine阻塞引起的。 time.Ticker没有关闭导致泄露。 Finalizer导致泄露。 Deferring Function Call导致泄露。 27. sync.Pool的适用场景 \tsync.Pool是一个单独保存和检索的临时对象。其目的是为了缓存已经分配到那时没有使用的元素以便于后续重用。其一大特点就是可以减轻垃圾收集器的压力,而且他是并发安全的。所以其可以很容易的构成高效、并发安全的空闲列表。一个很好的例子就是在fmt.Printf中,管理了一个动态大小用于存储输出的缓冲区。下面是官方源码注释:\n1 A Pool is a set of temporary objects that may be individually saved andretrieved.Any item stored in the Pool may be removed automatically at any time withoutnotification. If the Pool holds the only reference when this happens, theitem might be deallocated.A Pool is safe for use by multiple goroutines simultaneously.Pool\u0026#39;s purpose is to cache allocated but unused items for later reuse,relieving pressure on the garbage collector. That is, it makes it easy tobuild efficient, thread-safe free lists. However, it is not suitable for allfree lists.An appropriate use of a Pool is to manage a group of temporary itemssilently shared among and potentially reused by concurrent independentclients of a package. Pool provides a way to amortize allocation overheadacross many clients.An example of good use of a Pool is in the fmt package, which maintains adynamically-sized store of temporary output buffers. The store scales underload (when many goroutines are actively printing) and shrinks whenquiescent.On the other hand, a free list maintained as part of a short-lived object isnot a suitable use for a Pool, since the overhead does not amortize well inthat scenario. It is more efficient to have such objects implement their ownfree list.A Pool must not be copied after first use. 28. 对已经关闭的chan进行读写,会发生什么?如果是未初始化的chan呢? 对于已经关闭的chan需要分情况,情况如下:\n 读已经关闭的通道会一直读出信息,但是信息是什么会根据通道中是否还有数据来决定的。 第一种情况如果是关闭的通道还有数据,则会将数据读出,返回值第一个为取出的数据,第二个为标志是否读取成功的标志为true。 第二种情况关闭的通道中已经没有数据了,此时读出的数据为通道中存储数据类型的零值,第二个标志位为false。 向已经关闭的通道写数据会导致panic。 如果是未初始化的chan,从chan中读取数据会导致一直阻塞,同时向chan中写入数据也会导致阻塞。\n29. sync.map的优缺点和使用场景 由于go中的map不是并发安全的,go提供了sync.map用于并发使用。 从官方源码注释上我们了解到,sync.map主要更适合以下两个使用场景(对其专门做了优化),在这两种情况下使用sync.map比使用普通map加上MUtex、RWMutex性能要好很多。其工作原理就是在写的时候直接邪写入到dirty map,读取的时候先读read map如果read map中没有再去读dirty map。就是读写分离,空间换取时间。\n 对于map中元素多读少写的情况。 多个goroutine读取和写入是key没有相关的元素的情况。即插入元素或者读取元素分散性强。 所以其优缺点也很明显。\n优点:通过读写分离,降低锁时间来提高性能\n缺点:不适用于大量写的场景(大量写的场景可以使用普通map+锁的方式),这样会导致read map读不到数据而进一步加锁读取,同时dirty map也会一直晋升为read map,整体性能较差。\n30. 如何让主协程等待子协程完成之后再继续执行? 这一点涉及到了并发同步问题,一般简单有两种方式:\n 使用通道传递信号量,其实就是变相模仿sync.WaitGroup。即每个goroutine完成之后向通道中写入消息,而主goroutine则会通过for range来判断子goroutine是否完成。 使用sync.WaitGourp,即开始时间使用Add()方法确认有多少个子协程,在子goroutine中完成业务代码后调用Done()方法,主goroutine中调用Wait()方法等待子goroutine的完成。 31. channel有无缓存有何区别? \t带缓存的channel和不带缓存的channel最大的区别就在于无缓存的的channel如果发送方或者接收方没有准备好就会被阻塞,所以无缓存的channel一般用于需要同步的场景中。而有缓存的channel只有在缓冲区被写入满了并且没有读取才会阻塞写入。\n32. goroutine的并发控制 goroutine只能由自己本身控制在何种情况下退出,外界一般无法强制结束(程序崩溃或者是main函数结束除外)。而针对goroutine的并发控制类型只分为以下三种:\n 全局共享变量\n全局共享变量是一种最简单的控制并发的方式。一般的实现方式如下:\n 声明一个全局变量 所有子goroutine共享这个变量,并且不断轮询这个变量检查是否更新 在主goroutine中变更该全局变量 子goroutine检测到变量更新然后执行相应的逻辑。 其优点就是实现简单,但是缺点就是只能多读一写,如果想要多写,就需要解决全局共享变量的同步问题,例如给他加上锁,但是这样会降低性能,增加实现复杂度,并且不适合在子goroutine间进行通信。而且由于是单向通信,所以只能由主goroutine向子goroutine进行通信,所以主goroutine无法精确等待子goroutine完成之后再退出。\n channel通信\nchannel通信控制基于CSP模型,避免了大量加锁解锁的性能消耗,而且比Actor模型更加灵活。而使用channel进行通信一般用的最多的还有select、for range、sync.WaitGroup等等。\n Context包\ncontext通常叫做上下文,我们通常可以将一些数据封装在context变量中。最常见的就是再网络编程下,获取到一个请求之后,可能会对这个请求开启新的子goroutine进行后续处理。所以可以将信息封装再context中用于通信和控制。\n 33. channel的底层实现 \t首先说明一点,channel本身就是一个指针,指向的是堆中分配的一个hchan的结构体。在一般情况下即没有阻塞发生的情况,sendx表示在幻想链表中chan接收的元素将会存放的索引,而recvx表示chan将会发送的数据在环形链表中所在的索引。而如果缓存满了,所以这个时候会阻塞当前的goroutine。并将含有当前goroutine的指针和要send的元素放入到sendq队列中等待被唤醒,如果是要recv被阻塞则相应的放入到sendq中。\n1 type hchan struct {\tqcount uint // total data in the queue\tdataqsiz uint // size of the circular queue\tbuf unsafe.Pointer // points to an array of dataqsiz elements\telemsize uint16\tclosed uint32\telemtype *_type // element type\tsendx uint // send index\trecvx uint // receive index\trecvq waitq // list of recv waiters\tsendq waitq // list of send waiters\t// lock protects all fields in hchan, as well as several\t// fields in sudogs blocked on this channel.\t//\t// Do not change another G\u0026#39;s status while holding this lock\t// (in particular, do not ready a G), as this can deadlock\t// with stack shrinking.\tlock mutex} 具体详解查看引用7。值得注意的一个点是因为从chan中取数据被阻塞和因为将数据放入到chan中被阻塞,两种情况唤醒时处理方式不一样。chan中取数据被阻塞,唤醒的时候会直接从发送数据的那个goroutine中将数据复制到当前被唤醒的goroutine中。不会再经过chan。减少了内存复制的开销。\n34. 读写锁底层实现 读写锁主要是读与读之间不互斥,读写与写写之间是互斥的。要了解底层实现,首先我们要了解读写锁底层结构体以及相关加锁释放锁实现:\n1 type RWMutex struct {\tw Mutex // held if there are pending writers\twriterSem uint32 // semaphore for writers to wait for completing readers\treaderSem uint32 // semaphore for readers to wait for completing writers\treaderCount int32 // number of pending readers\treaderWait int32 // number of departing readers} 整个读写锁并发控制过程如下所示:\n 如果没有写操作进入,则每个读操作都会使得readerCount加1,完成后readerCount减1.整个过程是不会阻塞的,因为读与读之间不互斥。 当由写操作进入的时候,首先会进行互斥锁阻塞其他写操作,并将readerCount修改为很小的值,从而阻塞新来的读操作。 如果写操作进入的额时候还有没有完成的读操作,则会记录这些写操作的数量,等待他们全部完成的时候,再将写操作唤醒。注意这个时候已经不会由读操作在进入。 写操作完成之后需要将readerCount置为原来的值,保证新的读操作不会被阻塞,然后唤醒之前等待的读操作,再将互斥锁释放。使得后续写操作不会被阻塞。 35. golang中的CSP思想 传统的CSP模型是用于描述两个独立的并发实体通过共享的管道进行通信的并发模型,不关注发送消息的实体而关注与发送消息时使用的channel。golang借用了process和channel的概念,但是并没有完全实现CSP模型的所有理论。process再go语言上表现就是goroutine 是实际并发执行的实体,每个实体之间是通过channel通讯来实现数据共享。\n36. uintptr和unsafe.Pointer的区别? unsafe.Pointer只是单纯的通用指针类型,用于转换不同类型指针,它不可以参与指针运算; 而uintptr是用于指针运算的,GC 不把 uintptr 当指针,也就是说 uintptr 无法持有对象, uintptr 类型的目标会被回收; unsafe.Pointer 可以和 普通指针 进行相互转换; unsafe.Pointer 可以和 uintptr 进行相互转换。 37. golang垃圾回收 \t垃圾回收的概念主要是为了回收堆上的内存空间,对于那些没有任何变量引用的对象进行回收,垃圾回收机制做的最好的还是要看java的垃圾回收机制。垃圾回收总体上分为两个步骤,第一个步骤是判断哪些内存空间是需要被回收的,第二步是选择合适的回收算法来进行回收。c,c++,Rust等都是再栈上创建的变量作用域结束后自动回收,但是通过malloc在堆上申请的需要使用free手动释放内存。而python,java,go则是自动进行垃圾回收,垃圾回收器会周期性释放已经没有引用的对象所占用的内存空间。\n\t垃圾回收器的目标:\n 防止内存泄露,最基本的目标就是防止未及时收集而造成内存泄露。 自动回收没有用的内存。 减少内存碎片的产生,重整内存空间,提高内存利用率。 第一步判断哪些对象可以回收的主要有两种方式:\n 引用计数法,引用计数实现简单,并且回收快速,不需要暂停。但是有一个缺点就是不能解决循环引用的问题。 可达性分析算法。这也是大多数回收算法所用的判断是否可以回收的算法。可达性分析算法需要一个GC Root,一般情况下GC Root选择对象是全局对象,栈上的对象(函数参数与内部变量。)。但是可达性分析算法需要暂停整个程序,即Stop the World STW。因为如果不暂停程序,可能会造成标记的过程中会出现错误,有可能有的对象重新被使用,但是却在之前被标记为回收。 第二步回收算法:\n 标记清除 造成内存碎片较多 标记整理 整理内存碎片时间较长 标记复制 内存只能用一半 golang的垃圾回收算法则是三色标记法,在不暂停程序的情况下,完成对象的可达性分析。其会将全部对象分为三类:\n 白色:未搜索的对象,在回收周期开始时所有对象都是白色,在回收周期结束时所有的白色都是垃圾对象 灰色:正在搜索的对象,但是对象身上还有一个或多个引用没有扫描 黑色:已搜索完的对象,所有的引用已经被扫描完 具体搜索过程如下:\n 初始时所有对象都是白色对象 从GC Root对象出发,扫描所有可达对象并标记为灰色,放入待处理队列 从队列取出一个灰色对象并标记为黑色,将其引用对象标记为灰色放入队列 重复上一步骤,直到灰色对象队列为空 此时所有剩下的白色对象就是垃圾对象 其优点就是不用暂停程序就可以进行回收。但是在程序垃圾对象的产生速度大于垃圾对象的回收速度时,可能导致程序中的垃圾对象越来越多而无法及时收集。\n38. 写屏障,混合写屏障 \t这两个主要是为了三色标记法和用户程序并发过程出现的问题而出现的。当三色标记收集过程中满足下面两个条件就可能出现错误回收非垃圾对象的问题。\n 条件1:某一黑色对象引用白色对象 条件2:对于某个白色对象,所有和它存在可达关系的灰色对象丢失了访问它的可达路径 常见解决方法就是使用STW,但是这个违背了三色标记设计的目的,而另外一种就是读写屏障技术。\n使用屏障技术可以使得用户程序和三色标记过程并发执行,我们只需要达成下列任意一种三色不变性:\n 强三色不变性:黑色对象永远不会指向白色对象 弱三色不变性:黑色对象指向的白色对象至少包含一条由灰色对象经过白色对象的可达路径 GC中使用的内存读写屏障技术指的是编译器会在编译期间生成一段代码,该代码在运行期间用户读取、创建或更新对象指针时会拦截内存读写操作,相当于一个hook调用,根据hook时机不同可分为不同的屏障技术。由于读屏障Read barrier技术需要在读操作中插入代码片段从而影响用户程序性能,所以一般使用写屏障技术来保证三色标记的稳健性。\n39. var _ io.Writer = (*myWriter)(nil)这样写的目的是为了什么? 主要是为检查是否实现了某个接口。例如题目中的意思就是为了看myWriter是否实现了io.Writer接口。\n40. GMP模型 \tGMP模型golang的并发调度模型。其中G表示Goroutine,M表示内核线程,P表示调度器。GMP模型的组成就是由全局协程队列,每个P所具有的本地协程队列。当前P本地协程队列中没有G,则会取全局协程队列中取。而如果全局队列中也没有,就会从其他的P的本地协程队列中偷取。\n41. 必须要手动内存对齐的情况 手动内存对齐主要是为了平台的移植。例如struct中字段顺序不同,内存占用也会不同。主要是因为在编译过程中会使用内存对齐,所以在内存中分布会有高位地位的区别。也就是不同的顺序会造成内存部分内存无法使用。而且加入程序运行在不同对齐方式的平台,那么可能会导致panic。\n42. go 栈扩容和栈缩容,以及连续栈的缺点 go的栈更新过后,从分段栈转换为连续栈。连续栈的实现方式:当检测到需要耕读哦的栈的时候,分配比原来大一倍的栈,把旧数据拷贝到新栈,释放旧栈。\n 栈扩容会将栈扩充到比前面两倍。 栈缩容发生在GC期间,缩容就是用分配一块新的内存来替换原来的,大小也是缩小一倍。 连续栈虽然解决了分段栈的2个问题,但这种实现方式也会带来其他问题:\n 更多的虚拟内存碎片。尤其是你需要更大的栈时,分配一块连续的内存空间会变得更困难 指针会被限制放入栈。在go里面不允许二个协程的指针相互指向。这会增加实现的复杂性。 43. golang 闭包 闭包 是由函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)。从下面的例子,可以看出,a,b的值会根据调用闭包函数的次数逐渐更新。\n1 func fib() func() int {\ta, b := 0, 1\treturn func() int {\ta, b = b, a+b\treturn a\t}}// 调用如下f00 := fib()fmt.Println(f00(), f00(), f00(), f00(), f00())// 输出结果是:1 1 2 3 5 闭包函数主要有两种场景。\n 闭包里没有引用环境\u0026amp;获取引用全局变量。这种场景下,其实现就是普通的函数,按照普通的函数调用方式执行闭包调用。 闭包里引用局部变量。这种场景下,才是真正的闭包(函数+引用环境),并且以一个struct{FuncAddr, LocalAddr3, LocalAddr2, LocalAddr1}结构存储该闭包,等到调用闭包时,会把该结构地址提前放置一个寄存器,闭包内部通过该寄存器访问引用环境的变量。也就是上述例子的情况。a,b被存储。 44. Goroutine什么时候会被挂起? goroutine挂起的原因有很多,这个在go源码中由详细的叙述,并且将所有的原因列了出来。如下:\n1 // package runtime\\runtime2var waitReasonStrings = [...]string{\twaitReasonZero: \u0026#34;\u0026#34;,\twaitReasonGCAssistMarking: \u0026#34;GC assist marking\u0026#34;, // GC辅助标记阶段\twaitReasonIOWait: \u0026#34;IO wait\u0026#34;, // IO阻塞等待\twaitReasonChanReceiveNilChan: \u0026#34;chan receive (nil chan)\u0026#34;, // 对未初始化的chan进行读操作\twaitReasonChanSendNilChan: \u0026#34;chan send (nil chan)\u0026#34;, // 对未初始化的chan进行写操作\twaitReasonDumpingHeap: \u0026#34;dumping heap\u0026#34;, // 对 Go Heap 堆 dump 时个的使用场景仅在 runtime.debug 时,也就是常见的 pprof 这一类采集时阻塞。\twaitReasonGarbageCollection: \u0026#34;garbage collection\u0026#34;, // 在垃圾回收时,主要场景是 GC 标记终止(GC Mark Termination)阶段时触发。\twaitReasonGarbageCollectionScan: \u0026#34;garbage collection scan\u0026#34;, // 在垃圾回收扫描时,主要场景是 GC 标记(GC Mark)扫描 Root 阶段时触发。\twaitReasonPanicWait: \u0026#34;panicwait\u0026#34;, // 在 main goroutine 发生 panic 时,会触发。\twaitReasonSelect: \u0026#34;select\u0026#34;, // 关键字 select\twaitReasonSelectNoCases: \u0026#34;select (no cases)\u0026#34;, //在调用关键字 select 时,若一个 case 都没有,会直接触发。\twaitReasonGCAssistWait: \u0026#34;GC assist wait\u0026#34;, // GC 辅助标记阶段中的结束行为,会触发。\twaitReasonGCSweepWait: \u0026#34;GC sweep wait\u0026#34;, // GC 清扫阶段中的结束行为,会触发。\twaitReasonGCScavengeWait: \u0026#34;GC scavenge wait\u0026#34;, //GC scavenge 阶段的结束行为,会触发。GC Scavenge 主要是新空间的垃圾回收,是一种经常运行、快速的 GC,负责从新空间中清理较小的对象。\twaitReasonChanReceive: \u0026#34;chan receive\u0026#34;, //在 channel 进行读操作,会触发。\twaitReasonChanSend: \u0026#34;chan send\u0026#34;, // 在 channel 进行写操作,会触发。\twaitReasonFinalizerWait: \u0026#34;finalizer wait\u0026#34;, //在 finalizer 结束的阶段,会触发。在 Go 程序中,可以通过调用 runtime.SetFinalizer 函数来为一个对象设置一个终结者函数。这个行为对应着结束阶段造成的回收。 waitReasonForceGCIdle: \u0026#34;force gc (idle)\u0026#34;, // 强制 GC(空闲时间)结束时,会触发。\twaitReasonSemacquire: \u0026#34;semacquire\u0026#34;, // 信号量处理结束时,会触发。\twaitReasonSleep: \u0026#34;sleep\u0026#34;, // 经典的 sleep 行为,会触发。 waitReasonSyncCondWait: \u0026#34;sync.Cond.Wait\u0026#34;, // 结合 sync.Cond 用法能知道,是在调用 sync.Wait 方法时所触发。\twaitReasonTimerGoroutineIdle: \u0026#34;timer goroutine (idle)\u0026#34;, // 与 Timer 相关,在没有定时器需要执行任务时,会触发。\twaitReasonTraceReaderBlocked: \u0026#34;trace reader (blocked)\u0026#34;, // 与 Trace 相关,ReadTrace会返回二进制跟踪数据,将会阻塞直到数据可用。 waitReasonWaitForGCCycle: \u0026#34;wait for GC cycle\u0026#34;, // 等待 GC 周期,会休眠造成阻塞。\twaitReasonGCWorkerIdle: \u0026#34;GC worker (idle)\u0026#34;, // GC Worker 空闲时,会休眠造成阻塞。\twaitReasonPreempted: \u0026#34;preempted\u0026#34;, // 发生循环调用抢占时,会会休眠等待调度。\twaitReasonDebugCall: \u0026#34;debug call\u0026#34;, // 调用 GODEBUG 时,会触发。} 综上所述主要的场景是:\n 通道(Channel)。 垃圾回收(GC)。 休眠(Sleep)。 锁等待(Lock)。 抢占(Preempted)。 IO 阻塞(IO Wait) 其他,例如:panic、finalizer、select 等。 45. DATA Trace是什么?怎么检测以及解决? Data Trace就是并发过程中的数据竞争问题。通常我们可以使用-trace添加到编译命令行来检测data trace情况。\n解决这种情况就是解决并发同步问题。所以可以使用sync.WaitGroup,无缓冲通道,mutex锁。\n参考 [1].Go 并发之三种线程安全的 map - 知乎 (zhihu.com)\n[2]. Golang 之 struct能不能比较 - 掘金 (juejin.cn)\n[3].Review 《JSON and Go》 - 大白的碎碎念 (bwangel.me)\n[4].https://learnku.com/docs/the-way-to-go/\n[5].简单聊聊内存逃逸 | 剑指 offer - golang - SegmentFault 思否\n[6].深入golang之\u0026mdash;goroutine并发控制与通信 - 知乎 (zhihu.com)\n[7].图解Go的channel底层实现 - 菜刚RyuGou的博客 (i6448038.github.io)\n[8].go 读写锁实现原理解读 - SegmentFault 思否\n[9].图示Golang垃圾回收机制 - 知乎 (zhihu.com)\n[10].Golang 是否有必要内存对齐? - 云+社区 - 腾讯云 (tencent.com)\n[11].【面试高频问题】线程、进程、协程 - 知乎 (zhihu.com)\n","date":"Feb 25","permalink":"https://hearecho.github.io/post/go-question/","tags":["Go","面试"],"title":"Go Question"},{"categories":null,"contents":"排序 排序算法就是将一组数据按照特定的规则进行排列。排序算法主要从时间复杂度,空间复杂度,稳定性等方面来考虑性能。稳定性:稳定性指的是相等的排序元素再经过排序之后相对顺序是否发生了改变。这个主要是防止破环原有的顺序,一般再多次使用不同的key来排序元素的时候防止破坏上次排序。\n基数排序、计数排序、插入排序、冒泡排序、归并排序是稳定排序。选择排序、堆排序、快速排序不是稳定排序。\n选择排序 选择排序就是一次从未排序的数组中选择最小或者是最大的元素与当前元素进行交换。\n稳定性 由于选择排序存在swap操作,所以选择排序是一种不稳定排序算法。\n时间复杂度和空间复杂度 时间复杂度为$O(n^2)$,没有使用额外的空间。\n实现 1 2 3 4 5 6 7 8 9 10 11 func SelectSort(nums []int) { for i := 0; i \u0026lt; len(nums); i++ { min := i for j := i; j \u0026lt; len(nums); j++ { if nums[min] \u0026gt; nums[j] { min = j } } nums[i], nums[min] = nums[min], nums[i] } } 冒泡排序 冒泡排序相比较于选择排序同样是进行交换,不过冒泡排序是将较小的元素不断的进行上浮。工作原理就是每次比较相邻的两个元素,然后入喉第i个元素小于第i+1个元素,则将两个元素进行交换,知道某一次遍历过程中没有发生交换则证明排序完成。\n稳定性 冒泡排序虽然进行了交换,但是交换是相邻的两个元素进行交换,而相同的元素不会进行交换,所以没有破坏原有的顺序。冒泡排序是一个稳定的排序算法。\n时间复杂度和空间复杂度 时间复杂度为$O(n^2)$,完全有序的时候为$O(n)$.没有使用额外的空间。\n实现 1 2 3 4 5 6 7 8 9 10 11 12 func BubbleSort(nums []int) { flag := true for flag { flag = false for i := 0; i \u0026lt; len(nums)-1; i++ { if nums[i] \u0026gt; nums[i+1] { flag = true nums[i], nums[i+1] = nums[i+1], nums[i] } } } } 插入排序 插入排序是将未排序范围内的元素,选择一个然后在插入到已经排序的元素中的正确位置。和选择排序不相同,选择排序是直接将选择的元素加在已经排序元素的最后面,而插入排序需要搜索之后才知道元素的合适位置。\n稳定性 插入排序是一种稳定的排序算法。\n时间复杂度和空间复杂度 插入排序的最优时间复杂度为 $O(n)$,在数列几乎有序时效率很高。\n插入排序的最坏时间复杂度和平均时间复杂度都为 $O(n^2)$,没有使用额外的空间。\n实现 1 2 3 4 5 6 7 8 9 10 11 12 func InsertSort(nums []int) { for i := 1; i \u0026lt; len(nums); i++ { key := nums[i] j := i - 1 for j \u0026gt;= 0 \u0026amp;\u0026amp; nums[j] \u0026gt; key { // 从后往前遍历,来查找插入位置 \tnums[j+1] = nums[j] j-- } nums[j+1] = key } } 快速排序 快排的工作原理就是将数组化作两个部分,然后对每个部分再次进行递归排序,因为在划分的过程已经进行了排序,所以不需要进行合并,此时的数列已经完全有序了。快排在选择哨兵的方法和遍历交换过程都有几种实现方法。\n稳定性 快排是一种不稳定的排序算法。\n时间复杂度和空间复杂度 快速排序的最优时间复杂度和平均时间复杂度为 $O(nlogn)$,最坏时间复杂度为 $O(n^2)$。\n基础实现 基础实现,哨兵选择第一个元素,然后分别使用两个指针从后和从前找到元素进行交换。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func quickSort(nums []int, l, h int) { // 第一种 使用双指针从两端进行遍历 \t// 哨兵的话使用 nums[l] \tif l \u0026gt;= h { return } i, j := l, h watch := nums[l] for i \u0026lt; j { for i \u0026lt; j \u0026amp;\u0026amp; nums[j] \u0026gt;= watch { j-- } for i \u0026lt; j \u0026amp;\u0026amp; nums[i] \u0026lt;= watch { i++ } nums[i], nums[j] = nums[j], nums[i] } nums[i], nums[l] = nums[l], nums[i] quickSort(nums, i+1, h) quickSort(nums, l, i-1) } 优化思想 通过 三数取中(即选取第一个、最后一个以及中间的元素中的中位数) 的方法来选择两个子序列的分界元素(即比较基准)。这样可以避免极端数据(如升序序列或降序序列)带来的退化; 当序列较短时,使用 插入排序 的效率更高; 每趟排序后,将与分界元素相等的元素聚集在分界元素周围,这样可以避免极端数据(如序列中大部分元素都相等)带来的退化。 三路快排 三路快速排序是快速排序和 基数排序的混合。三路快排在选取分界点$M$之后,会将待排序数列划分为三个部分:小于$M$,等于$M$,以及大于$M$。从原理就可以看出三路快排在处理重复元素较多的数组中,效率远远高于原始的快速排序。其最佳时间复杂度为$O(n)$。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 func ThreeQuikcSort(nums []int, l, h int) { if l \u0026gt;= h { return } random_index := rand.Intn(h-l) + l pivot := nums[random_index] nums[l], nums[random_index] = pivot, nums[l] i, j, k := l+1, l, h+1 // i表示等于,j表示小于,k表示大于 \tfor i \u0026lt; k { if nums[i] \u0026lt; pivot { nums[i], nums[j+1] = nums[j+1], nums[i] i++ j++ } else if nums[i] \u0026gt; pivot { // 这个时候由于我们是从前往后进行遍历,所以不修改i \tnums[i], nums[k-1] = nums[k-1], nums[i] k-- } else { // 相等的时候 \ti++ } } nums[l], nums[j] = nums[j], nums[l] ThreeQuikcSort(nums, l, j-1) ThreeQuikcSort(nums, k, h) } 归并排序 归并排序的主要步骤就是分割之后合并。最重要的部分就是将两个有序的数组合并为一个有序的数组。其主要的三个步骤为:\n 将当前数组段划分为两个部分; 递归地分别对两个子学列进行归并排序。 将子序列进行合并 稳定性 归并排序是一种稳定的排序算法。\n时间复杂度和空间复杂度 快速排序的最优时间复杂度和平均时间复杂度为 $O(nlogn)$,空间复杂度为 $O(n)$。\n","date":"Dec 30","permalink":"https://hearecho.github.io/post/%E6%8E%92%E5%BA%8F/","tags":["排序"],"title":"排序"},{"categories":null,"contents":"单调队列是一种特殊的数据结构,类比于队列,它具有一种单调性,即队列中的元素单调递增或者单调递减。\n单调队列 我们理解单调队列之前先要理解什么是双端队列,一般的队列只能一端进一端出,而双端队列则是在两端都可以进行进队出队的操作。\n而单调队列的入队方式同样也是在队尾添加元素,不过需要将前面的比其小的元素都删除。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 type dqueue []int func (d *dqueue) Push(x int) { for len(*d) != 0 \u0026amp;\u0026amp; (*d)[len(*d)-1] \u0026lt; x { // 移除前面比该值小的数 \t*d = (*d)[:len(*d)-1] } *d = append(*d, x) } func (d *dqueue) Max() int { if len(*d) == 0 { return -1 } return (*d)[0] } func (d *dqueue) Pop(n int) { if len(*d) != 0 \u0026amp;\u0026amp; (*d)[0] == n { *d = (*d)[1:] } } 例题: 滑动窗口最大值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func maxSlidingWindowQueue(nums []int, k int) []int { // 使用单调队列或者是 优先队列 优先队列存储的索引,但是按照的是最大值进行的排序 \tres := make([]int, 0) i, j := 0, 0 q := make(dqueue, 0) for j \u0026lt;= len(nums) { if j-i \u0026lt; k { q.Push(nums[j]) } else { t := nums[i] res = append(res, q.Max()) q.Pop(t) if j \u0026lt; len(nums) { q.Push(nums[j]) } i++ } j++ } return res } 单调栈 单调栈和单调队列类似,也是为了维持单调性,所以在插入的过程中需要删除一些元素。例如如果一个栈中暂时有数据{81,45,11,0},那么要插入14的时候,就需要先把0,11出栈。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 type Monotonstack []int func (m *Monotonstack) Push(x int) { for len(*m) \u0026gt; 0 \u0026amp;\u0026amp; (*m)[len(*m)-1] \u0026lt; x { *m = (*m)[:len(*m)-1] } *m = append(*m, x) } // 之所以传入值 是因为可能需要取出的值已经不存在了 func (m *Monotonstack) Pop(x int) { if x == (*m)[len(*m)-1] { *m = (*m)[:len(*m)-1] } } 并查集 并查集是一种树形的数据结构,但是存储是使用数组进行存储的。主要用于处理一些不交集的合并及查询问题。\n初始化 初始化就是初始化一个数组,并且本身在自己的集合之中。即fa[i]=i。\n1 2 3 4 5 func makeSet(size int) { for i:=0;i\u0026lt;size;i++ { fa[i] = i } } 查找 通俗地讲一个故事:几个家族进行宴会,但是家族普遍长寿,所以人数众多。由于长时间的分离以及年龄的增长,这些人逐渐忘掉了自己的亲人,只记得自己的爸爸是谁了,而最长者(称为「祖先」)的父亲已经去世,他只知道自己是祖先。为了确定自己是哪个家族,他们想出了一个办法,只要问自己的爸爸是不是祖先,一层一层的向上问,直到问到祖先。如果要判断两人是否在同一家族,只要看两人的祖先是不是同一人就可以了。\n1 2 3 4 5 6 7 func find(x int) int{ if fa[x] == x { return x } return find(fa[x]) } // 查找就是查找给定角色的祖先 路径压缩 如果只是需要查找祖先,则这样查找有较多的浪费,所以进行路径压缩,将路径上的各个节点都直接连接欸在根节点上。\n1 2 3 4 5 6 func find(X int) int { if fa[x] != x { fa[x] = find(fa[x]) } return fa[x] } 合并 简单合并 两个不相交的并查集合并成为一个并查集,简单的合并就是让一个祖先成为另一个祖先的儿子。\n1 2 3 4 5 func union(x, y int) { x = find(x) y = find(y) fa[x] = y } 按照秩合并 1 2 3 4 5 6 7 8 9 10 11 func union(x,y int) { xx, yy := find(x), find(y) if (xx == yy) { return } if size[xx] \u0026gt; size[yy] { xx, yy = yy, xx } fa[xx] = yy size[yy] = size[xx] + size[yy] } 引用 并查集 - OI Wiki (oi-wiki.org) ","date":"Dec 24","permalink":"https://hearecho.github.io/post/%E7%89%B9%E6%AE%8A%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/","tags":null,"title":"特殊的数据结构"},{"categories":null,"contents":"树 树是一个大类 包括二叉树,二叉搜索树,AVL树,红黑树,N叉树等等。树的算法题大多都可以使用递归进行解决。\n二叉树 二叉树是指的是节点有小于等于两个出度的树,二叉树算是最基本的树,很多算法题目也是在二叉树的基础上出题。\n二叉树的遍历 二叉树的主要有四种遍历方式,前序遍历,中序遍历,后序遍历以及层次遍历。前中后序遍历指的都是根节点在遍历过程中的顺序位置。\n前序遍历 前序遍历的顺序就是中前后,先访问根节点,之后再递归访问左子树最后是右子树。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 // 递归实现 func preorderRecursive(root *TreeNode, res *[]int) { //递归方式 \tif root == nil { return } *res = append(*res, root.Val) preorderRecursive(root.Left, res) preorderRecursive(root.Right, res) } // 使用栈来代替系统栈 func preorder(root *TreeNode) []int { //使用栈来代替系统栈 \t//刚好和后续相反,只不过在加入栈的时候就进行了访问 \tstack := make([]*TreeNode, 0) res := make([]int, 0) for len(stack) != 0 || root != nil { //遍历左子树 包括根节点 \tfor root != nil { stack = append(stack, root) res = append(res, root.Val) root = root.Left } //弹出 \troot = stack[len(stack)-1] stack = stack[:len(stack)-1] root = root.Right } return res } 中序遍历 中序遍历则是先访问左边节点之后是根节点,最后才是右子树。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 // 递归 func inorderRecursive(root *TreeNode, res *[]int) { //递归方式 \tif root == nil { return } preorderRecursive(root.Left, res) *res = append(*res, root.Val) preorderRecursive(root.Right, res) } // 使用栈 func inorderTraversal(root *TreeNode) []int { res := make([]int, 0) stack := make([]*TreeNode, 0) //由于右子树可能为空 \tfor len(stack) != 0 || root != nil { //遍历左子树 \tfor root != nil { stack = append(stack, root) root = root.Left } //取出栈顶节点 \troot = stack[len(stack)-1] res = append(res, root.Val) stack = stack[:len(stack)-1] root = root.Right } return res } 后序遍历 后序遍历则是根节点最后访问,访问左子树之后紧跟着访问右子树。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 // 递归访问 func postorderRecursive(root *TreeNode, res *[]int) { //递归方式 \tif root == nil { return } preorderRecursive(root.Left, res) preorderRecursive(root.Right, res) *res = append(*res, root.Val) } // 使用栈 func postorderTraversal(root *TreeNode) []int { stack := make([]*TreeNode, 0) res := make([]int, 0) var prev *TreeNode for len(stack) != 0 || root != nil { for root != nil { stack = append(stack, root) root = root.Left } root = stack[len(stack)-1] stack = stack[:len(stack)-1] if root.Right == nil || root.Right == prev { res = append(res, root.Val) prev = root root = nil } else { stack = append(stack, root) root = root.Right } } return res } 层次遍历 层次遍历则是使用队列或者是叫做广度优先遍历,一层一层的访问所有的节点,一般层次遍历类型的题目较为简单。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func levelOrder(root *TreeNode) [][]int { //层次遍历 \tqueue := make([]*TreeNode, 0) res := make([][]int, 0) queue = append(queue, root) for len(queue) != 0 \u0026amp;\u0026amp; root != nil { temp := make([]int, 0) size := len(queue) for i := 0; i \u0026lt; size; i++ { temp = append(temp, queue[i].Val) if queue[i].Left != nil { queue = append(queue, queue[i].Left) } if queue[i].Right != nil { queue = append(queue, queue[i].Right) } } queue = queue[size:] res = append(res, temp) } return res } 二叉树着色游戏 这道题如果搞清楚什么情况才会赢是很简单的。因为最后比较的是着色的数量。所以二号玩家着色的位置只有三个位置:\n 一号玩家着色节点的左子树,想赢的话肯定要选择左孩子 一号玩家着色节点的右子树,右孩子 除了上述位置的其他位置,直接一号玩家着色节点的父节点 所以我们只需要统计一下这三个位置的节点数即可。然后只要该位置节点数大于另外两个位置节点数就可以了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func btreeGameWinningMove(root *TreeNode, n int, x int) bool { if nil == root { return false } xLeftSum, xRightSum := 0, 0 //x节点的左子树和右子树节点个数 sum := execute(root, x, \u0026amp;xLeftSum, \u0026amp;xRightSum) other := sum - xLeftSum - xRightSum - 1 //其余节点个数 return other \u0026gt; xLeftSum + xRightSum || xLeftSum \u0026gt; other + xRightSum || xRightSum \u0026gt; other + xLeftSum } func execute(root *TreeNode, x int, xLeftSum, xRightSum *int) (int) { if nil == root { return 0 } left := execute(root.Left, x, xLeftSum, xRightSum) right := execute(root.Right, x, xLeftSum, xRightSum) if root.Val == x { *xLeftSum = left *xRightSum = right } return 1 + left + right } 二叉树对称、镜像类型的题目 [对称二叉树](101. 对称二叉树 - 力扣(LeetCode) (leetcode-cn.com)) 给定一个二叉树,检查它是否是镜像对称的。\n 对称二叉树说的是整个树的对称,所以需要判断两种情况\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 func isSymmetric(root *TreeNode) bool { //对称是整个树的对称 if root == nil { return true } return Symmetric(root.Left, root.Right) } func Symmetric(left, right *TreeNode) bool { if left == nil \u0026amp;\u0026amp; right == nil { return true } if left == nil || right { return false } if left.Val != right.Val { return false } return Symmetric(left.Left,right.Right) \u0026amp;\u0026amp; Symmetric(left.Right, right.Left) } // 迭代的方式 // 迭代的方式就是通过将需要比较的节点暂时存储到队列中,之后再从队列中取出进行比较。 func isSymmetric(root *TreeNode) bool { u, v := root, root q := []*TreeNode{} q = append(q, u) q = append(q, v) for len(q) \u0026gt; 0 { u, v = q[0], q[1] q = q[2:] // 判断的部分没有进行改变 if u == nil \u0026amp;\u0026amp; v == nil { continue } if u == nil || v == nil { return false } if u.Val != v.Val { return false } q = append(q, u.Left) q = append(q, v.Right) q = append(q, u.Right) q = append(q, v.Left) } return true } 翻转二叉树 翻转二叉树类似于镜像二叉树,翻转是从最后的叶子节点开始进行反转.所以是先进行递归后续进行\n1 2 3 4 5 6 7 8 9 10 func invertTree(root *TreeNode) *TreeNode { if root == nil { return nil } left := invertTree(root.Left) right := invertTree(root.Right) root.Left = right root.Right = left return root } 与二叉树的深度相关的题目 一般情况下与二叉树深度相关的题目也不会太难。\n二叉树的最大深度 判断二叉树的最大深度即判断每个根节点到叶子节点的最长路径上的节点数,即遇到nil节点返回0,其他加1.\n1 2 3 4 5 6 func maxDepth(root *TreeNode) int { if root==nil { return 0 } return max(maxDepth(root.Left), maxDepth(root.Right)) + 1 } 二叉树的最小深度 不同于二叉树的最大深度,最小深度如果使用最大深度的那种做法,则在单边树则会出现问题,更加稳健的做法应该是将问题缩小为判断左子树和右子树,并且不计算nil节点。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func minDepth(root *TreeNode) int { if root == nil { return 0 } if root.Left == nil \u0026amp;\u0026amp; root.Right == nil { return 1 } minD := math.MaxInt32 if root.Left != nil { minD = min(minDepth(root.Left), minD) } if root.Right != nil { minD = min(minDepth(root.Right), minD) } return minD + 1 } 平衡二叉树 平衡二叉树的定义是一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。所以说可以通过判断左右子树的深度差来判断该二叉树是否为平衡二叉树。\n1 2 3 4 5 6 7 8 9 10 11 func isBalanced(root *TreeNode) bool { if root == nil { return true } left := depth(root.Left) right := depth(root.Right) if abs(left-right) \u0026gt; 1 { return false } return isBalanced(root.Left) \u0026amp;\u0026amp; isBalanced(root.Right) } 二叉树的直径 一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。但是这个这个最长路径所经过的节点一定位于一个节点的左右子树,除非该节点没有左右子树。所以我们只需要计算该节点左右子树的最大深度之后相加即为直径的长度\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func diameterOfBinaryTree(root *TreeNode) int { res := 0 var depth func(t *TreeNode) int depth = func(t *TreeNode) int { if t == nil { return 0 } left := depth(t.Left) right := depth(t.Right) res = max(res, left + right-2) return max(left, right) + 1 } depth(root) return res } 祖先问题 二叉树的公共祖先 公共祖先的问题有一个基本点就是自己可以算作自己的祖先,那么我们就可以按个搜索目标节点之后返回不为nil的那一只节点。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode { if root ==nil || root == p || root==q { return root } left := lowestCommonAncestor(root.Left, p, q) right := lowestCommonAncestor(root.Right, p, q) if left == nil { return right } if right == nil { return left } return root } 二叉树路径问题 二叉树的所有路径 路径一般指的是根节点到叶子节点的节点顺序。而寻找路径的情况下最多使用的方式是回溯,减少重复计算。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func binaryTreePaths(root *TreeNode) []string { res := make([]string,0) var bk func(t *TreeNode, cur []int) bk = func(t *TreeNode, cur []int) { if t.Left == nil \u0026amp;\u0026amp; t.Right == nil { // 回溯截止 temp := \u0026#34;\u0026#34; for _,v := range cur { temp += (strconv.Itoa(v) + \u0026#34;-\u0026gt;\u0026#34;) } res = append(res, temp + strconv.Itoa(t.Val)) } if t.Left != nil { bk(t.Left, append(cur, t.Val)) } if t.Right != nil { bk(t.Right, append(cur, t.Val)) } } bk(root, make([]int,0)) return res } 二叉搜索树 二叉搜索树(BST)是当前节点的值大于等于其左子树的值,小于等于其右子树的值。所以二叉搜索树中序遍历结果是有序递增的。二分查找的搜索顺序就是一颗二分搜索树。\n二叉搜索树搜索 相当于是对一个有序数组进行二分查找。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func searchBST(root *TreeNode, val int) *TreeNode { if root == nil { return nil } for root != nil { temp := root.Val if temp == val { return root } else if temp \u0026gt; val { root = root.Left } else { root = root.Right } } return nil } 创建最小高度的树 从有序数组中创建高度最小树,则使用二分搜索创建即可,根节点的左右子树高度应该尽可能的相同。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func sortedArrayToBST(nums []int) *TreeNode { // 高度最小 则应该是创建为 二分搜索树 var build func(l,h int) *TreeNode build = func(l,h int) *TreeNode { if l \u0026gt; h { return nil } mid := l + (h-l) / 2 root := \u0026amp;TreeNode{Val: nums[mid]} root.Left = build(l, mid-1) root.Right = build(mid+1, h) return root } return build(0,len(nums)-1) } 不同的二叉搜索树 二叉搜索树的性质就是左子树都比根节点小,右子树都比根节点大。我们可以知道随意给一个序列生成二叉搜索树的方式,那么我们可以随机选择一个点,然后一次作为分割,在两边生成不同的二叉搜索树。然后在整合到根节点上。注意:两边的二叉搜索树可能有多个,并不是必定一个\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 func generateTrees(n int) []*TreeNode { if n == 0 { return nil } return build(1, n) } func build(l, h int) []*TreeNode { if l \u0026gt; h { return []*TreeNode{nil} } allTrees := []*TreeNode{} for i:=l;i\u0026lt;=h;i++ { // 在这个区间中枚举所有可以为根节点 leftTrees := build(l, i-1) rightTrees := build(i+1, h) for _,left := range leftTrees { for _, right := range rightTrees { root := \u0026amp;TreeNode{Val: i} root.Left = left root.Right = right allTrees = append(allTrees, root) } } } return allTrees } 如果不需要具体的二叉搜索树只需要能够生成的数量,则可以使用动态规划。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func numTrees(n int) int { // 一个节点只能够构成一个 二叉搜索树 // 两个节点可以构成两个二叉搜索树 // 三个节点的话 就是分别枚举所有可能的根节点,然后左子树的个数乘以右子树的个数 // 如果为空个数设置为1 即dp[0] = 1 // 状态转移方程 dp[i] = dp[i-1] * dp[n-i] dp := make([]int, n+1) dp[0] = 1 dp[1] = 1 for i:=2;i\u0026lt;=n;i++ { for j:=1;j\u0026lt;=i;j++ { dp[i] += dp[j-1] * dp[i-j] } } return dp[n] } ","date":"Dec 10","permalink":"https://hearecho.github.io/post/%E6%A0%91/","tags":["算法","面试","树"],"title":"树"},{"categories":null,"contents":"Go常见错误 原文链接:50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs \n初级 不熟悉Go语言可能会犯的错误\n 1. 大括号问题 在大多数语言中,我们都可以将大括号放在任意位置,但是Go不同,Go不能将左括号放到新的一行。同时Go和Python相同是不需要分号的(即使含有分号也不会报错)。示例如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package main import \u0026#34;fmt\u0026#34; func main() { //error, 不能在新的一行放置新的括号,必须紧跟函数之后 fmt.Println(\u0026#34;hello there!\u0026#34;) } // 错误信息 // syntax error: unexpected semicolon or newline before { // 正确语句 package main import \u0026#34;fmt\u0026#34; func main() { fmt.Println(\u0026#34;hello there!\u0026#34;) } 2. 未使用的变量 在Go中如果出现没有被使用的变量会无法完成编译。如果在函数中声明了变量则必须使用,但是全局变量不适用则不会出现问题。如果将一个新的值分配给一个未使用的变量并不算做使用该变量。示例如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package main var gvar int //not an error func main() { var one int //error, unused variable two := 2 //error, unused variable var three int //error, even though it\u0026#39;s assigned 3 on the next line three = 3 func(unused string) { fmt.Println(\u0026#34;Unused arg. No compile error\u0026#34;) }(\u0026#34;what?\u0026#34;) } //正常代码 想办法“使用”变量 当然如果这个变量确实一点用处都没有,则可以考虑移除 package main import \u0026#34;fmt\u0026#34; func main() { var one int _ = one two := 2 fmt.Println(two) var three int three = 3 one = three var four int four = four } 3.未使用的导入的包 在使用集成开发环境的时间,一般包都是自动导自动删除的,所以这个问题一般不会出现。Go也不会允许出现未使用的包,一般情况我们都会将不适用的包删除或者是注释掉。但是在一些特殊的情况,却需要只导入包,但是并不使用他(例如连接数据库的包),所以我们一般情况下使用,_,作为包的名字(别名)。使用goimports可以直接对文件进行处理。示例如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;time\u0026#34; ) func main() { } // 错误信息 // imported and not used: \u0026#34;fmt\u0026#34; // imported and not used: \u0026#34;log\u0026#34; // imported and not used: \u0026#34;time\u0026#34; // 正确代码 package main import ( _ \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;time\u0026#34; ) var _ = log.Println func main() { _ = time.Now } 4. 短声明只能在函数内部使用 Go中声明方式有两种,一种是短声明,另外一种是正常声明。短声明只能在函数内部使用,所以说全局变量一般是使用正常声明方式。示例如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package main myvar := 1 //error func main() { } //错误信息 //non-declaration statement outside function body // 正确声明 package main var myvar = 1 func main() { } 5. 使用短声明对变量进行了重新声明 在Go中我们不能对一个变量重新声明,即使重新声明是相同的数据类型。但在至少声明一个新变量的多变量声明中是允许的。示例如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package main func main() { one := 0 one := 1 //error } // 错误信息 // no new variables on left side of := // 正确声明 package main func main() { one := 0 one, two := 1,2 one,two = two,one } 6. 不能使用短声明来填充结构体中字段变量 短声明是不可以直接用于结构体中的字段变量,一般情况下我们都是使用临时变量先赋值后声明。示例如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package main import ( \u0026#34;fmt\u0026#34; ) type info struct { result int } func work() (int,error) { return 13,nil } func main() { var data info data.result, err := work() //error fmt.Printf(\u0026#34;info: %+v\\n\u0026#34;,data) } // 错误信息 // non-name data.result on left side of := // 正确示例 package main import ( \u0026#34;fmt\u0026#34; ) type info struct { result int } func work() (int,error) { return 13,nil } func main() { var data info var err error data.result, err = work() //ok if err != nil { fmt.Println(err) return } fmt.Printf(\u0026#34;info: %+v\\n\u0026#34;,data) //prints: info: {result:13} } 7. 意外隐藏变量 短声明语法是非常方便,因此很容易将其视为常规赋值操作。如果您在新代码块中犯此错误,则不会出现编译器错误,但您的应用程序将不会按照您的预期运行。这是一个非常普通的陷阱,它很容易出错但是不容易被发现,您可以使用 vet 命令来查找其中一些问题。默认情况下,vet 不会执行任何隐藏变量检查。确保使用 -shadow 标志:go tool vet -shadow your_file.go。请注意, vet 命令不会报告所有隐藏的变量。使用 go-nyet 进行更积极的隐藏变量检测。示例如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package main import \u0026#34;fmt\u0026#34; func main() { x := 1 fmt.Println(x) //prints 1 // 代码块所以 第一条错误在这里不会出现 { fmt.Println(x) //prints 1 x := 2 fmt.Println(x) //prints 2 } fmt.Println(x) //prints 1 (bad if you need 2) } 8. 不能使用nil来初始化没有显式类型的变量 例如接口、函数、指针、哈希表、切片和通道的默认值是nil,但是如果我们没有指明一个变量的类型却将nil赋值或用于其初始化则是行不通的,因为Go无法推断出他的类型。示例如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package main func main() { var x = nil //error _ = x } // 错误信息 // use of untyped nil // 正确示例 package main func main() { var x interface{} = nil _ = x } 9. 使用“nil”的切片和哈希表 直接向\u0026quot;nil\u0026quot;切片中添加元素是没有问题的,但是向\u0026quot;nil\u0026quot;的哈希表中添加元素会产生运行时错误。示例如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 package main func main() { var m map[string]int m[\u0026#34;one\u0026#34;] = 1 //error } // slice package main func main() { var s []int s = append(s,1) } 10. 哈希表容量 我们在创建哈希表的时候可以指定哈希表的容量,但是函数cap()无法在哈希表上使用。示例如下:\n1 2 3 4 5 6 7 8 9 package main func main() { m := make(map[string]int,99) cap(m) //error } // 错误信息 // invalid argument m (type map[string]int) for cap // cap接收的参数是 Array、Pointer、Slice、 Channel 11. 字符串不可以被初始化赋值为nil 字符串的默认空值是\u0026quot;\u0026quot;,而不是nil。示例如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func main() { var x string = nil //error if x == nil { //error x = \u0026#34;default\u0026#34; } } // 错误信息 // cannot use nil as type string in assignment // invalid operation: x == nil (mismatched types string and nil) // 正确做法 func main() { var x string //defaults to \u0026#34;\u0026#34; (zero value) if x == \u0026#34;\u0026#34; { x = \u0026#34;default\u0026#34; } } 12. 数组参数 数组作为函数参数的时候是值复制,所以在函数内部修改数组的值是不会产生同步的修改。如果想要达到在函数内部的修改可以同步,则可以使用指针或者是使用切片。示例如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 func main() { x := [3]int{1,2,3} func(arr [3]int) { arr[0] = 7 fmt.Println(arr) //prints [7 2 3] }(x) fmt.Println(x) //prints [1 2 3] (not ok if you need [7 2 3]) } // 正确用法 // 使用指针 func main() { x := [3]int{1,2,3} func(arr *[3]int) { (*arr)[0] = 7 fmt.Println(arr) //prints \u0026amp;[7 2 3] }(\u0026amp;x) fmt.Println(x) //prints [7 2 3] } // 使用切片 实际上切片也是传入的指针参数 func main() { x := []int{1,2,3} func(arr []int) { arr[0] = 7 fmt.Println(arr) //prints [7 2 3] }(x) fmt.Println(x) //prints [7 2 3] } 13. 使用range遍历数组和切片时候的意外值 使用range遍历数组和切片的时候是返回索引和该索引对应的值一组键值对。第二位才是我们需要的值。第一位是索引。示例如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func main() { x := []string{\u0026#34;a\u0026#34;,\u0026#34;b\u0026#34;,\u0026#34;c\u0026#34;} for v := range x { fmt.Println(v) //prints 0, 1, 2 } } // 正确用法 func main() { x := []string{\u0026#34;a\u0026#34;,\u0026#34;b\u0026#34;,\u0026#34;c\u0026#34;} for _, v := range x { fmt.Println(v) //prints a, b, c } } 14. 数组和切片都是一维的 看起来Go支持多维数组和切片,但它并不支持。不过创建数组的数组或切片的切片是可能的。对于依赖动态多维数组的数值计算应用来说,在性能和复杂性方面都远非理想。你可以使用原始一维数组、\u0026ldquo;独立 \u0026ldquo;切片和 \u0026ldquo;共享数据 \u0026ldquo;切片来构建动态多维数组。如果你使用的是原始的一维数组,你要负责索引、边界检查,以及当数组需要增长时的内存重新分配。使用 \u0026ldquo;独立 \u0026ldquo;切片创建一个动态多维数组是一个两步过程。首先,你必须创建外层片。然后,你必须分配每个内片。内片是相互独立的。你可以在不影响其他内片的情况下增长和缩小它们。示例如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 func main() { x := 2 y := 4 table := make([][]int,x) for i:= range table { table[i] = make([]int,y) } } // 但是对于其他语言来说,底层是数据连续的层次 // 证明 func main() { h, w := 2, 4 raw := make([]int, h*w) for i := range raw { raw[i] = i } fmt.Println(raw, \u0026amp;raw[3], \u0026amp;raw[4]) //prints: [0 1 2 3 4 5 6 7] 0xc000010318 0xc000010320 table := make([][]int, h) for i := range table { table[i] = raw[i*w : i*w+w] } fmt.Println(table, \u0026amp;table[0][3], \u0026amp;table[1][0]) // prints [[0 1 2 3] [4 5 6 7]] 0xc000010318 0xc000010320 } 15. 访问不存在的哈希表键 一般情况下我们期望访问不存在的哈希表键的时候期望能够返回值为nil,但是返回nil的情况是那些value值的默认值为nil,但是还有很多数据类型的默认值不为nil。所以正确的做法是先辨别哈希表中是否存在哈希表键,之后再进行处理。示例如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // 错误示范 func main() { x := map[string]string{\u0026#34;one\u0026#34;:\u0026#34;a\u0026#34;,\u0026#34;two\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;three\u0026#34;:\u0026#34;c\u0026#34;} if v := x[\u0026#34;two\u0026#34;]; v == \u0026#34;\u0026#34; { //incorrect fmt.Println(\u0026#34;no entry\u0026#34;) } } // 正确做法 func main() { x := map[string]string{\u0026#34;one\u0026#34;:\u0026#34;a\u0026#34;,\u0026#34;two\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;three\u0026#34;:\u0026#34;c\u0026#34;} if _,ok := x[\u0026#34;two\u0026#34;]; !ok { fmt.Println(\u0026#34;no entry\u0026#34;) } } 16. 字符串是不可变类型 尝试直接更新字符串中某个字符是不可行的,字符串作为不可变类型是只能读取但是不能修改。如果需要更新一个字符串,则可以选择先将其转换为字节切片,之后在需要的时候转换为字符串。示例如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 //错误示范 func main() { x := \u0026#34;text\u0026#34; x[0] = \u0026#39;T\u0026#39; fmt.Println(x) } //错误信息 //annot assign to x[0] //正确示范 func main() { x := \u0026#34;text\u0026#34; xbytes := []byte(x) xbytes[0] = \u0026#39;T\u0026#39; fmt.Println(string(xbytes)) //prints Text } *注意:*这对于文本字符串来说并不是一个好的更新方式,因为文本字符串会存储在多个字节中。如果确实需要更新文本字符串,可以先将其转换为rune切片,但是即使使用rune切片也有可能会出现占据多个rune的情况。\n17. 字符串和字节切片之间的转换 当你将一个字符串转换为一个字节片时(反之亦然),你会得到一个原始数据的完整拷贝。这不像其他语言中的转换操作,也不像重新切分那样,新的切分变量指向原始字节切分所使用的同一个底层数组。\nGo确实对[]byte到string和string到[]byte的转换进行了一些优化,以避免额外的分配(在todo列表中还有更多优化)。\n第一个优化避免了在map[string]集合中使用[]byte键来查找条目时的额外分配:m[string(key)]。\n第二个优化避免了string被转换为[]byte的for range子句中的额外分配:for i,v := range []byte(str) {...}.\n18. 字符串与索引操作 直接使用索引操作得到是一个byte值而不是字符。示例如下:\n1 2 3 4 5 func main() { x := \u0026#34;text\u0026#34; fmt.Println(x[0]) //print 116 fmt.Printf(\u0026#34;%T\u0026#34;,x[0]) //prints uint8 } 如果想要得到字符,则可以使用for range短语,官方的 \u0026ldquo;unicode/utf8 \u0026ldquo;包和实验性的utf8string包(golang.org/x/exp/utf8string)也很有用。utf8string包包括一个方便的At()方法。将字符串转换为符文片也是一种选择。\n19. 字符串不一定都是UTF8文本 字符串值不要求是UTF8文本。它们可以包含任意的字节。只有在使用字符串字面的时候,字符串才是UTF8的。即使如此,它们也可以使用转义序列包含其他数据。要知道你是否有一个UTF8文本字符串,请使用 \u0026ldquo;unicode/utf8 \u0026ldquo;包中的ValidString()函数。 示例如下:\n1 2 3 4 5 6 7 func main() { data1 := \u0026#34;ABC\u0026#34; fmt.Println(utf8.ValidString(data1)) //prints: true data2 := \u0026#34;A\\xfeC\u0026#34; fmt.Println(utf8.ValidString(data2)) //prints: false } 20. 字符串长度 Go内置的len()函数返回值字节的数量而不是python中的字符的数量。为了得到字符的数量我们一般使用\u0026quot;unicode/utf8\u0026quot;包中的RuneCountInString()函数。示例如下:\n1 2 3 4 func main() { data := \u0026#34;♥\u0026#34; fmt.Println(utf8.RuneCountInString(data)) } *注意:*这个和上面转换一样存在意外情况就是字符串中含有é等类似字符,因为其占用两个字符。\n21. 在多行表示的切片,数组和哈希表中缺少逗号 在单行的时候,最后一个元素后面可以不用跟符号(多了也不会出现错误),但是多行不可以,每一行最后都需要逗号。示例如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func main() { x := []int{ 1, 2 //error } _ = x } // 错误信息 // syntax error: need trailing comma before // 正确示例 func main() { x := []int{ 1, 2, } x = x y := []int{3,4,} //no error y = y } 22. log.Fatal and log.Panic会停止程序 log.Fatal and log.Panic会让打断程序。示例如下:\n1 2 3 4 func main() { log.Fatalln(\u0026#34;Fatal Level: log entry\u0026#34;) //app exits here log.Println(\u0026#34;Normal Level: log entry\u0026#34;) } 23. 内置的数据结构不是同步的 Go的内置数据结构都不支持并发,但是可以使用携程和通道来实现原子操作。\n24. Go的计算优先级不太相同 在Go中位运算符的优先级是高于基础运算符(加减乘除)。示例如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 func main() { fmt.Printf(\u0026#34;0x2 \u0026amp; 0x2 + 0x4 -\u0026gt; %#x\\n\u0026#34;,0x2 \u0026amp; 0x2 + 0x4) //prints: 0x2 \u0026amp; 0x2 + 0x4 -\u0026gt; 0x6 //Go: (0x2 \u0026amp; 0x2) + 0x4 //C++: 0x2 \u0026amp; (0x2 + 0x4) -\u0026gt; 0x2 fmt.Printf(\u0026#34;0x2 + 0x2 \u0026lt;\u0026lt; 0x1 -\u0026gt; %#x\\n\u0026#34;,0x2 + 0x2 \u0026lt;\u0026lt; 0x1) //prints: 0x2 + 0x2 \u0026lt;\u0026lt; 0x1 -\u0026gt; 0x6 //Go: 0x2 + (0x2 \u0026lt;\u0026lt; 0x1) //C++: (0x2 + 0x2) \u0026lt;\u0026lt; 0x1 -\u0026gt; 0x8 fmt.Printf(\u0026#34;0xf | 0x2 ^ 0x2 -\u0026gt; %#x\\n\u0026#34;,0xf | 0x2 ^ 0x2) //prints: 0xf | 0x2 ^ 0x2 -\u0026gt; 0xd //Go: (0xf | 0x2) ^ 0x2 //C++: 0xf | (0x2 ^ 0x2) -\u0026gt; 0xf } 25. 协程还在运行程序便退出 基本上如果不增加操作,程序是不会主动等待协程完成之后才会退出的。在Go中最基本的方式是使用WaitGroup来使得主进程等待所有的协程完成。示例如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 // 不做操作 func main() { workerCount := 2 for i := 0; i \u0026lt; workerCount; i++ { go doit(i) } time.Sleep(1 * time.Second) fmt.Println(\u0026#34;all done!\u0026#34;) } func doit(workerId int) { fmt.Printf(\u0026#34;[%v] is running\\n\u0026#34;,workerId) time.Sleep(3 * time.Second) fmt.Printf(\u0026#34;[%v] is done\\n\u0026#34;,workerId) } //运行结果 /** [0] is running [1] is running all done! */ // 使用WaitGroup func main() { var wg sync.WaitGroup done := make(chan struct{}) wq := make(chan interface{}) workerCount := 2 for i := 0; i \u0026lt; workerCount; i++ { wg.Add(1) go doit(i,wq,done,\u0026amp;wg) } for i := 0; i \u0026lt; workerCount; i++ { wq \u0026lt;- i } close(done) wg.Wait() fmt.Println(\u0026#34;all done!\u0026#34;) } func doit(workerId int, wq \u0026lt;-chan interface{},done \u0026lt;-chan struct{},wg *sync.WaitGroup) { fmt.Printf(\u0026#34;[%v] is running\\n\u0026#34;,workerId) defer wg.Done() for { select { case m := \u0026lt;- wq: fmt.Printf(\u0026#34;[%v] m =\u0026gt; %v\\n\u0026#34;,workerId,m) case \u0026lt;- done: fmt.Printf(\u0026#34;[%v] is done\\n\u0026#34;,workerId) return } } } //运行结果 /* [1] is running [1] m =\u0026gt; 0 [0] is running [0] is done [1] m =\u0026gt; 1 [1] is done all done! */ 26. 向无缓冲通道中发送消息 在你的信息被接收方处理之前,发送方不会被阻塞。根据你运行代码的机器,在发送方继续执行之前,接收方的goroutine可能有也可能没有足够的时间来处理消息。示例如下:\n1 2 3 4 5 6 7 8 9 10 11 12 func main() { ch := make(chan string) go func() { for m := range ch { fmt.Println(\u0026#34;processed:\u0026#34;,m) } }() ch \u0026lt;- \u0026#34;cmd.1\u0026#34; ch \u0026lt;- \u0026#34;cmd.2\u0026#34; //won\u0026#39;t be processed } 27. 向已经关闭的通道发送消息 从已经关闭的通道中接收消息是安全的。返回值ok会被赋值为false表示没有数据被接收到。发送通道关闭周,向发送通道中再次发送数据会导致出错。示例如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 func main() { ch := make(chan int) for i := 0; i \u0026lt; 3; i++ { go func(idx int) { ch \u0026lt;- (idx + 1) * 2 }(i) } //get the first result fmt.Println(\u0026lt;-ch) close(ch) //not ok (you still have other senders) //do other work time.Sleep(2 * time.Second) } 这个错误的例子可以通过使用一个特殊的取消通道,向剩余的工作者发出不再需要他们的结果的信号来解决。示例如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func main() { ch := make(chan int) done := make(chan struct{}) for i := 0; i \u0026lt; 3; i++ { go func(idx int) { select { case ch \u0026lt;- (idx + 1) * 2: fmt.Println(idx,\u0026#34;sent result\u0026#34;) case \u0026lt;- done: fmt.Println(idx,\u0026#34;exiting\u0026#34;) } }(i) } //get first result fmt.Println(\u0026#34;result:\u0026#34;,\u0026lt;-ch) close(done) //do other work time.Sleep(3 * time.Second) } 28. 使用未初始化的通道 向一个未初始化的通道中发送消息或者是接收消息会永远阻塞。示例如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func main() { var ch chan int for i := 0; i \u0026lt; 3; i++ { go func(idx int) { ch \u0026lt;- (idx + 1) * 2 }(i) } //get first result fmt.Println(\u0026#34;result:\u0026#34;,\u0026lt;-ch) //do other work time.Sleep(2 * time.Second) } // 错误信息 // all goroutines are asleep - deadlock! 这种行为可以作为一种方式,在select语句中动态地启用和禁用case块。。示例如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 func main() { inch := make(chan int) outch := make(chan int) go func() { var in \u0026lt;- chan int = inch var out chan \u0026lt;- int var val int for { select { case out \u0026lt;- val: // 关闭输出通道 out = nil in = inch case val = \u0026lt;- in: out = outch // 关闭 输入通道 in = nil } } }() go func() { for r := range outch { fmt.Println(\u0026#34;result:\u0026#34;,r) } }() time.Sleep(0) inch \u0026lt;- 1 inch \u0026lt;- 2 time.Sleep(3 * time.Second) } 中级 1. 关闭HTTP响应 关闭HTTP相应我们通常使用defer进行关闭,但是在大多数情况下我们可能会将defer语句放错位置。当相应为nil的时候就会出现错误。示例如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 // 错误示范 // 如果请求正常相应,程序不会出错,但是如果不是正常相应,则会 func main() { resp, err := http.Get(\u0026#34;https://api.ipify.org?format=json\u0026#34;) defer resp.Body.Close()//not ok if err != nil { fmt.Println(err) return } body, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Println(err) return } fmt.Println(string(body)) } // 正确做法 func main() { resp, err := http.Get(\u0026#34;https://api.ipify.org?format=json\u0026#34;) // 为了防止 重定向相应,此时err为nil if resp != nil { defer resp.Body.Close() } if err != nil { fmt.Println(err) return } body, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Println(err) return } fmt.Println(string(body)) } resp.Body.Close()的原始实现也读取并丢弃剩余的响应体数据。这确保了如果启用了http连接的keepalive行为,该http连接可以被重新用于另一个请求。最新的http客户端行为是不同的。现在,你有责任读取并丢弃剩余的响应数据。如果你不这样做,http连接可能会被关闭,而不是被重新使用。这个小问题应该在Go 1.5中有所记载。如果重用http连接对你的应用程序很重要,你可能需要在响应处理逻辑的末尾添加类似这样的东西。\n1 _, err = io.Copy(ioutil.Discard, resp.Body) ","date":"Dec 08","permalink":"https://hearecho.github.io/post/gomistakes/","tags":["Go","基础"],"title":"Go常见错误及处理方法"},{"categories":null,"contents":"二分查找 \t二分查找常用来在排序数组中查找一个数的位置或者是一个数的边界。但是二分查找受到区间的影响很大。并且细节繁多,很容易就会产生偏差。二分查找不出错的关键就是先把所有的情况分支都完整的写出来,暂时不考虑分支合并的情况。总的来说只有三种分支情况那就是等于目标值、大于目标值以及小于目标值,基于这三种情况对边界进行修改。所以二分查找的基础框架如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 func binarySearch(nums []int, target int) { l, h := 0, len(num) or len(nums)-1 for l\u0026lt;h or l\u0026lt;=h { mid := l + (h-l)/2 if nums[mid] == target { //TODO } else if nums[mid] \u0026lt; target { //TODO l = ... } else if nums[mid] \u0026gt; target { //TODO\th = ... } } //TODO return ... } 基本的二分搜搜 基本的二分搜索就是查找一个数据的位置,或者是判断这个数在不在数组中。一般二分搜索的关键是在于区间的改变,一般有两种方式,左闭右开或者是两端均闭合\n左闭右开 左闭右开的意思就是左边的元素在此次搜索中可以访问到,右边的元素不可访问,所以区间[i,i)是没有意义的,不存在这种空间,所以循环条件为 $l\u0026lt;h$。并且为了保正每次修改区间之后仍未左闭右开,则$h$的每次改变应为$h = mid$。\n完整代码如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func binarySearch(nums []int, target int) int { // 我们遵循找下界的方式 即每次的区间改为左闭右开 \t// 所有的区间调整均要遵循左闭右开的原则 \tl, h := 0, len(nums) for l \u0026lt; h { mid := l + (h-l)/2 if nums[mid] == target { return mid } else if nums[mid] \u0026lt; target { l = mid + 1 } else if nums[mid] \u0026gt; target { h = mid } } return -1 } 双端闭合 双端闭合即两端元素均可访问,在这种情况下[i,i]是有意义的。所以循环条件为$l\u0026lt;=h$.而$h$的每次修改条件为$h=mid-1$.完整代码如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func binarySearch(nums []int, target int) int { // 如果使用两边全部闭合的区间 则对应需要修改循环的结束条件以及每次区间修改 \t// 包括区间的起始也要保证是两边闭合即均可访问到 \tl, h := 0, len(nums)-1 // 两边闭合的情况下会出现 l==h的情况 \tfor l \u0026lt;= h { mid := l + (h-l)/2 if nums[mid] == target { return mid } else if nums[mid] \u0026lt; target { l = mid + 1 } else if nums[mid] \u0026gt; target { //要保证两边均都要闭合 \th = mid - 1 } } return -1 } 寻找左边界 更多情况下,二分搜索都是应用在搜索不是固定的目标问题上,最常见的一种是搜索边界问题,左边界或者是右边界。\n左边界 我们是为了返回左边界的位置,所以等于目标值的时候缩小区间大小,并且缩小的右边界。其他情况不变,最后返回右边界和左边界都一样,因为循环截止条件就是$l==h$\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func searchLeft(nums []int, target int) int { // 寻找左侧边界问题 \t// 应该使用左闭右开的区间大小 \tl, h := 0, len(nums) for l \u0026lt; h { // 只有这样最后可以精确的返回左边界 \t// 因为不是为了寻找确定的值的位置 \t// 所以等于的情况并不会进行返回 也是修改区间 \t// 可以看到 相等的情况和大于的情况是相同的处理,所以可以进行合并 \tmid := l + (h-l)/2 if nums[mid] == target { // 我们是寻找左边界,所以需要修改h \t// 又因为是左闭右开 \th = mid } else if nums[mid] \u0026lt; target { //TODO l = ... \tl = mid + 1 } else if nums[mid] \u0026gt; target { //TODO h = . \th = mid } } // 这里也可以返回h \treturn l } 右边界 右边界和左边界类似,只是在等于的时候修改的的是$l$索引。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func searchRight(nums []int, target int) int { l, h := 0, len(nums) for l \u0026lt; h { mid := l + (h-l)/2 if nums[mid] == target { h = mid + 1 } else if nums[mid] \u0026lt; target { //TODO l = ... \tl = mid + 1 } else if nums[mid] \u0026gt; target { //TODO h = . \th = mid } } // 这里也可以返回h \treturn l - 1 } ","date":"Dec 02","permalink":"https://hearecho.github.io/post/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE/","tags":["算法"],"title":"二分查找及其变种"},{"categories":["论文阅读","算法","静态调度"],"contents":"论文 《Performance-effective and low-complexity task scheduling for heterogeneous computing 》\n 本文是异构平台的两种静态调度方法,分别是heft和cpop算法。\n 模型符号 符号 意义 $\\overline {w_i}$ 任务i的平均执行时间 $w_{i,j}$ 任务i在处理器j上的执行时间 $L$ 处理器通信模块启动时间 $L_m$ 处理器m的通信模块启动时间 $c_{i,k} = L_m + \\frac{data_{i,k}}{B_{m,n}}$ 任务i和任务j的通信时间 $B_{m,n}$ 处理器m,n的单位时间通信量 $data_{i,k}$ 任务i和任务j的通信量 $EST(n_i,p_j) = max{avail[j], max_{n_m\\in pred(n_i)} (AFT(n_m)+c_{m,j})}$ 任务i在处理器j上的最早启动时间 $EFT(n_i,p_j) = w_{i,j} + EST(n_i,p_j)$ 任务i在处理器j上的最早完成时间 $avail[j]$ 处理器j最早可用时间 $makespan = max{AFT(n_{exit})}$ dag的工作完成时间 $rank_u(n_i) = \\overline {w_i} + max_{n_j \\in succ(n_i)}(\\overline {c_{i,j}}+rank_u(n_j)) $ 作为权重 $rank_d(n_i) = max_{n_j \\in pred(ni)} {rankd(n_j) + \\overline {w_j} + \\overline {c_{j,i}}}$ 作为权重 算法 两个重要指标 $rank_{u}$ $rank_{u}$是从任务ni到出口节点关键路径的长度,是包括任务ni的计算耗时的。对于出口节点$rank_{u}$的值等于它的平均执行时间。\n $rank_{d}$ $rank_{d}$是从入口节点到任务ni的关键路径长度,不包括任务ni的执行时间,对于入口节点其$rank_{d}$的值等于0。\n heft算法 heft算法主要有两个阶段,第一个阶段计算所有任务的权重优先级,第二个阶段按照优先级顺序调度任务到最适合他们的处理器,以达到最小化任务的完成时间。\n 计算任务优先级 heft算法会将任务的优先度设置为$rank_u$,之后按照$rank_u$降低的顺序排列生成任务执行序列,如果两个任务的$rank_u$是相同的则可以有很多打破这种局面的策略,比如选择直接后继任务的$rank_u$更大的任务。最后生成的序列是一个拓扑排序所以肯定满足前序约束。\n 处理器选择 对于大多数调度算法,处理器的最早启动时间都是在该处理完成最后一个被分配任务之后。但是heft算法是考虑一个插入策略,考虑将任务插入到一个最早的空间时间间隔在两个已经完成调度的任务。空闲时隙时间的长度,即,在同一处理器上连续调度的两个任务的执行开始时间和完成时间之间的差异,应该至少能够满足计算要调度的任务的成本。并且必须要满足前置要求。\n 算法伪代码 1 2 3 4 5 6 7 8 9 # set the computation costs of tasks and communication costs of edges with mean values # Compute ranku for all tasks by traversing graph upward, starting from the exit task # Sort tasks in a scheduling list by noincreasing order of ranku values while there are unscheduled tasks in the list do: Select the first task ni from the list for scheduling for each processor pk in the processor-set (pk in Q) do: Comput EFT(ni,pk) value using the insertion-based scheduling policy Assign task ni to processor pj that minimizes EFT of task ni endwhile cpop算法 cpop算法和heft算法结构相同都有两部分,不过使用的算法不相同。使用不同的属性设置任务的优先级以及不同的策略来选择最佳的处理器。\n 计算任务优先级 对于cpop算法需要将每个任务的$rank_u,rank_d$使用平均执行时间和平均通信时间进行计算。cpop算法使用dag的关键路径,路径的长度是沿着这条路径计算时间以及任务之间通信时间的总和。这条路径上所有计算时间总和是调度长度的下限(所有的任务都在同一个处理器上进行处理)。而每个任务的优先度是$rank_u,rank_d$的总和。入口的优先度等于关键路径的长度。对于平等的情况,选择第一个直系后继有更高的优先级的任务。具体选择在伪代码部分给出。\n 处理器选择 选择一个处理器作为关键路径专用处理器,这个处理器能够最小化执行时间。如果一个任务在关键路径上,直接调度到关键路径处理器。除此之外选择能够让该任务执行时间最短的其他处理器,注意不能调度到关键路径专用处理器。\n 算法伪代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # set the computation costs of tasks and communication costs of edges with mean values # Compute ranku for all tasks by traversing graph upward, starting from the exit task # Compute rankd for all tasks by traversing graph downward, starting from the entry task # Compute priority(ni) = ranku(ni) + rankd(ni) for each task ni in the graph # SET_CP = {n_entry}, where SET_CP is the set of tasks on the critical path nk \u0026lt;- n_entry while nk is not the exit task do: Select nj where ((nj in succ(nk)) and (priority(nj)== |CP|)) SET_CP = SET_CP.append(nj) nk \u0026lt;- nj endwhile Select the critical path processor(p_cp) which minizes sum(w_i,j) Initialize the priority queue whith entry task while ther is an unscheduled task in the priority queue do: Select the highest priority task ni from priority queue if ni in SET_CP: Assign the task ni on p_cp else: Assign the task ni to processor pj which minimizes the EFT(ni,pj) Update the priority-queue with the sucessors of ni if they ready tasks endwhile ","date":"Aug 31","permalink":"https://hearecho.github.io/post/heft%E7%AE%97%E6%B3%95-%E9%9D%99%E6%80%81%E8%B0%83%E5%BA%A6/","tags":["论文阅读","DAG","算法","静态调度"],"title":"HEFT算法 静态调度"},{"categories":["论文阅读","算法","动态调度"],"contents":"论文介绍 《Dynamic_scheduling_algorithm_for_parpllel_real_time_jobs_in_heterogeneous_system》\n 异构系统中并行实时作业的动态任务调度仍然是一些研究人员正在研究的具有挑战性的问题。但是基于DAG的实时任务调度还没有得到足够的重视。提出了一种基于DAG的实时任务调度模型和一种时间复杂度较低的实时调度算法DEFF。仿真实验表明,该调度模型和调度算法是可行的,在中小型并行作业的情况下,该算法可以获得较高的调度成功率\n 模型符号 符号 意义 $V$ 实时任务集合 $E$ 任务之间通信 $dl(v_i)$ 任务$v_i$的截至时间 $cv_i$ 任务$v_i$的计算量 $e_{i,j}=(v_i,v_j)$ 表示任务$v_i,v_j$之间的通信量 $P$ 处理器集合 $p_i$ 拥有本地存储的处理器 $C:V*P \\rightarrow R$ 表示不同的计算能力 $w_k$ 表示处理器$p_k$在单位时间内的计算量 $cv_i/w_k$ 表示任务$v_i$在处理器$p_k$上的计算时间 $M:E * P * P \\rightarrow R$ 表示异构通信能力。 $w_{km}$ 表示处理器$p_k,p_m$之间单位长度信息的传输时间 $w_{km}*e_{i,j}$ 表示$e_{i,j}$的传输时间 queue-Global Job Queue (GJQ) 全局作业队列,所有到达系统的任务首先都要进入这个队列中,然后再进入中心调度器。进入这个队列的是DAG任务 queue-Task Dispatch Queue (TDQ) 和中心调度器进行交互的,分解之后的dag子任务 Local Scheduling Queue (LSQ) 每个处理器拥有的本地任务队列 $at(p_k)$ 处理器$p_k$的最早空闲时间 $st_k(v_i)$ 实时任务$v_i$的最早开始时间 $ft_k(v_i)$ 映射到处理器$p_k$实时任务$v_i$的最早完成时间 调度算法 定义1 如果映射到处理器$p_k$实时任务$v_i$的最早完成时间$ft_k(v_i)$小于等于任务$v_i$的截至时间$dl(v_i)$,则实时任务$v_i$可以被调度到处理器$p_k$中。\n 定义2 映射到处理器$p_k$实时任务$v_i$的最早完成时间$ft_k(v_i)$被定义为:$$ft_k(v_i) = cv_i/w_k + max(st_k(v_i),at(p_k))$$\n 定义3 实时任务$v_i$的最早开始时间$st_k(v_i)$被定义为:$st_k(v_i)=max_{v_j\\in pred(v_i)}( ft_m(v_j+w_{mk}*e_{i,j}))$;其中$pred(v_i)$表示任务$v_i$的前一个集合。\n 算法实现 deff调度算法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 \u0026#34;\u0026#34;\u0026#34; 论文算法实现 deff AST 本次deff调度算法启动的时间 AFT 上次deff调度算法结束的时间 DIFT 中间的插值 DIFT = AST-DIFT \u0026#34;\u0026#34;\u0026#34; import time import sys def deff(AFT, processors, tasks_dtq, tasks_scheduled, w, e, cp_w, deL_dag_tasks): \u0026#34;\u0026#34;\u0026#34; :param deL_dag_tasks: 无法进行调度的dag任务 :param cp_w: 处理器的计算能力一个数组 :param e: 表示任务之间的通信量,一个字典 e[task_i][task_j]表示两个任务之间的通信量 :param w: 表示处理器之间单位长度信息的传输时间 二维矩阵 :param tasks_scheduled: 已经被调度过的任务 :param tasks_dtq: dtq中的准备被调度的任务队列 :param AFT: 上次deff调度算法结束的时间 :param processors: 处理器数组,每个处理器元素都是一个字段储存处理器相关的信息 :return: \u0026#34;\u0026#34;\u0026#34; DIFT = time.time() - AFT for p in processors: if p[\u0026#39;at\u0026#39;] - DIFT \u0026lt; 0: p[\u0026#39;at\u0026#39;] = 0 else: p[\u0026#39;at\u0026#39;] -= DIFT for v in tasks_scheduled: if v[\u0026#39;ft\u0026#39;][v[\u0026#39;mapped_p\u0026#39;][\u0026#39;index\u0026#39;]] - DIFT \u0026lt; 0: v[\u0026#39;ft\u0026#39;][v[\u0026#39;mapped_p\u0026#39;][\u0026#39;index\u0026#39;]] = 0 else: v[\u0026#39;ft\u0026#39;][v[\u0026#39;mapped_p\u0026#39;][\u0026#39;index\u0026#39;]] = v[\u0026#39;ft\u0026#39;][v[\u0026#39;mapped_p\u0026#39;][\u0026#39;index\u0026#39;]] - DIFT while len(tasks_dtq) \u0026gt; 0: task = tasks_dtq[0] # 如果该子任务的其他任务已经存在不可调度的子任务 则直接舍弃 if task[\u0026#39;father\u0026#39;] in deL_dag_tasks: continue sps = [] for i in range(len(processors)): if len(task[\u0026#39;pred\u0026#39;]) \u0026gt; 0: max_st = -1 for pred_task in task[\u0026#39;pred\u0026#39;]: temp = pred_task[\u0026#39;ft\u0026#39;][pred_task[\u0026#39;mapped_p\u0026#39;][\u0026#39;index\u0026#39;]] + w[pred_task[\u0026#39;mapped_p\u0026#39;][\u0026#39;index\u0026#39;]][i] * e[task[\u0026#39;name\u0026#39;]][ pred_task[\u0026#39;name\u0026#39;]] if temp \u0026gt; max_st: max_st = temp task[\u0026#39;st\u0026#39;][i] = max_st else: task[\u0026#39;st\u0026#39;][i] = 0 task[\u0026#39;ft\u0026#39;][i] = task[\u0026#39;c\u0026#39;]/cp_w[i] + max(task[\u0026#39;st\u0026#39;][i], processors[i][\u0026#39;at\u0026#39;]) if task[\u0026#39;ft\u0026#39;][i] \u0026lt;= task[\u0026#39;dl\u0026#39;]: sps.append(processors[i]) if len(sps) \u0026gt; 0: min_p = None min_p_ft = sys.maxsize for p in sps: if task[\u0026#39;ft\u0026#39;][p[\u0026#39;index\u0026#39;]] \u0026lt; min_p_ft: min_p = p min_p_ft = task[\u0026#39;ft\u0026#39;][p[\u0026#39;index\u0026#39;]] task[\u0026#39;mapped_p\u0026#39;] = min_p min_p[\u0026#39;at\u0026#39;] = min_p_ft else: # 无法进行调度(也就是调度最后实时性不满足,直接舍弃) # 需要删除的是这个子任务所属dag任务的所有任务,但是不包括已经调度。 deL_dag_tasks.append(task[\u0026#39;father\u0026#39;]) # 我的做法是不在这里删除,只是将这个任务所属的dag任务加入到一个内存中进行存储 # 之后再进行再从gjq向tdq调度的时候进行判断 然后执行算法的时候进行判断不对他们进行调度即可 tasks_dtq = tasks_dtq[1:] # 返回算法结束时间 return time.time() pass 触发器算法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 \u0026#34;\u0026#34;\u0026#34; 论文调度器的触发算法实现 触发器的触发条件,一个新的dag任务图到达,或者是一个一个子任务完成则会调用触发器算法 调用触发器的时刻,当GJQ新到达一个dag任务或者是一个子任务完成了 GJQ 全局作业队列 TDQ 任务分发队列 \u0026#34;\u0026#34;\u0026#34; def trigger(completed_tasks, task, trigger_type, remain_tasks, TDQ, completed_job, GJQ): \u0026#34;\u0026#34;\u0026#34; :param GJQ: job队列 :param completed_job: 完成的并行任务 :param TDQ: :param remain_tasks: 剩余任务 :param trigger_type: 0 新job到达 1 任务完成 :param completed_tasks: 已经完成的子任务,字典格式,key为dag任务的图 :param task: 完成的任务或者是新到的dag任务的开头任务, :return: \u0026#34;\u0026#34;\u0026#34; if trigger_type == 0: # 将所有的入口任务都加进去 也就是没有前继的任务 for task in remain_tasks: if len(task[\u0026#39;pred\u0026#39;]) == 0: TDQ.append(task) # 从剩余任务中删除这个任务 remain_tasks.remove(task) else: if len(remain_tasks) == 0: # 任务完成 completed_job.append({task[\u0026#39;father\u0026#39;]: task[\u0026#39;ft\u0026#39;][task[\u0026#39;mapped_p\u0026#39;][\u0026#39;index\u0026#39;]]}) return completed_tasks.append(task) for task in remain_tasks: if len(task[\u0026#39;pred\u0026#39;]) == 0: TDQ.append(task) # 从剩余任务中删除这个任务 remain_tasks.remove(task) else: flag = True for pred_t in task[\u0026#39;pred\u0026#39;]: if pred_t not in completed_tasks: flag = False if flag: TDQ.append(task) # 从剩余任务中删除这个任务 remain_tasks.remove(task) ","date":"Aug 22","permalink":"https://hearecho.github.io/post/deff/","tags":["论文阅读","DAG","算法","动态调度"],"title":"DEFF算法论文阅读"},{"categories":["论文阅读","算法"],"contents":"动态DAG调度综述 在异构系统中调度的核心问题是:由于异构系统中处理器的性能不尽相同,所以为了获得更高的性能。系统调度需要将应用程序的任务分配给合适的处理器,并对每个资源上的任务执行进行排序。给定一个有向无环图(DAG)建模的应用程序,调度问题处理的是在异构环境中映射每个任务以最小化执行时间(makspan)。\n 动态调度基础 动态调度,任务在到达时分配给处理器,调度决策必须在运行时做出。调度决策基于在运行时可能更改的动态参数。在动态调度中,任务可以在运行时重新分配给其他处理器。动态调度相比静态调度灵活且速度更快。由于我们异构系统处理同一个任务的时间是不相同的,所以我们每次调度的时候应该考虑将任务放在最合适的处理器中。\n 相关的DAG调度算法 Monte Carlo 算法 Stochastic DAG scheduling using a Monte Carlo approach\n该算法使用静态调度方法。主要目的是最大限度地缩短完工时间。该算法避免了适用于任意随机分布的随机变量的复杂计算。阈值用于优化调度。概率分布用于最小化完工时间。\n CBHD 算法 基于集群的复制权重,用于异构系统上任务的高效调度和映射,将程序分解为子任务,同时与其他任务一起工作[12]。该算法将三重聚类算法与HEFT算法相结合,将任务分为相互关联的组,并对每个簇中的任务进行排序,以提高负载均衡调度算法的性能。其主要目标是最小化执行时间,最大化处理器利用率和处理器之间的负载平衡。\n P-HEFT算法 P-HEFT(并行异构最早完成时间)的算法。该算法在不影响最大完工时间的前提下,以很高的效率处理异构集群中的并行任务。该算法的主要目标是最小化具有不同到达时间的作业的最大完工时间和批处理时间,从而在作业执行期间更改分配给作业的处理器(看描述也不是动态算法)\n High Performance and Energy Efficient Task Scheduling Algorithm 该算法关注调度长度和能量/功耗的最小化。该算法分为编译阶段和运行阶段。编译时间阶段有三个阶段,例如级别排序、任务优先级和处理器选择。在运行期间,为了节省能量,该算法将任务从繁忙节点重新调度到理想节点。该算法的性能明显优于传统算法。\n该算法为动态调度算法。\n Constrained Earliest Finish Time (CEFT) Algorithm 受约束的最早完成时间。这种新方法是使用受约束关键路径(CCPs)的概念为异构系统提供更好的调度。一旦发现DAG中的CCPs,任务将使用整个CCPs的完成时间进行调度。这种方法有助于以较短的完工时间生成时间表,并且工作复杂度很低。\n Sorted Nodes in Leveled DAG Division (SNLDD) Algorithm 该算法称为高性能任务调度算法。这种类型的算法将DAG划分为不同的级别,每个级别根据计算时间按降序排序,从而减少任务之间的依赖性。静态任务调度适用于处理器数量有限的异构系统。这种类型可能会产生高质量的任务调度。DAG中所有任务的计算时间只计算一次,因此消除了运行时开销。处理器的平滑时间也被最小化,因为DAG被调平并分配给处理器。\n Multi – Queue Balancing Algorithm 在功能单元中引入了调度概念。该算法具有各种类型的功能单元,不使用有限数量的硬件和软件。这种类型的算法称为离线非确定性算法。该算法的主要目标是最小化作业完成时间,根据资源的可用性将任务分配给机器,并最大限度地利用异构系统。每个任务都在其匹配的处理器上执行。也是静态调度算法。\n Clustering Scheduling Strategies Algorithm 以减少应用程序的配置数量和执行时间,同时提高现场可编程门路(FPGA)器件的利用率。在可重构计算系统中,引入启发式调度策略和动态规划调度策略,将有向无环图(DAG)划分为多个簇。\n SD-based Algorithm for Task Scheduling (SDBATS) 基于SD的算法是一种标准偏差方法,用于计算异构计算环境中可用资源上给定任务的预期执行时间。该算法将各种条件应用于标准任务图应用,如高斯消去和快速傅立叶变换应用。这种方法产生了高质量的计划,并产生了低成本的计划和系统效率。 可以用于动态调度算法。\n Online Scheduling of Dynamic Task Graphs 与传统方案相比,动态任务图的在线调度更为现实,任务图在运行时会发生变化,处理器间的通信也会固定数量。在线调度采用广播和点对点通信。该算法的主要目标是减少完工时间。\n Node Duplication Modified Genetic Algorithm Approach (NMGA) 该方法采用了顶层和底层方法。它展示了节点复制技术的效率。主要目的是最小化完成时间并增加系统吞吐量。复制任务以减少总时间。\n Heuristic based for Genetic Algorithm 该算法需要较少的计算时间[22]。该算法基于多处理器系统中的任务调度,通过选择合适的处理器来达到次优解。底层方法是通过选择合格的处理器来分配任务,以获得最小的完成时间。\n List based Heuristic Task Scheduling 该方法具有不同类型的任务优先级,以获得最小的调度长度和速度。这只继承静态调度,静态调度进一步分为作业调度和任务调度。该算法的主要目的是最小化执行时间和通信延迟,最大限度地提高资源利用率。\n Efficient Genetic Algorithm 该算法继承了定制的遗传算法,为HEDCS生成高质量的任务调度。新算法的性能由两种调度算法实现,即HEFT算法和DLS算法。这适用于随机生成的任务图和某些实际数值应用的任务图。该算法的主要目标是增加调度长度、加速比和效率。\n BNP Scheduling Algorithms 它将一个由边有向无环图(DAG)表示的并行程序引入一组同质处理器。主要目标是尽可能缩短完成时间。几类算法和一类调度的性能称为有界处理器数(BNP)。比较基于不同的调度参数,如最大完工时间、速度、处理器利用率和调度长度比。主要重点是增加处理器上的任务数量,以提高这些算法的性能\n Predict Earliest Finish Time (PEFT) 该算法具有相同的时间复杂度,即v任务和p处理器的时间复杂度为O(v2:p),在不增加与计算乐观代价表(OCT)相关的时间复杂度的情况下引入了一个特性。设计值是一个乐观的成本,因为计算中未测量处理器可用性。此算法仅基于用于对任务进行排序和处理器选择的OCT表。PEFT算法在调度长度比、效率和频率方面为异构系统执行基于列表的算法。\n Hybrid Genetic Scheduling Algorithm 该算法为每个任务分配一个耦合因子,并通过避免大量通信时间将其调度到同一处理器上。该算法的目标是通过将相互强耦合的任务调度到同一个处理器上来生成高质量的初始解,并通过使用耦合初始解、随机解、,通过交叉和变异算子中的列表调度算法获得近似最优解。\n Fork – Join Method (TSFJ) 该算法继承了多处理机系统中的任务调度,目的是最小化总执行时间,从而达到最大的速度和效率。应用程序由有向无环图(DAG)表示,任务根据fork-join结构分配给处理器。该算法的主要性能基于调度长度、加速效率和负载平衡。\n Non – Dominated Sorting Genetic Algorithm – II (NSGA-II) 该算法在异构多处理器系统上引入了调度应用程序,主要用于单一目的,如执行时间、成本或总数据传输时间。所提出的算法是利用进化技术开发一种多目标调度算法,用于在多处理器环境中调度一组依赖于可用资源的任务。主要目标是最大限度地缩短完工时间。\n Task Scheduling and Energy Conservation Techniques 该算法解决了多处理器系统中依赖任务的能量感知调度技术中的“最新技术”,以减少总体完工时间和能源消耗。\n Critical Path Scheduling with T – level (CPST) 在CPST中,使用任务的t级值生成基于关键路径的任务序列,其中使用基于方差的计算和通信成本。任务按优先级的非递增顺序排序和选择,并在处理器上调度,以优化各种性能指标。目标是将任务映射到合适的资源上,并最小化最大完工时间。\n Multi – Core Scheduling 引入异构协处理器(即一个处理器和多个异构协处理器),处理器处理控制信号\u0026amp;异构协处理器用于数据计算,因为需要多个协处理器调度器来确定哪些子任务传输到哪个协处理器。该算法采用迁移策略来提高资源的利用率。\n Task Scheduling Algorithm using Merge Conditions 该算法在保留调度长度的同时减少了处理器数量。该算法根据条件找到合并对,并在不增加调度长度的情况下合并它们。函数用于查找最大对以减少合并次数。\n Linear Programming based Scheduling Algorithms 该算法应用于具有不同特征的五种不同应用中的各种邻近查询,并通过额外的计算资源有力地提高了性能。所提出的算法是为并行邻近计算而设计的,以最小化计算资源所花费的最大时间。\n Comparative Study of Scheduling Algorithm 一种称为调度算法比较研究的算法,用于在静态资源集上映射具有不同截止日期的多个工作流的任务。主要目的是将任务映射到处理器,最大限度地提高资源利用效率,同时满足所有工作流的最后期限。\n 算法比较以及未来提升 算法 目的 优点 未来提升方向 Monte Carlo based DAG scheduling approach 最大限度缩短完工时间 避免了随机变量的复杂计算,适用于人任何随机分布 动态调度 Clustering based HEFT with duplication 最小化执行时间 最大限度的提高处理器利用率和复杂平衡 更好的优化方法 P-HEFT 尽可能缩短完成时间 高效和最佳完成时间 多用户环境的优化 High performance and energy efficient task scheduling algorithm 最小化完成时间 调度长度和能量消耗 大规模任务图 Constrained Earliest Finish Time 最小化执行时间 明细表长度的最小阈值 用于在机器的后续操作中选择多条受约束的关键路径 Sorted Nodes in Leveled DAG Division 加速、效率、复杂性和质量 处理器的最小完成时间和最大利用率 在异构系统上调度更多作业 Multi Queue Balancing Algorithm 最大限度地缩短完成时间和最大限度地利用资源 处理器的最小完成时间和最大利用率 在异构系统上调度更多作业 Heuristic and Dynamic programming scheduling strategy 尽量减少执行时间 高度并行而不丧失通用性 热力性能 SD-Based Algorithm for Task Scheduling 时间表长度和速度 减少执行时间并分配任务优先级。 同质系统 Online Scheduling of Dynamic Task Graphs 使制造跨度最小化 固定通道数的处理器间通信 离线 Node duplication Modified Genetic Algorithm Approach 使完成时间和吞吐量最小化 复制任务以减少总时间 非确定性同质系统 Heuristic based for genetic algorithm 使制造跨度最小化 通过选择合格的处理器来分配任务的底层方法 非确定性同质系统 List based heuristic task Scheduling 执行时间、通信延迟和最大化资源利用率 并行架构的效率。任务执行时间、通信成本和任务相关性在执行前可用。 动态调度 An Efficient Genetic Algorithm 调度长度、加速和效率。 异构处理器的部分连接网络。 BNP Scheduling Algorithms 尽可能缩短完成时间 在性能上增加任务和处理器的数量 异构系统 Predict Earliest Finish Time 使制造跨度最小化 调度长度比率、效率和频率 异构系统 A Hybrid Genetic Scheduling Algorithm 将任务分配给可用的处理器并生成跨度 避免过多的沟通时间。通信成本长,更有效,图形结构更灵活 更好的优化方法 Fork-Join Method 以最小化总执行时间 调度长度、加速比、效率和负载平衡 更多节点数。 Non-dominated sorting Genetic Algorithm-II 使完工时间和可靠性成本最小化 用于在最短时间内选择最佳计划 动态调度 Task Scheduling \u0026amp; Energy Conservation Techniques 减少总完工时间和能源消耗 高性能计算系统用途广泛,性能价格低廉,能耗巨大。 异构系统是一组关于不同任务图特征的随机和随机集合。 Critical path scheduling with t-level 最小化总体执行时间和最大完工时间 任务按其优先级的非递增顺序排序和选择 同质系统 Multi-core Scheduling 尽量缩短响应时间,提高资源利用率 它使用调度机制来调度不同的子任务和迁移策略,以减少响应时间 同质系统 Task Scheduling Algorithm using Merge Conditions 要最小化时间表长度 减少合并的次数。 预处理调度方法 Scheduling in Heterogeneous Computing Environments for Proximity Queries 在异构计算系统中最小化最大完工时间和邻近计算 鲁棒性强,专为并行邻近计算而设计 针对更多种类作业的最佳优化方法 Comparative study of scheduling algorithm 最大限度地提高资源利用效率 提高效率,按时完成任务。 动态和电子科学基础设施平台 ","date":"Aug 22","permalink":"https://hearecho.github.io/post/surver-dynamic-dag-schedule/","tags":["论文阅读","DAG","算法"],"title":"Surver Dynamic Dag Schedule"},{"categories":["论文阅读","算法"],"contents":"论文简介 《Runtime Parallel Incremental Scheduling of DAGs》\n这篇论文主要是提出一种并行增量的DAG调度方法。主要学习一下这篇论文关于动态调度DAG任务方面的内容。因为之前主要用的最多的静态调度方法就是HEFT与CPOP。所以想通过这篇文章,看是否可以将动态思路运用到HEFT或者CPOP中。本篇论文仍然是针对一个DAG图进行调度。并未给出针对多DAG图的相关思路。\n论文出发点 本篇论文出发点是从当前DAG调度算法的缺点出发的,主要列举出五点,分别是:\n 因为它们在单处理器机器上运行,所以速度很慢。调度程序可能需要现代工作站数十小时的计算时间来生成1K处理器的调度计划。 它们需要很大的内存空间来存储图形,并且此后无法扩展。例如,要将并行程序调度到1K处理器,数百万个节点的图形可能需要数百MB的内存空间。 获得的计划的质量在很大程度上依赖于对执行时间的准确估计。没有这些信息,复杂的调度算法就无法提供令人满意的性能。 应用程序必须针对不同的问题大小重新编译,因为任务数量和每个任务的估计执行时间随问题大小而变化。 它是静态的,因为编译时必须知道DAG中任务的数量和任务之间的依赖关系。因此,它不能应用于动态问题。 论文概述 静态动态调度系统的区别 在静态系统中,DAG由用户程序生成,并在编译时调度。然后将计划的DAG加载到PEs以执行。在运行时调度系统中,DAG不是一次生成的。相反,它是增量生成的。为此,在编译时生成一种紧凑形式的DAG(紧凑DAG或CDAG)。然后在运行时将其增量扩展到DAG。CDAG的大小与程序大小成正比,而DAG的大小与问题大小或矩阵大小成正比。\n增量执行模型 在增量执行模型中,每个系统阶段只调度一个子图。每次生成的子图的大小通常受到可用内存空间的限制。系统调度活动与基础计算工作交替进行。它从一个系统阶段开始,在此阶段仅生成和调度DAG的一部分。然后是用户计算阶段,以执行计划任务。PEs将执行,直到大多数任务完成,并转移到下一个系统阶段,以生成和安排DAG的下一部分\n策略决定何时从用户计算阶段转移到下一个系统阶段。当任何PE的任务用完时,都会触发该事件。PE通过向所有其他PE广播暂停信号来启动调度活动。PE在接收到暂停信号后,完成当前任务的执行,并从该用户阶段切换到下一个系统阶段。在下一个系统阶段,将生成DAG的另一部分。新生成的任务与旧任务一起调度。这样,可以容忍由于估算不准确而导致的负载不平衡。然后将计划任务发送给PEs,以开始下一个用户阶段。\n并行调度算法 由于动态调度算法,需要实时进行调度,所以我们应该找到,调度时间和任务执行时间之和最短的算法。\nALAP(as-laste-as-possible):一个节点的尽可能晚的时间定义为$T_L(n_i) = T_{critical}-level(n_i)$,其中$T_{critical}$是计算节点和边权重的关键路径长度。$level(n_i)$是当前节点到最后节点的最长路径的长度,包括当前节点。\nPPE:执行调度算法的PE\nTPE:执行任务的目标PE\nMCP算法 计算每个节点的ALAP时间 按递增的ALAP顺序对节点列表进行排序。通过使用后继节点的最小ALAP时间、后继节点的后继时间等来断开连接 将列表中的第一个节点调度到允许最早开始时间的PE。从列表中删除节点并重复步骤3,直到列表为空。 作者通过使用MCP算法的非插入版本来进一步降低复杂性。它的复杂度是$O(e+nlogn+np)$,其中e是边的个数,p是PEs的个数,n是图中的节点数。作者的实验表明,对于粗粒度划分,该算法产生的调度长度最多比原始MCP长3%,但其调度时间减少了一到两个数量级。\n水平并行MCP算法(HPMCP) 图形分区后,每个PPE使用MCP调度其分区以生成其子调度。在应用MCP时,我们忽略了分区之间的依赖关系,因此每个分区都可以独立调度。如果一个节点的所有父节点都不是本地节点,则该节点在其分区中被视为入口节点。节点按其ALAP优先级的顺序进行调度。每个PPE从其本地时间0开始调度其分区。然后连接相邻的子调度以形成最终调度。如下图所示:\n 对节点进行分区,每个分区分配给一个PPE 每个PPE将MCP算法应用于其分区以生成子调度,忽略节点与其远程父节点之间的边缘。将列表中的第一个节点安排到允许最早启动时间的TPE。从列表中删除节点并重复此计划步骤,直到列表为空。 连接每对相邻的子表。 系统概览 作者提出的运行时系统主要包括DAG图形生成、调度、节点执行、通信处理和增量执行处理模块。\nDAG图形生成 在系统阶段k,新扩展的节点和上阶段未执行的节点集。生成的DAG子图$G_k = {S_k,E_k}$。如果节点$n_i,n_j$都在节点集合$S_k$中那么$e_{i,j}$是$E_k$中的一条边。如果目标节点不在$S_k$中,则来自$S_k$中节点任何传出边缘将成为*future message*。之后子图$G_k$将会被安排到PE中,并在用户阶段k执行。\n调度 调度模块将为$S_k$中的每个节点建立从其逻辑ID到其物理ID的映射,该物理ID由目标PE编号和目标PE处的本地ID组成。对于每个在$E_k$中的边$e_{i,j}$,节点$n_i$拥有节点$n_j$的物理ID。因此,当节点i执行完成时,其所有传出消息可以立即定向到其目的地。一旦PPE生成其节点子集,它将使用MCP算法独立地调度这些节点以形成子调度。一个已经被调度的DAG会被加载到TPEs中执行,每个TPE获得一个按执行顺序排序的节点列表(使用本地ID)。\n执行 在执行模块中,调度例程负责选择节点并准备执行。在被调度的DAG中,列表中的节点将按顺序执行。分派例程选择列表中的下一个节点并检查其传入消息。当所有传入消息到达后,节点就准备就绪并执行。分派例程为节点的执行分配内存并准备参数。然后调用节点过程。节点执行完成后,通信处理模块处理输出参数。为节点分配的所有内存空间也将被释放。这种消息驱动的宏数据流执行模型可以有效地利用内存。\n通信处理 当PE没有准备好执行的节点时,或者在一个节点执行完成和下一个准备好的节点执行开始之间的时间段内,处理消息接收。由于推送方案应用于包含其目的地PE号码以及本地ID的每个传出消息,因此到达的消息可以容易地附加到相应的节点。一旦所有传入消息到达,节点本身就可以执行了。节点执行后,将为每个传出边缘发送一条消息。如果消息只有一个目的地,则将其分类为单播。如果消息具有多个回执,则将其分类为多播。尽管多播消息可以逐个发送到不同的目的地,但它可能需要不可接受的通信时间。通信模块使用多播树进行有效的多播\n增量执行处理 接收到暂停消息的每个PE将完成当前节点执行并暂停其当前用户阶段k。在进入系统阶段k+1之前,需要处理尚未执行的剩余节点以及相应的消息,以将其合并到阶段k+1中。在进入阶段k+1之前,必须使节点逻辑ID到其物理ID的当前映射无效,因为物理ID仅对特定阶段有意义。当剩余节点被发送回重新调度时,已经到达这些节点的消息被分离并转换为将来要附加到阻塞队列中的消息,以便延迟这些消息的传递,直到新映射可用于阶段k+1。如果该消息是多播消息,它将被删除,因为多播消息将在以后的每个阶段重新广播。(多播消息是每个阶段都会广播一次,而单播之后发送一次,所以需要进行保存,防止节点未执行,重新分配之后,不能收到消息)\n","date":"Aug 08","permalink":"https://hearecho.github.io/post/runtime_parallel_incremental_scheduling_of_dags%E8%AE%BA%E6%96%87%E9%98%85%E8%AF%BB/","tags":["论文阅读","DAG","算法"],"title":"Runtime_Parallel_Incremental_Scheduling_of_DAGs论文阅读"},{"categories":["论文阅读","EDF","DAG","算法"],"contents":"论文阅读 《A Stretching Algorithm for Parallel Real-time DAG Tasks on Multiprocessor Systems》\n本篇论文的算法属于静态调度算法,预先知道DAG图,然后根据DAG图转换为MTS。再在MTS的基础上对整个任务进行拉伸。也就是本文提出的全局拉伸算法。\n本文的贡献 提出了一种适用于DAG任务模型的拉伸算法。该算法是DAG调度过程的前一步(静态调度)。将并行的DAG转换为独立的舒徐线程,其他无法进行转换,或者转换之后超出时间的则在其他处理器并行执行。 对于在m个相同处理器平台上执行的由n个DAG任务组成的任务集合,作者证明了扩展任务的全局EDF调度具有相同的资源扩充界限:$\\frac{3+\\sqrt{5}}{2}$,在$n\u0026lt;\\varphi*m^{'}$,其中$m^{'}\u0026lt;=m$,是除了主线程之外的其他线程的数量,而$\\varphi$是黄金分割率。 文章中参数的含义 参数名 含义 ${t_{i,j} 1\u0026lt;=j\u0026lt;=n_i}$ $G_i$ 子任务之间的依赖关系 $O_i$ DAG任务的偏移量 $D_i$ DAG任务的相对截至时间 $T_i$ 连续任务之间的间隔时间,一般看作$T_i=D_i$ $C_{i,j}$ 单个子任务任务的最坏执行情况所用的时间 $C_i = \\sum_{j=1}^{n_i}C_{i,j}$ 单个DAG任务的最坏执行情况所用的时间 $U_i=\\frac{C_i}{T_i}$ 利用率,最坏运行情况在限制时间内多少 $Li$ DAG图中所有路径中执行时间最长的路径所用的时间 $Sl_i = D_i-L_i$ 执行最长路径之后剩余的时间。 $S_i$ 转换为MTS形式之后,段的总数。 $e_{i,j}$ 每个段的最长执行时间,按这个段中所有子任务最短的计算。 $m_{i,j}$ 每个段中任务的数量 $MTS L_i = \\sum_{j=1}^{s_i}e_{i,j}$ MTS模式下的关键路径的长度 $MTSC_i = \\sum_{j=1}^{s_i}m_{i,j}*e_{i,j}$ MTS模式下的最坏情况执行时间 $ f_i = \\frac{Sl_i}{C_i-L_i}=\\frac{D_i-L_i}{C_i-L_i}\u0026lt;=\\frac{D_i}{C_i}\u0026lt;1 $ 分发参数 $f_{i,j} = f_i*(m_{i,j}-1)$ 要添加到主线程的段$S_{i,j}$中的线程数 $D_{i,j} = (1+f_{i,j})*e_{i,j}$ 每段$S_{i,j}$的中间截止日期$D_{i,j}$ $O_{i,j}=\\sum_{k=1}^{j-1}D_{i,k}$ 每个段的偏移量 任务模型 文章提出的任务模型就是DAG集合,其中每个DAG任务使用$({t_{i,j}|1\u0026lt;=j\u0026lt;=n_i},G_i,O_i,D_i)$来进行表示,其中$t_{i,j}$表示任务$T_i$的每个子任务,而$G_i$表示各个子任务之间的依赖关系,$O_i$表示整个任务的偏移量,而$D_i$表示任务的相对截至时间。\n任务集如果想在m个处理器使用任何调度算法进行调度,则需要满足下面两个条件。 $$ \\forall{t_i}\\in{T},L_i\u0026lt;D_i \\\nU(T) = \\sum_{i=1}^{n}U_i = \\sum_{i=1}^{n}\\frac{C_i}{T_i}\u0026lt;=m $$\nDAG拉伸算法 我们结合文中给出的示例来说明文章中提出的算法。\n图中我们可以获得的信息就是路径总共有六条可执行路径,分别是{1,4,6},{1,4,7},{2,4,6},{2,4,7},{3,6},{5,7}。则L = 6,也就是主路径的情况。\n这是转换为MTS之后的形式。我们根据依赖关系和主路径,将子任务转换为MTS形式。其中如果DAG图的利用率小于1,证明整个任务可以在一个处理器中顺序执行,所以只有当利用率大于1的时候才会将DAG图转换为MTS形式。从图中我们可以看出总共有5段,每段的任务数量不相同。如第一段,由于任务3,5的最坏执行时间都是2,所以第一段的$e_{i,j}$也为2。后续同理。\n转换为MTS之后,最重要的就是计算每个段有几个线程可以加入到主线程中,也就是$f_{i,j}$的计算。\n转换结果如上图。整个算法流程如此,所以整个来说,这个就是一个静态的调度算法。\n","date":"Jun 30","permalink":"https://hearecho.github.io/post/%E8%AE%BA%E6%96%87%E9%98%85%E8%AF%BB-edf/","tags":["论文阅读","EDF","DAG","算法"],"title":"论文阅读 EDF"},{"categories":["面试","redis"],"contents":"Redis 1.简介 Redis是一种非关系型(NoSQL)内存键值数据库,可以存储键和五种不同类型的值之间的映射。\n键的类型只能为字符串,保证不可变性,值的类型只有五种:字符串、列表、集合、哈希表、有序集合。\nRedis面试问题的主要出发点:数据类型、跳表、缓存、持久化、数据淘汰策略。\n2.数据类型 数据类型 可以存储的值 操作 STRING 字符串、整数或者浮点数 对整个字符串或者字符串的其中一部分执行操作,对整数和浮点数执行自增或者自减操作 LIST 列表 从两端压入或者弹出元素 ,对单个或者多个元素进行修剪, 只保留一个范围内的元素 SET 无序集合 添加、获取、移除单个元素, 检查一个元素是否存在于集合中, 计算交集、并集、差集, 从集合里面随机获取元素 HASH 包含键值对的无序散列表 添加、获取、移除单个键值对, 获取所有键值对,检查某个键是否存在 ZSET 有序集合 添加、获取、删除元素,根据分值范围或者成员来获取元素, 计算一个键的排名 3.数据结构 字典 字典是HASH的底层结构,是一种散列表结构,使用的是拉链法解决哈希冲突。Redis 的字典 dict 中包含两个哈希表 dictht,这是为了方便进行 rehash 操作。在扩容时,将其中一个 dictht 上的键值对 rehash 到另一个 dictht 上面,完成之后释放空间并交换两个 dictht 的角色。有点类似于CopyandWriteList中扩容的操作。rehash 操作不是一次性完成,而是采用渐进方式,这是为了避免一次性执行过多的 rehash 操作给服务器带来过大的负担。\n跳表 是有序集合的底层实现之一。跳跃表是基于多指针有序链表实现的,可以看成多个有序链表。在查找时,从上层指针开始查找,找到对应的区间之后再到下一层去查找。\n与红黑树等平衡树相比,跳跃表具有以下优点:\n 插入速度非常快速,因为不需要进行旋转等操作来维护平衡性; 更容易实现; 支持无锁操作。 4. redis使用场景 使用场景 详细 计数器 可以对 String 进行自增自减运算,从而实现计数器功能。Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。 缓存 一般和关系型数据库进行配合,将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。 查找表 例如 DNS 记录就很适合使用 Redis 进行存储。查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源 消息队列 List 是一个双向链表,可以通过 lpush 和 rpop 写入和读取消息 会话缓存 可以使用 Redis 来统一存储多台应用服务器的会话信息。 分布式锁 利用SETNX实现分布式锁,或者是ReadLock实现分布式锁。 5.键的过期时间 Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。\n对于散列表这种容器,只能为整个键设置过期时间(整个散列表),而不能为键里面的单个元素设置过期时间。散列表算作值。\n6.数据淘汰策略 因为我们的内存是有限的,所以对于部分数据我们总会要实行内存淘汰策略,以免超出内存发生大小。\nRedis具体有6中淘汰策略:\n 策略 描述 volatile-lru 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 volatile-ttl 从已设置过期时间的数据集中挑选将要过期的数据淘汰 volatile-random 从已设置过期时间的数据集中任意选择数据淘汰 allkeys-lru 从所有数据集中挑选最近最少使用的数据淘汰 allkeys-random 从所有数据集中任意选择数据进行淘汰 noeviction 禁止驱逐数据 使用 Redis 缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据。可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。\nRedis 4.0 引入了 volatile-lfu 和 allkeys-lfu 淘汰策略,LFU 策略通过统计访问频率,将访问频率最少的键值对淘汰。\n一般情况下还是使用前三种最好,因为不设置过期时间可能确实这些数据比较重要。\n7.持久化 Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。\nRDB 持久化 将某个时间点的所有数据都存放到硬盘上。\n可以将快照复制到其它服务器从而创建具有相同数据的服务器副本。\n如果系统发生故障,将会丢失最后一次创建快照之后的数据。\n如果数据量很大,保存快照的时间会很长。\n持久化 将写命令添加到 AOF 文件(Append Only File)的末尾。\n使用 AOF 持久化需要设置同步选项,从而确保写命令同步到磁盘文件上的时机。这是因为对文件进行写入并不会马上将内容同步到磁盘上,而是先存储到缓冲区,然后由操作系统决定什么时候同步到磁盘。有以下同步选项:\n 选项 同步频率 always 每个写命令都同步 everysec 每秒同步一次 no 让操作系统来决定何时同步 always 选项会严重减低服务器的性能; everysec 选项比较合适,可以保证系统崩溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响; no 选项并不能给服务器性能带来多大的提升,而且也会增加系统崩溃时数据丢失的数量。 随着服务器写请求的增多,AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。大致思路是将redis文件中所有的数据作为插入指令重新写入一个文件中,之后将以前文件丢弃即可。以后的命令都写入新的AOF文件。\n8.哨兵机制 Sentinel(哨兵)可以监听集群中的服务器,并在主服务器进入下线状态时,自动从从服务器中选举出新的主服务器。\n9. 缓存问题 缓存穿透 指的是对某个一定不存在的数据进行请求,该请求将会穿透缓存到达数据库。\n解决方案:\n 对这些不存在的数据缓存一个空数据; 对这类请求进行过滤 缓存雪崩 指的是由于数据没有被加载到缓存中,或者缓存数据在同一时间大面积失效(过期),又或者缓存服务器宕机,导致大量的请求都到达数据库。\n在有缓存的系统中,系统非常依赖于缓存,缓存分担了很大一部分的数据请求。当发生缓存雪崩时,数据库无法处理这么大的请求,导致数据库崩溃。\n解决方案:\n 为了防止缓存在同一时间大面积过期导致的缓存雪崩,可以通过观察用户行为,合理设置缓存过期时间来实现; 为了防止缓存服务器宕机出现的缓存雪崩,可以使用分布式缓存,分布式缓存中每一个节点只缓存部分的数据,当某个节点宕机时可以保证其它节点的缓存仍然可用。最好的解决方案。增加了系统的高可用性。 也可以进行缓存预热,避免在系统刚启动不久由于还未将大量数据进行缓存而导致缓存雪崩。 缓存一致性 缓存一致性一般就是redis数据库和mysql数据库之间在更新和删除时候的问题。其实一般情况下设置过期时间,一段时间之后缓存中数据都会消失,但是就怕在过期时间内又缓存更新的情况。\n先更新数据库,然后再立即去删除缓存。再读缓存的时候判断缓存是否是最新的,如果不是先进行更新。\n每日一题 在一个 2 x 3 的板上(board)有 5 块砖瓦,用数字 1~5 来表示, 以及一块空缺用 0 来表示.\n一次移动定义为选择 0 与一个相邻的数字(上下左右)进行交换.\n最终当板 board 的结果是 [[1,2,3],[4,5,0]] 谜板被解开。\n给出一个谜板的初始状态,返回最少可以通过多少次移动解开谜板,如果不能解开谜板,则返回 -1 。\n 遇到这种问题基本很多都是广度优先搜索,这些可以将二维数组转换为1维。按照行优先的顺序给2×3 的谜板进行编号。因此,我们在 status 中找出 00 所在的位置 x,对于每一个与 x 相邻的位置 y,我们将 status[x] 与 status[y] 进行交换,即等同于进行了一次操作。就是对位置进行标号,找出位置的相邻关系,然后进行广度优先搜索。\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 var neighbors = [6][]int{{1, 3}, {0, 2, 4}, {1, 5}, {0, 4}, {1, 3, 5}, {2, 4}} // 这种最少通过多少次变换,很多都是使用广度优先搜索 func slidingPuzzle(board [][]int) int { const target = \u0026#34;123450\u0026#34; s := make([]byte, 0, 6) for _, r := range board { for _, v := range r { s = append(s, \u0026#39;0\u0026#39;+byte(v)) } } start := string(s) if start == target { return 0 } // 枚举 status 通过一次交换操作得到的状态 get := func(status string) (ret []string) { s := []byte(status) x := strings.Index(status, \u0026#34;0\u0026#34;) for _, y := range neighbors[x] { s[x], s[y] = s[y], s[x] ret = append(ret, string(s)) s[x], s[y] = s[y], s[x] } return } type pair struct { status string step int } q := []pair{{start, 0}} seen := map[string]bool{start: true} for len(q) \u0026gt; 0 { p := q[0] q = q[1:] for _, nxt := range get(p.status) { if !seen[nxt] { if nxt == target { return p.step + 1 } seen[nxt] = true q = append(q, pair{nxt, p.step + 1}) } } } return -1 } 参考资料 CsNotes 缓存数据库更新 ","date":"Jun 26","permalink":"https://hearecho.github.io/post/redis%E9%9D%A2%E8%AF%95%E9%97%AE%E9%A2%98/","tags":["面试","redis"],"title":"Redis面试问题"},{"categories":["论文阅读","算法"],"contents":"论文简介 《A new DAG based dynamic task scheduling algorithm (DYTAS) for multiprocessor systems》\n论文提出一种基于有向无环图(DAG)的动态任务调度算法。关注点主要是通过多处理系统,进行并行处理任务。主要关注点在于多处理器系统,并行。在多处理器系统中实现高性能是调度并行任务的关键因素。而动态任务调度的目标是将并行任务映射到多处理器上,并对执行顺序进行排序。\n本文旨在建立一个基于DAGs的动态调度模型。在该模型中,一个被分配的处理器称为中心调度器,负责动态地调度任务。在提出的动态调度模型的基础上,提出了一种新的动态调度算法。该算法在仿真环境下进行了实验,实验结果表明,所提出的调度算法是一种有效的动态调度算法,具有更好的性能。\n 我们常说的对于有向无环图最短执行时间的拓扑排序算法是在单处理器上进行运行的,任务是按照排序好的序列进行执行。\n DAG介绍 有向无环图(DAG) G = (V, E),其中V是v个节点/顶点的集合,E是e个有向边的集合。边缘的源节点称为父节点,而汇聚节点称为子节点。没有父节点的节点称为入口节点,没有子节点的节点称为出口节点。\n相关工作 很多动态调度算法是为了支持实时系统进行设计的。实时系统是指系统的性能不仅取决与逻辑计算结果,而且还取决于结果产生的时间。\n调度算法分为静态调度算法和动态调度算法。\n 静态调度算法:任务的分配是离线进行的,即在实时任务正式在处理机上调度执行前,先把任务在处理机上的分配和调度时间安排好,在任务正式开始执行后按照预先的调度方案执行。这种调度方法主要用于周期任务的调度,它的优点在于能够预先安排好调动,减少任务调度过程中的开销;而缺点在于缺乏灵活性,在实际的调度中不能够及时地根据系统资源和任务的执行情况进行及时的调整。 动态调度算法:在实时系统中,很多任务并非都以周期方式在处理机上进行调度,更多任务,特别是非周期任务都是随机到达系统并动态调度执行的。在动态调度方法中,任务的分派和可调度性测试都是在系统运行时在线进行的。这种情况下,可调度性测试实际上变成了一种接受测试(acceptance test), 测试动态到达任务的截止期是否会被保证,如果无法保证任务的截止期,任务将被拒绝调度。可以看出,动态调度与静态调度相比有更好的灵活性,然后由于可调度性测试需要在线进行,它的调度算法的复杂度不能太高,并且由 于无法保证是否可以被调度,算法的可预测性(predictability)很差。也就是说动态调度算法主要算法是在线测试预估任务是否可以满足。 系统模型 负载模型 并行任务采用DAG建模。非实时DAG[7]定义为:G=(V,E),其中V是一组v个节点,E是一组w条有向边。DAG中的一个节点表示一个任务,而这个任务又是一组指令,这些指令必须在同一个处理器中顺序执行而不被抢占。节点ni的权重称为计算成本,用w(vi)表示。DAG中的边,每个边用(vi,vj)表示,对应于节点之间的通信消息和优先约束。边的权重称为边的通信开销,用c(vi,vj)表示。\n我们的系统看作有一组处理器组成,P={P1,P2,P3,…Pm},其中Pi表示具有本地存储器的处理器。处理器之间是也是具有通信开销的。\n调度器模型 如下图所示,描述的是一个同构环境中一个新的非实时调度器模型。当所有并行任务到达一个被指定的中央调度器时,他将进入一个称为初始任务队列(ITQ)的队列,等待被调度;除了ITQ之外,还管理着两个队列:调度任务队列(DTQ)和完成任务队列(CTQ)。封装在调度器中的调度算法开始与ITQ一起工作。中央调度器负责调度DTQ中的每个就绪任务。一旦调度算法启动,所有的任务都会根据其依赖的任务进行安排。在安排任务之后,调度器将任务安排到单个处理器任务队列(PTQ)。处理器将在自己的PTQ中通过同时检查CTQ中的依赖任务结果来完成任务。如果CTQ没有利用其相关任务的结果,则PTQi应指向下一个PTQi+1、PTQi+2、…PTQn、PTQ1、…PTQi-1以确定合适的任务,并将该任务迁移到PTQi。在PTQi变空之前,调度算法应停止工作。Processor Status Window(PSW)显示处于运行状态和空闲状态的每个处理器的状态。\n动态调度算法-DYTAS 基于上述的调度模型,提出了一种新的动态调度算法。ITQ中的任务是通过依赖关系进行调度的。该算法首先对于ITQ中的前面的任务进行调度,并将其映射到处理器上。而在静态调度算法中,由于DAG的数据是预先知道的,所以任务是按一定的优先级排序的。但是,本文提出的动态调度算法不同于静态调度算法,它在运行时迁移任务。\nDYTAS算法的核心是处理器的选择策略,也就是对于任务的迁徙。主要取决于如何选择任务映射到的处理器,即使任务被调度得更早。\n当从PTQ集合中中选择处理器来执行特定任务时,必须考虑两个时间索引:\n 处理器Pi的最早空闲时间 处理器Pi上任务vi的最早开始时间。 在所提出的调度模型中,ITQ中的并行任务和就绪任务都在处理器的PTQ中。即使ITQ和DTQ位于中央调度器上,实际执行映射任务的处理器也与调度器分离并放置在PTQi处。同时,调度过程和执行过程是并行的。因此,调度器和工作处理器之间是同步的。\n 该论文算法有点问题,没有讲清楚部分参数的含义。给出的cc参数不知道是什么意思。不知道为啥引用还那么多。\n 算法简单解释 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 Procedure DYTAS # 这个部分是对整个任务进行拓扑排序 dtq[ ] = SORT[Ti , Tj] l = 0; # 将排序好的任务放置在各个不同ptq也就是处理器本地存储队列 while (dtq[ ] is not empty) do for i = 1 to n ptqi = dtq [l] l = l + 1 end for; end while; for each processor Pk in processor group do # 对每个处理器状态进行检查,如果处理器在运行,则选择下一个处理器队列 while (Pk is in running state) skip and select the next ptqk+1 end while Pk = ptqk[j] # 如果这个任务的前置任务已经完成,并且有处理器空闲,则执行任务 if (dependent task of ptqk[j] is in ctq) TASK(ptqk[ ],Pk , j, ctq, cpk, CTj) else # 否则的话,就是将指针指向下一个ptq队列判断,下一个队列同样位置上的任务是否可以执行。算法的关键就在这个部分 # 关键在于在执行的时间迁移任务。 do move the pointer to the next ptq if (dependent task of ptqk[j] is in ctq) TASK(ptqk[ ],Pk , j, ctq, cpk, CTj) exit do endif while(checking with all ptq’s once) endif end for end DYTAS Procedure for TASK # 任务执行 procedure TASK(ptqk[ ],Pk,j,ctq,cpk,CTj) do Tj with Pk remove Tj from ptqk insert Tj in ctq cpk = cpk + Ctj end TASK 实验 实验数据 cp是任务执行时间,cc不清楚是什么,也没说。\n 经过拓扑排序之后,装配在ptq中的任务仿真图。拓扑排序结果是有很多种结果的。但是不影响算法运行。\n !!!重要的运行时间迁移任务\n 结果展示,单处理器中的调度长度是240个时间单位。在第一个处理器(P1)中完成任务所用的时间是80个时间单位,P2是65个时间单位,P3是67个时间单位,P4是66个时间单位。\n !!!论文意义不是很大,不清楚为什么那么多引用这篇文章的。\n每日一题 你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字: \u0026lsquo;0\u0026rsquo;, \u0026lsquo;1\u0026rsquo;, \u0026lsquo;2\u0026rsquo;, \u0026lsquo;3\u0026rsquo;, \u0026lsquo;4\u0026rsquo;, \u0026lsquo;5\u0026rsquo;, \u0026lsquo;6\u0026rsquo;, \u0026lsquo;7\u0026rsquo;, \u0026lsquo;8\u0026rsquo;, \u0026lsquo;9\u0026rsquo; 。每个拨轮可以自由旋转:例如把 \u0026lsquo;9\u0026rsquo; 变为 \u0026lsquo;0\u0026rsquo;,\u0026lsquo;0\u0026rsquo; 变为 \u0026lsquo;9\u0026rsquo; 。每次旋转都只能旋转一个拨轮的一位数字。\n锁的初始数字为 \u0026lsquo;0000\u0026rsquo; ,一个代表四个拨轮的数字的字符串。\n列表 deadends 包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。\n字符串 target 代表可以解锁的数字,你需要给出解锁需要的最小旋转次数,如果无论如何不能解锁,返回 -1 。\n 题目中问的是最小拨动次数,对应图中两者之间的最短路径,所以这种类型的题大多都广度优先搜索,因为有四个位置,并且每次拨动都有两种选择。而对于已经搜索过的图将不会再次搜索。对于每次的字符串他的下一个变化的字符串有八个。\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 func openLock(deadends []string, target string) int { step := -1 queue := make([]string, 0) visited := make(map[string]bool, 0) for i := 0; i \u0026lt; len(deadends); i++ { visited[deadends[i]] = true } if _, ok := visited[\u0026#34;0000\u0026#34;]; ok { return -1 } queue = append(queue, \u0026#34;0000\u0026#34;) for len(queue) != 0 { size := len(queue) //没过一层就要步数加1,刚开始的0000不算在内 \tstep++ for i := 0; i \u0026lt; size; i++ { cur := queue[i] //当前字符串与目标字符串相同则return \tif cur == target { return step } //取出现在的队头字符串 \tfor j := 0; j \u0026lt; len(cur); j++ { //每个字符的变化,之后再将 \tchangenum, _ := strconv.Atoi(cur[j : j+1]) nstr1, nstr2 := \u0026#34;\u0026#34;, \u0026#34;\u0026#34; if changenum == 9 { nstr1 = cur[:j] + strconv.Itoa(0) + cur[j+1:] } else { nstr1 = cur[:j] + strconv.Itoa(changenum+1) + cur[j+1:] } if changenum == 0 { nstr2 = cur[:j] + strconv.Itoa(9) + cur[j+1:] } else { nstr2 = cur[:j] + strconv.Itoa(changenum-1) + cur[j+1:] } if _, ok := visited[nstr1]; !ok { queue = append(queue, nstr1) visited[nstr1] = true } if _, ok := visited[nstr2]; !ok { queue = append(queue, nstr2) visited[nstr2] = true } } } queue = queue[size:] } return -1 } ","date":"Jun 25","permalink":"https://hearecho.github.io/post/%E8%AE%BA%E6%96%87%E9%98%85%E8%AF%BB-dag/","tags":["论文阅读","DAG","算法"],"title":"论文阅读 DAG"},{"categories":["blog"],"contents":"添加谷歌收录 添加谷歌收录的方式主要有四种方式:\n Google Analytics HTML file HTML tag Google Tag Manager Domain name provider 最简单的方式就是使用HTML TAG直接在模板head标签里面加上网站给出的验证标签即可。\ngoogle 分析 添加谷歌分析,可以获知自己网站的各项数据。同时也可以用于上述使得网站被谷歌收录。\n注册Google 分析 打开Google Analytics官网注册账户并添加自己的网站域名 打开主页,添加数据流,之后记录衡量ID。 修改配置文件 在config.toml中新建googleAnalytics参数并设置成自己的衡量ID\n1 googleAnalytics = \u0026#34;xx-xxxxxxxxx-x\u0026#34; # Enable Google Analytics by entering your tracking id 新建模板 在Hugo站点根目录下新建模板文件(./layouts/_internal/google_analytics_async.html)并添加如下代码.\n1 2 3 4 5 6 7 8 9 \u0026lt;!-- Global Site Tag (gtag.js) - Google Analytics --\u0026gt; \u0026lt;script async src=\u0026#34;https://www.googletagmanager.com/gtag/js?id={{ .Site.GoogleAnalytics }}\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script\u0026gt; window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag(\u0026#39;js\u0026#39;, new Date()); gtag(\u0026#39;config\u0026#39;, \u0026#39;{{ .Site.GoogleAnalytics }}\u0026#39;); \u0026lt;/script\u0026gt; 引用模板 在baseof.html基础模板文件中的head标签尾部添加如下代码, 这样站点发布到非Hugo Server后就会自动引用Google Analytics模板.或者也可以将上述的模板内容直接粘贴复制到baseof.html相应的位置。\n1 2 3 4 5 \u0026lt;head\u0026gt; {{- if not .Site.IsServer }} {{ template \u0026#34;_internal/google_analytics_async.html\u0026#34; . }} {{- end }} \u0026lt;/head\u0026gt; 每日一题 下一个排列 实现获取 下一个排列 的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。必须 原地 修改,只允许使用额外常数空间。\n 目的是寻求按照字典序来说,下一个排列,当然可以找到全部排列,但是不太靠谱。我们可以自己通过该排列的顺序,找到下一个排列。\n例如:\n[1,2,3]\n[1,3,2]\n[2,1,3]\n[2,3,1]\n[3,1,2]\n[3,2,1]\n所以我们的目标是将左边一个较小的数和右边的一个较大的数进行交换,如此下一个排列才会更大,同时由于不能打太多。同时我们要让这个「较小数」尽量靠右,而「较大数」尽可能小。当交换完成后,「较大数」右边的数需要按照升序重新排列。这样可以在保证新排列大于原来排列的情况下,使变大的幅度尽可能小。所以可以使用两次排序来确定两个数字的位置,然后进行交换.\n以排列 [4,5,2,6,3,1][4,5,2,6,3,1] 为例:\n我们能找到的符合条件的一对「较小数」与「较大数」的组合为 2 与 3,满足「较小数」尽量靠右,而「较大数」尽可能小。\n当我们完成交换后排列变为 [4,5,3,6,2,1][4,5,3,6,2,1],此时我们可以重排「较小数」右边的序列,序列变为 [4,5,3,1,2,6][4,5,3,1,2,6]。\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 func nextPermutation(nums []int) { // 先找到一个最小数,然后找到比这个较小数稍微大的较大数 \tn := len(nums) i := n - 2 //找到此时分割左右边的位置 // 也就是较小数尽可能的小 \tfor i \u0026gt;= 0 \u0026amp;\u0026amp; nums[i] \u0026gt;= nums[i+1] { i-- } if i \u0026gt;= 0 { j := n - 1 //找到较小数 \tfor j \u0026gt;= 0 \u0026amp;\u0026amp; nums[i] \u0026gt;= nums[j] { j-- } //交换 \tnums[i], nums[j] = nums[j], nums[i] } //之后反转右边的序列 \treverse(nums[i+1:]) } func reverse(a []int) { for i, n := 0, len(a); i \u0026lt; n/2; i++ { a[i], a[n-1-i] = a[n-1-i], a[i] } } ","date":"Jun 24","permalink":"https://hearecho.github.io/post/hugo%E6%B7%BB%E5%8A%A0google%E6%94%B6%E5%BD%95/","tags":["Hugo","Google Console","杂记"],"title":"Hugo添加Google收录"},{"categories":["blog"],"contents":"简介 使用hugo搭建个人博客,并结合Github与Travis CI实现自动化集成部署。\n 本地运行 hugo下载(windows) 1 brew install hugo 检查可用之后,使用命令新建一个网站(不用新建文件夹,hugo会自动建立):\n1 2 hugo new site your-site-name cd your-site-name 主题下载 主题是放到themes目录中,一般从hugo themes中找到想要的主题,下载到themes文件夹中。需要修改配置文件中相关配置,名字为文件夹名称。\n静态资源位置 静态资源位置一般是在网站目录下的static文件夹中\n添加文章 1 2 3 hugo new post/first.md # 该文件会在 content/post/目录下 # 执行编译之后,产生的文件在public目录下 运行 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 hugo server -D # 该条命令是本地测试运行,有可能markdown文件的draft标签为true,在真正编译的时候需要把true改为false # 不然不会显示 hugo # 就是编译命令 编译结果如下 Start building sites … | EN -------------------+----- Pages | 12 Paginator pages | 0 Non-page files | 0 Static files | 1 Processed images | 0 Aliases | 4 Sitemaps | 1 Cleaned | 0 Total in 76 ms 部署 部署我们一般使用两个仓库,一个仓库(blog)用于存放源文件,一个仓库(*.githu.io)用于存放生成的网站静态文件。\n存放源文件的仓库会在Travis中使用。\n 生成github token 前提是仓库已经全部建立,此时我们进入token,生成GITHUB_TOKEN。repo标签内全部选上即可。\n 结合Travis 前提是仓库已经全部建立,此时我们登录tracis ci,使用github进行登录,然后根据提示选择我们需要的仓库。也就是用于存放源文件的仓库。点击仓库右边的setting按钮。进入之后,在下方Environment Variables中添加变量名为GITHUB_TOKEN(这个随意,不过后面取得时候要注意保持一致)。\n 添加.travis.yml文件 这里我直接列举我自己得文件,对应改以下就好。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 language:gogo:- \u0026#34;1.15\u0026#34;# 指定Golang 1.15# Specify which branches to build using a safelist# 分支白名单限制:只有 master 分支的提交才会触发构建branches:only:- masterinstall:# 安装最新的hugo- wget https://github.com/gohugoio/hugo/releases/download/v0.84.0/hugo_0.84.0_Linux-64bit.deb- sudo dpkg -i hugo*.deb# 安装主题- git clone https://github.com/WingLim/hugo-tania.git themes/tania --depth=1script:# 运行hugo命令- hugoafter_script:# 部署- cd ./public- git init- git config user.name \u0026#34;hearecho\u0026#34;- git config user.email \u0026#34;1540302560@qq.com\u0026#34;- git add .- git commit -m \u0026#34;Update Blog By TravisCI With Build $TRAVIS_BUILD_NUMBER\u0026#34;# Github Pages- git push --force --quiet \u0026#34;https://$GITHUB_TOKEN@${GH_REF}\u0026#34; master:masterenv:global:# Github Pages- GH_REF:github.com/hearecho/hearecho.github.iodeploy:provider:pages# 重要,指定这是一份github pages的部署配置skip-cleanup:true# 重要,不能省略local-dir:public# 静态站点文件所在目录# target-branch: master # 要将静态站点文件发布到哪个分支github-token:$GITHUB_TOKEN# 重要,$GITHUB_TOKEN是变量,需要在GitHub上申请、再到配置到Travis# fqdn: # 如果是自定义域名,此处要填keep-history:true# 是否保持target-branch分支的提交记录on:branch:master# 博客源码的分支 提交与检查 到此时,基本上工作已经完成。提交我们此次更改之后,travis会自动进行build,如果出错,应该是步骤问题。\nleetcode 每日一题 请实现一个函数,输入一个整数(以二进制串形式),输出该数二进制表示中 1 的个数。例如,把 9 表示成二进制是 1001,有 2 位是 1。因此,如果输入 9,则该函数输出 2。\n 解题思路,num与num-1在二进制位进行表示得时候,每次都会在第一个1出现得位置,变得不同,所以我们每次让num与num-1做与运算,直至num\u0026lt;=0\n 1 2 3 4 5 6 7 8 func hammingWeight(num uint32) int { x := 0 for num \u0026gt; 0 { num \u0026amp;= num-1 x++ } return x } ","date":"Jun 23","permalink":"https://hearecho.github.io/post/hugo%E6%90%AD%E5%BB%BA%E4%B8%AA%E4%BA%BA%E5%8D%9A%E5%AE%A2/","tags":["Hugo","Travis CI","杂记"],"title":"Hugo搭建个人博客"},{"categories":null,"contents":"","date":"Jun 23","permalink":"https://hearecho.github.io/articles/","tags":null,"title":"全部文章"}]