Skip to content

How to turn on AVX2 in VirtualBox Guest

  • CPU: Intel(R) Core(TM) i7-4790 CPU @ 3.60GHz (AVX2\AVX)
  • HOST OS: Windows 7
  • Guest: Ubuntu 16.04.1 LTS 64 bit

In Host (which Dev is my VM name)

C:\Program Files\Oracle\VirtualBox>VBoxManage.exe setextradata "Dev" VBoxInternal/CPUM/IsaExts/AVX2 1

Check by:

C:\Program Files\Oracle\VirtualBox>VBoxManage.exe getextradata "Dev" enumerate
Key: GUI/Fullscreen, Value: true
Key: GUI/LastCloseAction, Value: SaveState
Key: GUI/LastGuestSizeHint, Value: 1920,995
Key: GUI/LastNormalWindowPosition, Value: 26,30,944,394,max
Key: GUI/LastScaleWindowPosition, Value: 8,30,1272,928
Key: GUI/RestrictedRuntimeDevicesMenuActions, Value: HardDrives
Key: GUI/RestrictedRuntimeMachineMenuActions, Value: SaveState,PowerOff
Key: GUI/ScaleFactor, Value: 1
Key: GUI/StatusBar/Enabled, Value: false
Key: GUI/StatusBar/IndicatorOrder, Value: HardDisks,OpticalDisks,FloppyDisks,Net
work,USB,SharedFolders,Display,VideoCapture,Features,Mouse,Keyboard
Key: VBoxInternal/CPUM/IsaExts/AVX2, Value: 1

Restart your VM and bingo!

Go汇编学习 2.解构AMD64 bytes.Equal

SIMD

上一篇学了内存结构基本知识,本文将学习符号(symbol)、语句的含义。
我个人喜欢通过例子来学习,所以,我就从src/runtime/asm_amd64.s里的bytes·Equal入手吧:)

对应代码

TEXT bytes·Equal(SB),NOSPLIT,$0-49
MOVQ a_len+8(FP), BX
MOVQ b_len+32(FP), CX
CMPQ BX, CX
JNE eqret
MOVQ a+0(FP), SI
MOVQ b+24(FP), DI
LEAQ ret+48(FP), AX
JMP runtime·memeqbody(SB)
eqret:
MOVB $0, ret+48(FP)
RET

预备知识

SB (static base)相关知识

以下是Go asm中的介绍

The SB pseudo-register can be thought of as the origin of memory, so the symbol foo(SB) is the name foo as an address in memory. This form is used to name global functions and data. Adding <>to the name, as in foo<>(SB), makes the name visible only in the current source file, like a top-level static declaration in a C file. Adding an offset to the name refers to that offset from the symbol’s address, so foo+4(SB) is four bytes past the start of foo.

大致翻译一下,例如foo(SB)的符号,就对应了code segement中的地址,全局可见。当添加了 <> 符号后,就变为了当前文件可见,类似于C文件的static声明,还可以通过添加偏移量(offset)来访问其他地址。

指令格式

例子中的TEXT指令就定义了一个叫bytes·Equal的符号(注意是中点号·),接下来就是对应的指令(可以理解成函数体),而最后RET则是返回指令(退出当前stack)。通常情况下,参数大小后跟随着stack frame的大小,使用减号(-)分割。$0-49意味着这是一个0-byte的栈,并且有49-byte长的参数。NOSPLIT说明,不允许调度器调整stack frame的大小,这就意味着必须人工指定stack frame大小。
但为什么是49个byte?

因为我们可以看看bytes.Equal的定义

func Equal(a, b []byte) bool

a, b 分别为[]byte(不定长的byte slice),而slice的结构是:

type slice struct {
array unsafe.Pointer
len int
lcap int
}

unsafe.Pointer 在amd64上是uintptr,即uint64。int在amd64上背后是int64。
因此一个slice占用了3个qword(word=2byte qual=4 即 2x4=8byte 8x8=64bit),即 3x8 = 24byte,然后又有两个slice做为参数,再加上一个bool byte,因此,这个call stack frame应该有 24x2 ([]byte) + 1 (bool) = 49byte。又因为不需要局部变量,因此定义为0个.

$0-49

函数指令解构

汇编是门“死脑筋”+“疯狂简写”的语言,接下来是对函数语句的解析,一旦理解了以后,语句是很简单的。

 MOVQ a_len+8(FP), BX // move qword, a slice的长度放入BX寄存器
MOVQ b_len+32(FP), CX // b slice的长度放入CX寄存器
CMPQ BX, CX // compare qword 对比BX,CX
JNE eqret // jump not equal 如果不相等就跳转至标签eqretequal ret
MOVQ a+0(FP), SI // a的指针放入SI寄存器中
MOVQ b+24(FP), DI // b的指针放入DI寄存器中
LEAQ ret+48(FP), AX // load effective address, 将返回值的内存地址放入AX寄存器中
JMP runtime·memeqbody(SB) // JUMP, 跳转至 runtime·memeqbody(SB) 地址空间
eqret:
MOVB $0, ret+48(FP) // move byte, $0 (意思是数字0, false = 0)传入返回的参数中,即两个slice不相等
RET

这里出现了两个新的概念:

偏移量定义,例如a_len+8(FP),还记得上一篇中讲过,FP是指在低内存位上的么?因此,这里就定义了a_len,即a length = +8(FP),相对于FP偏移8个byte(记得slice的结构吧),这个正好是a的长度所在的位置。不记得的话,可以参考下图


FP +------------> b pointer
|
+ | +-------> b length
| | |
v | | +-> b capacity
Low | | |
+----+----+----+-+--+-+--+--+-+-+
| | | | | | | |
+-+--+-+--+-+--+----+----+----+++
| | | |
| | +-> a capacity +-> return value
| |
| +------> a length
|
+-----------> a uint64-pointer

每个空格相当于一个byte

另一个概念是label,汇编不同于高级语言,汇编的条件跳转基本上都是靠label(标签)实现的,例子中的eqret,就是个label。

AVX

接下来是精华hugeloop

AVX是Intel引以为傲的SIMD指令集,具体介绍在AVX,Go在字符比较中根据CPU的能力分别会使用SSE、AVX、AVX2,这种指令集优化就是我们为什么要写汇编的原因了。


// 64 bytes at a time using ymm registers (一次就能对比64byte64倍性能就问你怕不怕)
hugeloop_avx2:
CMPQ BX, $64 // 对比字符长度
JB bigloop_avx2 // 不够64个字节就用其他方法。
VMOVDQU (SI), Y0 // AVX2 专用加载数据的指令,将SI前32个byte加载进Y0寄存器512bit
VMOVDQU (DI), Y1 // ...
VMOVDQU 32(SI), Y2
VMOVDQU 32(DI), Y3
VPCMPEQB Y1, Y0, Y4 // 对比Y0 - Y1,把结果存入Y4中
VPCMPEQB Y2, Y3, Y5 // 同上
VPAND Y4, Y5, Y6 // AND 操作
VPMOVMSKB Y6, DX // MOVE BYTE MASK  Y6中的每8个bit做一个掩码存入DX中(简单点就是相同就都是0xf
ADDQ $64, SI // SI位移64个byte
ADDQ $64, DI // DI位移64个byte
SUBQ $64, BX // BX 长度减64
CMPL DX, $0xffffffff // 对比DX的低位
JEQ hugeloop_avx2 // 相同则继续对比
VZEROUPPER // 清空Y寄存器
MOVB $0, (AX) // 发现不同,返回
RET

小结

现在对Golang的汇编比较熟悉了,下一篇会摘抄并翻译一些注意事项。

Go汇编学习 1.进程内存地址与寄存器

前言

本文是我学习Go汇编的一些心得,网上关于Go的汇编资料实在是太少了,本人计算机不是科班,所以__可能会有错误__。

进程内存地址空间

操作系统执行程序时,会把程序的二进制部分加载进内存,同时分配一定的内存空间。如下图所示

TEXT/Code segment

TEXT segment,或者叫Code segment,保存了程序的指令。

  • 只能执行
  • 固定长度

Data segment

Data segment 保留着进程初始化的全局和静态的变量。这个数据段可以变为初始化只读区域(initialized read-only area)和初始化读写区域(initialized read-write area)。

Gvar Section

一个全局变量初始化和存储在data segment。这个区域可读/写,但是不能在进程之间共享数据。

BSS Section

这个section保存着未初始化的数据。 这个section的又叫做零初始化数据section(zero-initialized data section)

Heap Section

heap是进程的线性增长地址空间,当程序使用malloc()获取的动态内存,这份内存就是在heap之中。heap区域是在BSS segement之后,并会“增长”到更高的地址空间。heap区域是所有共享库、动态模块都可见的。

  • 可读写
  • 变量大小
  • 请求之后动态分配,需要自己释放

Stack Segment

这个Segement保存所有局部变量。当调用函数时,这个函数的局部变量就全部分布在stask上(这个函数就形成了stack frame)。当函数退出时,所有该函数相关的局部变量会自动从stask上退出。其他信息,包括返回地址和局部参数,也是保存在stack的stack frame之上。stack是LIFO(last in first out 先进后出)结构,一般是分配在内存的高地址上,因此是向低位增长,还有每个寄存器,中间值或者stack frame。

  • 可读写
  • 变量大小
  • 动态增长\缩小

跟Go asm有什么关系?

Go有几个虚拟寄存器

  • FP (stack frame pointer) stack帧低位指针,指向参数和局部变量,offset为正数
  • SP(virtual stack pointer)stack帧高位指针(栈顶),offset应为负数
  • PC (program counter) 程序计数器,负责跳转和分支
  • SB (static base pointer) 静态全局符号(symbol)

其中FP和SP的区别如下图所示:


High +-----------------+
| |
| Stack frame 0 |
| |
+-----------------+
| | <---+ SP
| Stack frame 1 |
| | <---+ FP
Low +-----------------+

这里有个疑问的地方,啥是符号(symbol),根据Symbol里的解释是:

符号(symbol)是帮助人们标记特定内存地址的东西

也就是说例如

TEXT DEMO(SB)

就代表指向code segment中代表DEMO函数的内存地址。

寄存器

每种CPU都有不同的寄存器,AMD64平台里常用的有以下几个:

  • [A-D]X //通用的64位寄存器
  • R8-15 // 之前的其实是A-Dx、SI、DI等寄存器占用了
  • SI // 源(source register)
  • DI // 目的地 (destination register)
  • X0-15 // sse用
  • Y0-15 // avx2 用

了解完内存结构和寄存器后,下一篇将学习各种符号的意义:)

程序中的时间问题

leap second

最近Russ Cox提了一条proposal《Monotonic Elapsed Time Measurements in Go》

这个proposal主要解决了一个问题,计算时间差。
我一开始觉得,这不是很简单么?例如:

t1 := time.Now()# ... 10 ms of work 
t2 := time.Now() 
const f = "15:04:05.000" 
fmt.Println(t1.Format(f), t2.Sub(t1), t2.Format(f))

简单的相减,就可以得到时间差。但这里隐含了一个谬误,就是:

默认:程序后执行的Now,肯定比之前执行的Now晚。

时间,在人的认知里肯定是单向向前,但在计算机系统里,却不是。因为有正、负闰秒的存在,常常有bug出现,例如:Cloudflare RRDNS 故障

这起事故中,CF的RRDNS使用两个时间的差值进行权重计算,平时是没有问题,但当闰秒的出现时,程序差值就出现了负数,最终酿成了事故。所以在对时间差有严重依赖的程序(纳入计算权重等),需要使用的是monotonic time(单调递增时间),而不是wall time(系统时间),这也就促成了Russ Cox的proposal。

不但如此,程序员还有对于时区、时差、时间精度、起始的谬误。因此,就有人总结出了《程序员认知时间的谬误(Falsehoods programmers believe about time)》 ,我摘抄了部分国内程序员常犯的错误,括号中是我个人对于这些谬误的理解:

  1. 一天总有24小时
  2. 一个月有30或者31天(还有二月份)
  3. 一年有365天 (闰秒导致多一秒,1582年整整消失了10天
  4. 二月份总是28天(有29天)
  5. 24小时是一天、周、月的周期值(对时间取模算天数、我也犯过)
  6. 每年相同月份里,周的起始是相同的
  7. 机器的时区永远是GMT,不,开玩笑的,是机器的时区永远不变(时区的设置可能会被调整)
  8. 系统时钟永远设置的是本地对应的时区的时间
  9. 目标时区和GMT永远有相同的间隔时间(时区会变化,夏令时)
  10. 客户端的时间和服务器时间永远相同
  11. 客户端时间和服务器时间的差值没什么大不了的
  12. 就算CS有差值,总是相同的间隔(客户端可能会动态调整时间)
  13. 服务器和客户端的时间永远在同一个时区
  14. 系统时间不会在5000年前、或者5000年后
  15. 时间没有起点和尽头(千年虫、Unix epoch)
  16. 系统中的一分钟和其他机器上的一分钟应该是一样的(各个机器上CPU频率等问题导致时钟偏移,所以请使用ntp)
  17. 最小的时间单位是秒、呃、毫秒……(最小单位是CPU确定的)
  18. 大家都能明白时间戳格式(1339972628 或者是 133997262837)
  19. 时间戳格式永远是同一个格式(64位和32位就有区别)
  20. 时间戳精度永远相同(float精度问题)
  21. 时间戳的精度保证了可以做uid
  22. 全世界都能明白11/07/05是什么时间格式(年月日显示格式不明确)

我的经历中,见得比较多的是对于时区了解的匮乏,很多程序员并不知道夏令时,甚至有产品或者老板的需求就只是给国人服务,当要全球化时就抓瞎了。当然,最多的还是对闰秒的无视,或者有人根本不知道闰秒的存在。不过,这些bug都因为系统对时间依赖程度不高,或因为系统跑的时间不够长,(没长到碰到闰秒就下线了),所以并没有导致算错帐或者生产事故。不过,这些理由都不能为这种bug开脱,所以程序员要学习东西还是很多的。

p.s. 你在生产系统里碰到过什么关于时间的bug?欢迎讨论~

《思考的技术》读后感

最近读《思考的技术》,摘抄一些重点,梳理一下读后感。

总结:逻辑思考力可以解决问题,预见未来!

科学地思考

怎么做比做到了没有重要,而科学证明结论的顺序是:
收集数据 -> 假设 -> 结论

容易犯错的地方:

  1. 把假设视为结论(不证明)
  2. 仅仅把初步结论或现象视为根本原因或问题所在(缺乏进一步思考,问为什么?)
  3. 信息收集错误(一定要保证数据的正确性)
  4. 全方位的努力是错的。(自身时间、资源有限,要抓重点)
  5. 信息要全面的综合分析,不能仅仅分析数据,要注意其他影响因素。(MECE原则)

如何提建议

  1. 最好要判断题(一个建议就够了)
  2. 要有事实依据
  3. 说明时提出充分必要条件(必要=必须要这么做,充分=这么做会更好)
  4. 前期沟通(感性)、事后追踪效果(理性)。
  5. 从事前工作开始报告,然后简要地得出结论。
  6. 要按对方能理解的顺序讲

提案的构成

  1. 业界的动态 (大趋势)
  2. 其他竞争对手的动态 (对手趋势)
  3. 自身状态分析 (我方状态)
  4. 改善条件
  5. 解决方案
  6. 建议
  7. 计划步骤

思考问题的本质

  1. 5W1H (Who、When、Where、What、Why、How)
  2. 对事不对人
  3. 头脑风暴、否定自己
  4. 思考动机,加以验证

非线性思维

  • 要多问为什么
  • 不相信权威、不盲从
  • 储蓄智慧:与人接触获取知识(杂志、报纸、书、电视、广播、与人交流、互联网、自己思考)
  • 生活简单才能思考:应该思考、彻底思考;不该思考,别浪费精力。以能否规律化为依据。
  • 先分解功能再进行思考。(先将问题简单化条理化)

TCP BBR 测评与开启方法

速率提升490%!
这就是Google在Linux 4.9内核中提交的TCP BBR拥塞控制算法带来的提升.😲

源站点位于香港,我分别从东京, 美国 和深圳进行了下载测试.结果如下图
BBR vs Cubic benchmark

可见开启BBR后,东京的服务器下载速度达到了36MB/s,近乎内网的速度.
对于深圳的提速也十分明显,几乎是之前的4倍.

而BBR的方法也很简单.我以Debian8做例子
在/etc/apt-get/source.list中添加

deb http://http.hk.debian.org/debian sid main non-free contrib

升级和安装新内核

apt-get update && apt-get upgrade

下载新的内核后,在/etc/sysctl.d/local.conf中添加

net.core.default_qdisc=fq
net.ipv4.tcp_congestion_control=bbr

根据文件进行开启

sysctl -p /etc/sysctl.d/local.conf

验证

sysctl net.ipv4.tcp_congestion_control

如果返回bbr,就说明成功了.

FPM 应用打包神器

pkg

背景

应用程序写好了以后,需要分发给其他人,这时就需要控制启动脚本啊、配置管理啊,这些事情其实还是需要操作系统提供的rpm、deb、pkg来完成,但是要写rpm-build, dpkg-config。之前写过rpm打包过程,要有自动构建等操作,还需要对文件attr进行控制,各种细节都要写。 累都累死了。
包对安装者很方便,对写包的人太蛋疼,而这就是我们今天的主角要解决的问题。

fpm @Github

真不是php-fpm模块

fpm官方号称支持以下类型打包

  • deb
  • rpm
  • solaris
  • freebsd
  • tar
  • directories
  • Mac OS X .pkg files (osxpkg)
  • pacman (ArchLinux) packages

太赞了!我的主机是Ubuntu(deb),那这次我们的目标就是Redhat的rpm好了!

安装fpm

首先安装fpm,这货依赖ruby的gem,所以

sudo apt-get install ruby ruby-dev rubygems gcc make
sudo gem install --no-ri --no-rdoc fpm

这些都安装好了以后,我们就可以开始打包了。

打包方式

推荐直接构建文件目录的方式,因为这样很简单。简单到你怎么安排文件目录结构,fpm就忠实地从根目录开始还原。例如下面这个结构

 $ mkdir test && cd test && mkdir -p usr/bin && mkdir -p var/log/dummy
$ cd .. && tree test
.
├── usr
│   └── bin
└── var
└── log
└── dummy

执行

fpm -s dir -C test

就是把test当成根目录/ , test/var 就会变成/var

简单吧!

目标包确定

我们这次选择的是rpm,所以-t (type)就指定rpm了。

fpm -s dir -C test -t rpm

接下来要确定包的名字,例如我们这次叫hi,所以-n (name) 就是 hi

$ fpm -s dir -C test -t rpm -n hi
Created package {:path=>"hi-1.0-1.x86_64.rpm"}

执行完了以后,就自动出了一个rpm包。
看看里面有什么

rpm -qlp hi-1.0-1.x86_64.rpm
/usr/bin
/var/log/dummy

Bingo!这样就做好rpm包了。

高级用法

脚本

某些包需要执行一些脚本, 可以通过--after-install --before-install等参数指定.需要注意的是, fpm会自动读取并放在包中.

配置文件

rpm和deb都有自动配置文件管理(自动diff等功能)所以最好一开始就加上所有的配置文件. --config--files 就可以指定了. 不过,deb会默认/etc下的所有文件都是配置.

一些小tips

  • git自动生成版本号 git describe --tags
  • rpm 查询包内容 rpm -qlp *.rpm
  • deb 查询包内容 dpkg -c *.deb
  • logrotate是个好功能 fpm -d logrotate会自动更新配置

2016年终总结

工作

😂这个表情真的代表了我今年的心情,本来以为可以在Glu再多干一年的,结果4月份公司就解散了。加上之前答应老婆去深圳,于是就搬来深圳了。
刚到的那几天就开始面试,结果各个公司都问,为啥不去鹅厂试试,没想到,一面试,就真的进了鹅厂清水衙门TEG😂

6月份入的职,当时就震惊啦,朗科这边不比腾大差啊,超级海景办公室,配合着六月的阳光,那是相当的漂亮。

鹅厂大牛是多,问题也多。不过得到了离职那天再来评论评论。

博客

这里也开始长草了,不过新版的bla已经日趋完善了,希望明年能和川普一样能猛刷博客。

读书

  • 《那些古怪又让人忧心的问题》
  • 《思考的技巧》
  • 《Wireshark网络分析》
  • 《数据科学入门》
  • 《机器学习 在线课程》

自己在AI领域没有优势,今年只是积累了些基础,明年一定要好好在实战中啃下来。
Go语言方面最好能学会用汇编编写,这也是提高性能,深入学习计算机的好办法。

给程序添加Prometheus监控

Demo

最近在编写Bla的时候,发现需要统计平均响应时间,CPU和内存之类的数据,在公司已经用过了metricbeat,感觉还好,但技术栈是Java的,而且内存要求太高了,所以只好换成Golang编写Prometheus做分析数据库。

Prometheus跟Golang官方标准库expvar是属于Pull的监控类型。需要服务器去拉取采集器上面的数据,因此,daemon类的程序(比如说server)最方便的地方就是直接bind一个接口暴露数据就可以了。

代码时间

go get github.com/prometheus/client_golang/prometheus

然后在自己的启动main.go里添加

func init() {
prometheus.MustRegister(httpRequestCount)
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":8080", nil)
}
var (
httpRequestCount = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: "http",
Subsystem: "request",
Name: "requests_count",
Help: "The total number of http request",
})
)

代码部分就完成了。
当你需要变更数值时,直接Inc()那个httpRequestCount就好了。

测试一下

curl http://localhost:8080/metrics
# HELP http_request_requests_count The total number of http request
# TYPE http_request_requests_count counter
http_request_requests_count 73

官方对于这些Namespace、Subsystem如何命名有详细的规定,具体的大家自己看就好了。

配置Prometheus

下载安装好Prometheus之后,将刚才我们暴露的metric接口配置进去

 - job_name: "bla"
scrape_interval: "15s"
static_configs:
- targets: ['localhost:9200']

大功告成啦~~

3种优雅的Go channel用法

写Go的人应该都听过Rob Pike的这句话

Do not communicate by sharing memory; instead, share memory by
communicating.

相信很多朋友和我一样,在实际应用中总感觉不到好处,为了用channel而用。但以我的切身体会来说,这是写代码时碰到的场景不复杂、对channel不熟悉导致的,所以希望这篇文章能给大家带来点新思路,对Golang优雅的channel有更深的认识
:)

Fan In/Out

数据的输出有时候需要做扇出/入(Fan
In/Out),但是在函数中调用常常得修改接口,而且上下游对于数据的依赖程度非常高,所以一般使用通过channel进行Fan
In/Out,这样就可以轻易实现类似于shell里的管道。

func fanIn(input1, input2 <-chan string) <-chan string {
c := make(chan string)
go func() {
for {
select {
case s := <-input1: c <- s
case s := <-input2: c <- s
}
}
}()
return c
}

同步Goroutine

两个goroutine之间同步状态,例如A goroutine需要让B goroutine退出,一般做法如下:

func main() {
g = make(chan int)
quit = make(chan bool)
go B()
for i := 0; i < 3; i++ {
g <- i
}
quit <- true // 没办法等待B的退出只能Sleep
fmt.Println("Main quit")
}
func B() {
for {
select {
case i := <-g:
fmt.Println(i + 1)
case <-quit:
fmt.Println("B quit")
return
}
}
}
/*
Output:
1
2
3
Main quit
*/

可是了main函数没办法等待B合适地退出,所以B quit
没办法打印,程序直接退出了。然而,chan是Go里的第一对象,所以可以把chan传入chan中,所以上面的代码可以把quit 定义为chan chan
bool,以此控制两个goroutine的同步

func main() {
g = make(chan int)
quit = make(chan chan bool)
go B()
for i := 0; i < 5; i++ {
g <- i
}
wait := make(chan bool)
quit <- wait
<-wait //这样就可以等待B的退出了
fmt.Println("Main Quit")
}
func B() {
for {
select {
case i := <-g:
fmt.Println(i + 1)
case c := <-quit:
c <- true
fmt.Println("B Quit")
return
}
}
}
/* Output
1
2
3
B Quit
Main Quit
*/

分布式递归调用

在现实生活中,如果你要找美国总统聊天,你会怎么做?第一步打电话给在美国的朋友,然后他们也会发动自己的关系网,再找可能认识美国总统的人,以此类推,直到找到为止。这在Kadmelia分布式系统中也是一样的,如果需要获取目标ID信息,那么就不停地查询,被查询节点就算没有相关信息,也会返回它觉得最近节点,直到找到ID或者等待超时。
好了,这个要用Go来实现怎么做呢?

func recursiveCall(ctx context.Context, id []byte, initialNodes []*node){
seen := map[string]*node{} //已见过的节点记录
request := make(chan *node, 3) //设置请求节点channel
// 输入初始节点
go func() {
for _, n := range initialNodes {
request <- n
}
}()
OUT:
for {
//循环直到找到数据
if data != nil {
return
}
// 在新的请求,超时和上层取消请求中select
select {
case n := <-request:
go func() {
// 发送新的请求
response := s.sendQuery(ctx, n, MethodFindValue, id)
select {
case <-ctx.Done():
case msg :=<-response:
seen[responseToNode(response)] = n //更新已见过的节点信息
// 加载新的节点
for _, rn := range LoadNodeInfoFromByte(msg[PayLoadStart:]) {
mu.Lock()
_, ok := seen[rn.HexID()]
mu.Unlock()
// 见过了,跳过这个节点
if ok {
continue
}
AddNode(rn)
// 将新的节点送入channel
request <- rn
}
}
}
}()
case <-time.After(500 * time.Millisecond):
break OUT // break至外层,否则仅仅是跳至loop外
case <-ctx.Done():
break OUT
}
}
return
}

这时的buffered
channel类似于一个局部queue,对需要的节点进行处理,但这段代码的精妙之处在于,这里的block操作是select的,随时可以取消,而不是要等待或者对queue的长度有认识。

你对这三种channel的用法有什么疑问,欢迎讨论╮(╯▽╰)╭