标签 goroutine 下的文章

TB一周萃选[第5期]

本文是首发于个人微信公众号的文章“TB一周萃选[第5期]”的归档。

img{512x368}

人生十鉴

大喜易失言
大怒易失礼
大惊易失态
大哀易失颜
大乐易失察
大惧易失节
大思易失爱
大醉易失德
大话易失信
大欲易失命

下雪,是北方城市冬天的“常规操作”,是最不需要被单独关注的的事情。但今年冬天的“雪”却成为了这边的热门话题,原因:自从入冬以来一直就没下一场像样儿的雪!

雪的姗姗来迟让病毒细菌异常活跃,医院发热门诊尤其是儿科人满为患,笔者入冬后也是连续感冒了两次。好在2018年元旦后没几天,在三九天到来之前,大家期盼已久的“像样儿的雪”终于落了下来。

img{512x368}
图:小区里的初雪

2018年的这第一场雪注定是一场“瑞雪”,它不仅降低了空气的病毒浓度,提升了空气湿度,帮助人们有效抵御病毒入侵人体,而且让缺雪的北方城市瞬间焕发出那冬天独有的“魅力”。

很多事情看起来很难,但一旦捅破了那层窗户纸之后,也就感觉没那么难了。下雪这事儿似乎也是如此,在被第一场雪打了个“样儿”之后,一场场雪便接踵而至了。在笔者撰写本文的时候,窗外还飞舞着洁白的雪花。

一、一周文章精粹

1. Go“不足够好”文章大集合

就像世界上其他事物一样,编程语言也没有完美的,每一门编程语言都有优点,也有“不够好”的地方。Go诞生以来,虽然赞美之声此起彼伏,但对Go的“批评”之声也从未中断过。因此有人就整理了Go“不足够好”文章大集合,供Go设计者反思,供Gopher学习,以更好地、更深刻地理解Go这门语言。

文章链接:“Go is not good enough”

2. 好的Go代码库应该具备的“特征”

Go是一门简洁的编程语言,入门容易,上手快。但写出好的Go代码还是需要一番功夫的。国外的一名gopher总结了“一个好的Go代码库应该具备的特征”,文章中按照依赖、API、错误处理、并发、调试等几个方面列举了诸如:给库打语义版本标签(semantic versioning tag)、除了标准库之外没有第三方依赖、一旦有非标准库的第三方依赖如何应对、不用使用vendor、使用包依赖管理工具、最小化public functions、接收iterface返回struct、避免创建goroutine、避免在公共API中使用channel等特征,强烈推荐每一个gopher阅读学习。

文章链接:好的Go代码库应该具备的“特征”

3. Apollo 2.0发布

在2018 美国消费电子展CES上,百度发布了其无人车平台Apollo的2.0版本,该版本将平台之前宣布的四大模块全部开放,并支持了简单城市道路的自动驾驶。

文章链接:Apollo 2.0

这是我之前写过的一篇文章Apollo 1.0的入门,可以帮助你了解Apollo。

img{512x368}

4. 2018,微服务将结束疯狂

微服务近两年在容器和k8s的赋能下迅速发展,成为架构师口中的“时尚词汇”,每每涉及系统设计,就会首先问是否要做成微服务。一个新事物的出现和发展,有人唱好,自然就会有人看衰。这不,“2018,微服务将结束疯狂”这篇文章就是给“微服务”泼冷水降温的!文章从微服务架构对开发者、运维人员、devops的影响、需要专家级技能、真实世界系统边界模糊、状态复杂性、通信复杂性、版本管理、分布式事务等方面探讨了微服务的劣势,并给出了一个问题列表,建议大家在决定采用微服务之前,用这些问题问问自己,以避免陷入“微服务”泥潭中去。

文章链接:《The Death of microservice madness in 2018》

5. 2017 Google Brain Team的总结 by Jeff Dean

在人工智能的工程领域,Google大神Jeff Dean领导的Brain Team具有举足轻重的地位,也可以说是世界上最好的人工智能实践和研究团队之一了。在2018年伊始,Jeff Dean代表Google Brain Team撰文对团队在2017年的工作及成果进行了总结:包括AutoML、语言理解、机器学习算法、机器学习系统等核心研究工作,以及开源软件Tensorflow、数据集和新的机器学习硬件TPU等方面的最新进展。 对非人工智能领域而言,文章中满满的都是“黑科技”啊,能真正看懂文章中这些内容的朋友你一定也是人工智能领域的大牛了。

img{512x368}
图:Tensorflow用户分布

文章链接:
“2017 Google Brain Team的总结- part1″
“2017 Google Brain Team的总结- part2″
Part1 中文版

6. Javascript工作原理

Javascript诞生之后,估计没人想到过js能像今天这么流行:统治了前端,渗透到了后端,并成为后端服务开发的重要技术栈之一。Js语言也十分简单,但外延也很大,你要至少要深入理解浏览器原理才能更好地发挥JS的威力。sessionstack公司官方blog曾发表了几篇有关Javascript工作原理的文章,可以系统地帮助Javascript了解Js的运行机制。

img{512x368}

文章链接:
Javascript工作原理: 引擎、运行时与调用栈
Javascript工作原理: V8引擎和优化技巧
Javascript工作原理: 内存管理与避免内存泄露的技巧
Javascript工作原理: event loop与async编程

二、一周资料分享

1. 斯坦福大学面向Tensorflow深度学习研究课程

欧美一流大学在计算机技术方面的“与时俱进”的能力与速度真的是十分值得我们学习和借鉴的,尤其是斯坦福这样靠近硅谷的大学,其技术课程更新的速度非常快。Tensorflow于2015年末开源,2017年2月正式发布1.0版本。斯坦福大学在2017年就开了一门新课:“CS 20SI: Tensorflow for Deep Learning Research”,教授学生如何使用Tensorflow进行深度学习研究。

这门课程涵盖了用于深入学习研究的Tensorflow基本原理和使用用法。 目标是帮助学生们理解TensorFlow的graphical computational model,探索它提供的各种功能,并学习如何构建最适合深度学习项目的模型。 课程中学生将使用TensorFlow建立不同复杂度的模型,从简单的线性/逻辑回归到卷积神经网络和递归神经网络,解决词嵌入,单词翻译,光学字符识别,强化学习等任务。 学生还将学习到构建模型和研究实验管理的最佳实践。

资料链接:
CS 20SI: Tensorflow for Deep Learning Research 课程主页
CS 20SI: Tensorflow for Deep Learning Research 2017课程归档

三、一周工具推荐

1. stackedit

自从我的博客转到使用Markdown格式进行编辑后,我就一直使用stackedit提供的在线所见即所得的Markdown编辑器进行内容的编辑。最初的stackedit v4表现的还不强大,随着stackedit v5在线版本的推出,stackedit已经可以满足绝大多数Markdown编辑的功能需求了。

img{512x368}

  • 支持在线/离线管理多个markdown文件
  • 支持多种文件格式导出,包括HTML、PDF、WORD、EPUB
  • 支持文件的云同步,支持Google Drive, Dropbox等主流云存储系统
  • 支持将Markdown直接上传到Blogger/Blogspot, WordPress, Zendesk
  • 支持将Markdown直接发布到GitHub, Gist, Google Drive, Dropbox
    … …

更难得的是stackedit还是一个受关注度极高的开源项目(stars over 1w),你可以自己本地部署一个专用的stackedit。

工具链接:stackedit.io
工具开源项目链接:stackedit

四、一周图书推荐

1.《演讲模式:演讲的技巧与禁忌》

img{512x368}

技术人升级到一定level后,可能少不了要做些演讲、做些培训之类的活动。但对多数技术人而言,演讲这事并不是“舒适区”范围内。市面上有关介绍演讲技巧方面的书可谓是“汗牛充栋”,但这Neal Ford领衔编写的这本书《演讲模式》却独具特色。Neal Ford是Thoughtworks的大神,其他两位作者也是IT圈中的牛人。与其他作者相比,他们更熟悉IT技术人的思维方式,他们也以IT人独特的思维方式,创造性地将建筑和软件开发领域“模式”的概念引入演讲领域,围绕演讲的全过程总结了能迅速有效提升演讲技能的88个模式(应该掌握的技巧)和反模式(应该避免的不好的做法)。对于IT人而言,“模式”这词再熟悉不过了,因此这本书更像是IT技术人员间地手把手地传道解惑。

图书链接:
中文版:《演讲模式:演讲的技巧与禁忌》
英文版:《Presentation Patterns: Techniques for Crafting Better Presentations》


我的联系方式:

微博:http://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

Goroutine调度实例简要分析

前两天一位网友在微博私信我这样一个问题:

抱歉打扰您咨询您一个关于Go的问题:对于goroutine的概念我是明了的,但很疑惑goroutine的调度问题, 根据《Go语言编程》一书:“当一个任务正在执行时,外部没有办法终止它。要进行任务切换,只能通过由该任务自身调用yield()来主动出让CPU使用权。” 那么,假设我的goroutine是一个死循环的话,是否其它goroutine就没有执行的机会呢?我测试的结果是这些goroutine会轮流执行。那么除了syscall时会主动出让cpu时间外,我的死循环goroutine 之间是怎么做到切换的呢?

我在第一时间做了回复。不过由于并不了解具体的细节,我在答复中做了一个假定,即假定这位网友的死循环带中没有调用任何可以交出执行权的代码。事后,这位网友在他的回复后道出了死循环goroutine切换的真实原因:他在死循环中调用了fmt.Println

事后总觉得应该针对这个问题写点什么? 于是就构思了这样一篇文章,旨在循着这位网友的思路通过一些例子来step by step演示如何分析go schedule。如果您对Goroutine的调度完全不了解,那么请先读一读这篇前导文 《也谈goroutine调度器》

一、为何在deadloop的参与下,多个goroutine依旧会轮流执行

我们先来看case1,我们顺着那位网友的思路来构造第一个例子,并回答:“为何在deadloop的参与下,多个goroutine依旧会轮流执行?”这个问题。下面是case1的源码:

//github.com/bigwhite/experiments/go-sched-examples/case1.go
package main

import (
    "fmt"
    "time"
)

func deadloop() {
    for {
    }
}

func main() {
    go deadloop()
    for {
        time.Sleep(time.Second * 1)
        fmt.Println("I got scheduled!")
    }
}

在case1.go中,我们启动了两个goroutine,一个是main goroutine,一个是deadloop goroutine。deadloop goroutine顾名思义,其逻辑是一个死循环;而main goroutine为了展示方便,也用了一个“死循环”,并每隔一秒钟打印一条信息。在我的macbook air上运行这个例子(我的机器是两核四线程的,runtime的NumCPU函数返回4):

$go run case1.go
I got scheduled!
I got scheduled!
I got scheduled!
... ...

从运行结果输出的日志来看,尽管有deadloop goroutine的存在,main goroutine仍然得到了调度。其根本原因在于机器是多核多线程的(硬件线程哦,不是操作系统线程)。Go从1.5版本之后将默认的P的数量改为 = CPU core的数量(实际上还乘以了每个core上硬线程数量),这样case1在启动时创建了不止一个P,我们用一幅图来解释一下:

img{512x368}

我们假设deadloop Goroutine被调度与P1上,P1在M1(对应一个os kernel thread)上运行;而main goroutine被调度到P2上,P2在M2上运行,M2对应另外一个os kernel thread,而os kernel threads在操作系统调度层面被调度到物理的CPU core上运行,而我们有多个core,即便deadloop占满一个core,我们还可以在另外一个cpu core上运行P2上的main goroutine,这也是main goroutine得到调度的原因。

Tips: 在mac os上查看你的硬件cpu core数量和硬件线程总数量:

$sysctl -n machdep.cpu.core_count
2
$sysctl -n machdep.cpu.thread_count
4

二、如何让deadloop goroutine以外的goroutine无法得到调度?

如果我们非要deadloop goroutine以外的goroutine无法得到调度,我们该如何做呢?一种思路:让Go runtime不要启动那么多P,让所有用户级的goroutines在一个P上被调度。

三种办法:

  • 在main函数的最开头处调用runtime.GOMAXPROCS(1);
  • 设置环境变量export GOMAXPROCS=1后再运行程序
  • 找一个单核单线程的机器^0^(现在这样的机器太难找了,只能使用云服务器实现)

我们以第一种方法为例:

//github.com/bigwhite/experiments/go-sched-examples/case2.go
package main

import (
    "fmt"
    "runtime"
    "time"
)

func deadloop() {
    for {
    }
}

func main() {
    runtime.GOMAXPROCS(1)
    go deadloop()
    for {
        time.Sleep(time.Second * 1)
        fmt.Println("I got scheduled!")
    }
}

运行这个程序后,你会发现main goroutine的”I got scheduled”字样再也无法输出了。这里的调度原理可以用下面图示说明:

img{512x368}

deadloop goroutine在P1上被调度,由于deadloop内部逻辑没有给调度器任何抢占的机会,比如:进入runtime.morestack_noctxt。于是即便是sysmon这样的监控goroutine,也仅仅是能给deadloop goroutine的抢占标志位设为true而已。由于deadloop内部没有任何进入调度器代码的机会,Goroutine重新调度始终无法发生。main goroutine只能躺在P1的local queue中徘徊着。

三、反转:如何在GOMAXPROCS=1的情况下,让main goroutine得到调度呢?

我们做个反转:如何在GOMAXPROCS=1的情况下,让main goroutine得到调度呢?听说在Go中 “有函数调用,就有了进入调度器代码的机会”,我们来试验一下是否属实。我们在deadloop goroutine的for-loop逻辑中加上一个函数调用:

// github.com/bigwhite/experiments/go-sched-examples/case3.go
package main

import (
    "fmt"
    "runtime"
    "time"
)

func add(a, b int) int {
    return a + b
}

func deadloop() {
    for {
        add(3, 5)
    }
}

func main() {
    runtime.GOMAXPROCS(1)
    go deadloop()
    for {
        time.Sleep(time.Second * 1)
        fmt.Println("I got scheduled!")
    }
}

我们在deadloop goroutine的for loop中加入了一个add函数调用。我们来运行一下这个程序,看是否能达成我们的目的:

$ go run case3.go

“I got scheduled!”字样依旧没有出现在我们眼前!也就是说main goroutine没有得到调度!为什么呢?其实所谓的“有函数调用,就有了进入调度器代码的机会”,实际上是go compiler在函数的入口处插入了一个runtime的函数调用:runtime.morestack_noctxt。这个函数会检查是否扩容连续栈,并进入抢占调度的逻辑中。一旦所在goroutine被置为可被抢占的,那么抢占调度代码就会剥夺该Goroutine的执行权,将其让给其他goroutine。但是上面代码为什么没有实现这一点呢?我们需要在汇编层次看看go compiler生成的代码是什么样子的。

查看Go程序的汇编代码有许多种方法:

  • 使用objdump工具:objdump -S go-binary
  • 使用gdb disassemble
  • 构建go程序同时生成汇编代码文件:go build -gcflags ‘-S’ xx.go > xx.s 2>&1
  • 将Go代码编译成汇编代码:go tool compile -S xx.go > xx.s
  • 使用go tool工具反编译Go程序:go tool objdump -S go-binary > xx.s

我们这里使用最后一种方法:利用go tool objdump反编译(并结合其他输出的汇编形式):

$go build -o case3 case3.go
$go tool objdump -S case3 > case3.s

打开case3.s,搜索main.add,我们居然找不到这个函数的汇编代码,而main.deadloop的定义如下:

TEXT main.deadloop(SB) github.com/bigwhite/experiments/go-sched-examples/case3.go
        for {
  0x1093a10             ebfe                    JMP main.deadloop(SB)

  0x1093a12             cc                      INT $0x3
  0x1093a13             cc                      INT $0x3
  0x1093a14             cc                      INT $0x3
  0x1093a15             cc                      INT $0x3
   ... ...
  0x1093a1f             cc                      INT $0x3

我们看到deadloop中对add的调用也消失了。这显然是go compiler执行生成代码优化的结果,因为add的调用对deadloop的行为结果没有任何影响。我们关闭优化再来试试:

$go build -gcflags '-N -l' -o case3-unoptimized case3.go
$go tool objdump -S case3-unoptimized > case3-unoptimized.s

打开 case3-unoptimized.s查找main.add,这回我们找到了它:

TEXT main.add(SB) github.com/bigwhite/experiments/go-sched-examples/case3.go
func add(a, b int) int {
  0x1093a10             48c744241800000000      MOVQ $0x0, 0x18(SP)
        return a + b
  0x1093a19             488b442408              MOVQ 0x8(SP), AX
  0x1093a1e             4803442410              ADDQ 0x10(SP), AX
  0x1093a23             4889442418              MOVQ AX, 0x18(SP)
  0x1093a28             c3                      RET

  0x1093a29             cc                      INT $0x3
... ...
  0x1093a2f             cc                      INT $0x3

deadloop中也有了对add的显式调用:

TEXT main.deadloop(SB) github.com/bigwhite/experiments/go-sched-examples/case3.go
  ... ...
  0x1093a51             48c7042403000000        MOVQ $0x3, 0(SP)
  0x1093a59             48c744240805000000      MOVQ $0x5, 0x8(SP)
  0x1093a62             e8a9ffffff              CALL main.add(SB)
        for {
  0x1093a67             eb00                    JMP 0x1093a69
  0x1093a69             ebe4                    JMP 0x1093a4f
... ...

不过我们这个程序中的main goroutine依旧得不到调度,因为在main.add代码中,我们没有发现morestack函数的踪迹,也就是说即便调用了add函数,deadloop也没有机会进入到runtime的调度逻辑中去。

不过,为什么Go compiler没有在main.add函数中插入morestack的调用呢?那是因为add函数位于调用树的leaf(叶子)位置,compiler可以确保其不再有新栈帧生成,不会导致栈分裂或超出现有栈边界,于是就不再插入morestack。而位于morestack中的调度器的抢占式检查也就无法得以执行。下面是go build -gcflags ‘-S’方式输出的case3.go的汇编输出:

"".add STEXT nosplit size=19 args=0x18 locals=0x0
     TEXT    "".add(SB), NOSPLIT, $0-24
     FUNCDATA        $0, gclocals·54241e171da8af6ae173d69da0236748(SB)
     FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
     MOVQ    "".b+16(SP), AX
     MOVQ    "".a+8(SP), CX
     ADDQ    CX, AX
     MOVQ    AX, "".~r2+24(SP)
    RET

我们看到nosplit字样,这就说明add使用的栈是固定大小,不会再split,且size为24字节。

关于在for loop中的leaf function是否应该插入morestack目前还有一定争议,将来也许会对这样的情况做特殊处理。

既然明白了原理,我们就在deadloop和add之间加入一个dummy函数,见下面case4.go代码:

//github.com/bigwhite/experiments/go-sched-examples/case4.go
package main

import (
    "fmt"
    "runtime"
    "time"
)

//go:noinline
func add(a, b int) int {
    return a + b
}

func dummy() {
    add(3, 5)
}

func deadloop() {
    for {
        dummy()
    }
}

func main() {
    runtime.GOMAXPROCS(1)
    go deadloop()
    for {
        time.Sleep(time.Second * 1)
        fmt.Println("I got scheduled!")
    }
}

执行该代码:

$go run case4.go
I got scheduled!
I got scheduled!
I got scheduled!

Wow! main goroutine果然得到了调度。我们再来看看go compiler为程序生成的汇编代码:

$go build -gcflags '-N -l' -o case4 case4.go
$go tool objdump -S case4 > case4.s

TEXT main.add(SB) github.com/bigwhite/experiments/go-sched-examples/case4.go
func add(a, b int) int {
  0x1093a10             48c744241800000000      MOVQ $0x0, 0x18(SP)
        return a + b
  0x1093a19             488b442408              MOVQ 0x8(SP), AX
  0x1093a1e             4803442410              ADDQ 0x10(SP), AX
  0x1093a23             4889442418              MOVQ AX, 0x18(SP)
  0x1093a28             c3                      RET

  0x1093a29             cc                      INT $0x3
  0x1093a2a             cc                      INT $0x3
... ...

TEXT main.dummy(SB) github.com/bigwhite/experiments/go-sched-examples/case4.s
func dummy() {
  0x1093a30             65488b0c25a0080000      MOVQ GS:0x8a0, CX
  0x1093a39             483b6110                CMPQ 0x10(CX), SP
  0x1093a3d             762e                    JBE 0x1093a6d
  0x1093a3f             4883ec20                SUBQ $0x20, SP
  0x1093a43             48896c2418              MOVQ BP, 0x18(SP)
  0x1093a48             488d6c2418              LEAQ 0x18(SP), BP
        add(3, 5)
  0x1093a4d             48c7042403000000        MOVQ $0x3, 0(SP)
  0x1093a55             48c744240805000000      MOVQ $0x5, 0x8(SP)
  0x1093a5e             e8adffffff              CALL main.add(SB)
}
  0x1093a63             488b6c2418              MOVQ 0x18(SP), BP
  0x1093a68             4883c420                ADDQ $0x20, SP
  0x1093a6c             c3                      RET

  0x1093a6d             e86eacfbff              CALL runtime.morestack_noctxt(SB)
  0x1093a72             ebbc                    JMP main.dummy(SB)

  0x1093a74             cc                      INT $0x3
  0x1093a75             cc                      INT $0x3
  0x1093a76             cc                      INT $0x3
.... ....

我们看到main.add函数依旧是leaf,没有morestack插入;但在新增的dummy函数中我们看到了CALL runtime.morestack_noctxt(SB)的身影。

四、为何runtime.morestack_noctxt(SB)放到了RET后面?

在传统印象中,morestack是放在函数入口处的。但实际编译出来的汇编代码中(见上面函数dummy的汇编),runtime.morestack_noctxt(SB)却放在了RET的后面。解释这个问题,我们最好来看一下另外一种形式的汇编输出(go build -gcflags ‘-S’方式输出的格式):

"".dummy STEXT size=68 args=0x0 locals=0x20
        0x0000 00000 TEXT    "".dummy(SB), $32-0
        0x0000 00000 MOVQ    (TLS), CX
        0x0009 00009 CMPQ    SP, 16(CX)
        0x000d 00013 JLS     61
        0x000f 00015 SUBQ    $32, SP
        0x0013 00019 MOVQ    BP, 24(SP)
        0x0018 00024 LEAQ    24(SP), BP
        ... ...
        0x001d 00029 MOVQ    $3, (SP)
        0x0025 00037 MOVQ    $5, 8(SP)
        0x002e 00046 PCDATA  $0, $0
        0x002e 00046 CALL    "".add(SB)
        0x0033 00051 MOVQ    24(SP), BP
        0x0038 00056 ADDQ    $32, SP
        0x003c 00060 RET
        0x003d 00061 NOP
        0x003d 00061 PCDATA  $0, $-1
        0x003d 00061 CALL    runtime.morestack_noctxt(SB)
        0x0042 00066 JMP     0

我们看到在函数入口处,compiler插入三行汇编:

        0x0000 00000 MOVQ    (TLS), CX  // 将TLS的值(GS:0x8a0)放入CX寄存器
        0x0009 00009 CMPQ    SP, 16(CX)  //比较SP与CX+16的值
        0x000d 00013 JLS     61 // 如果SP > CX + 16,则jump到61这个位置

这种形式输出的是标准Plan9的汇编语法,资料很少(比如JLS跳转指令的含义),注释也是大致猜测的。如果跳转,则进入到 runtime.morestack_noctxt,从 runtime.morestack_noctxt返回后,再次jmp到开头执行。

为什么要这么做呢?按照go team的说法,是为了更好的利用现代CPU的“static branch prediction”,提升执行性能。

五、参考资料

文中的代码可以点击这里下载。


微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

微信赞赏:
img{512x368}

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言进阶课 Go语言精进之路1 Go语言精进之路2 Go语言第一课 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats