31

Golang 介绍及踩坑系列之四

 5 years ago
source link: https://studygolang.com/articles/14145?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

这篇坑文来自最近的一件趣事。

我认识一位非常精通golang编程技巧的工程师。他/她经验丰富,擅长各种解决工程问题的技法,对系统了解也极为深入。遇到golang的实战问题,他/她往往可以一语中的,谈笑间bug灰飞烟灭。

这么一位值得尊敬的工程师,在别人问他golang的goroutine是个啥玩意的时候,他/她瞠目结舌,不知道该怎么跟对方解释好,居然说:“goroutine就有点像java的线程池啦。”excuse me!这也太狗屁不通了吧!

所以我觉得,我来装出一副我比他/她更懂的姿态,给大家科普一下什么是goroutine。对goroutine了如指掌的同学请绕行。

那到底啥是goroutine捏?

要了解啥是goroutine,我们得先了解啥是coroutine。(不了解coroutine的同学请举起腿来!---郭德纲)

coroutine也就是协程。

要了解什么是协程,我们先得了解他的特殊形式:例程。

一个不built in支持协程的语言,写出来的函数我们叫subroutine,或者叫例程。

subroutine A 调用 subroutine B 意味着在A的stack里边开创一片空间来作为B的stack。B里边可以继续调用C,C里边可以继续调用D... 要注意的是,所有后面被调用的家伙都会共享A的线程开辟的stack空间。如果我们写了一个调用嵌套特别复杂的函数,我们很有可能看见StackOverFlow! 当然如果我们写一个A调用B,B里边再调用A这样的子子孙孙无穷尽的函数调用,我们更容易碰到StackOverFlow!

例程基本讲完了。c/c++/java 不加上一些特殊库的支持的话,我们写的函数调用方式都是把对方当做例程来的。

而例程是协程的特殊形式!(重要的话要大黑粗)

我们可以很容易推断出来,在一个线程里边,调用者例程一定是要等到被调用的例程执行完成并且返回才能继续执行。比如:

public static void funcionA(){
  int resultFromB = functionB();
  System.out.println("B returned : " + resultFromB);
}

而被调用的例程里边如果调用了调用者例程的话,也是重新开一个function stack来执行的。比如上面的栗子:如果functionB里边调用了functionA(好吧,我知道这么写的人是大sb),那么另一个functionA的stack会被创建,然后执行。

但是coroutine呢?

var q := new queue

coroutine produce
    loop
        while q is not full
            create some new items
            add the items to q
        yield to consume

coroutine consume
    loop
        while q is not empty
            remove some items from q
            use the items
        yield to produce

coroutine produce和consume可以使用 yield 关键字把执行权推来推去。我们在这个例子里边可以直白的把 yield 理解为:我先歇歇。

produce向q里边丢了东西,然后表示它要歇歇,让consume干会儿活。

consume用了q里边的东西,然后表示它要歇歇,让produce干会儿活。

produce和consume不是互为subroutine,互相的stack也是独立的。

假如produce不使用yield关键字,直接调用consume,那就变成了subroutine的调用了。

所以我们说,subroutine是coroutine的特殊形式。

我们来看看goroutine:

func main(){
ch:=make(chan int)
go routineA(ch)
go routineB(ch)
println("goroutines scheduled!")
<-ch
<-ch
}
func routineA(ch chan int){
 println("A executing!")
 ch<-1
}
func routineB(ch chan int){
 println("B executing!")
 ch<-2
}

go这个关键字非常有用!他的意思是:滚!

routineA 滚开,然后执行!

routineB 滚开,然后执行!

我们开到,main函数这个goroutine里边打开了两个新的goroutine,并且要求他们滚开去找个时间执行自己。我们可以断言:"goroutines scheduled!"这行字将会先被输出到console。而”A/B executing!“则会晚一些才输出。

那么问题来了,A和B啥时候才能得到执行机会呢?

答案:当正在执行的goroutine遇到系统IO(timer,channel read/write,file read/write...)的时候,go scheduler会切换出去看看是不是有别的goroutine可以执行一把,这个时候A和B就有机会了。实际上,这就是golang协程的概念。同时用少数的几个线程来执行大量的goroutine协程,谁正在调用系统IO谁就歇着,让别人用CPU。

所以如果我们用pprof看你的服务,可能发现有几千条goroutine,但是真正运行的线程只有小猫两三只。

引申问题:假如我写个不做任何系统IO的函数会怎么样?

func noIO(){
go routineA()
go routineB()
for {
 println("i will never stop!")
}
}

go scheduler 专门对此作了处理。如果是早期的go版本,你将会看到大量的"i will never stop!",并且发现routineA和B没啥执行机会。现在go1.9会怎么样,各位童鞋不放举起腿来自己试试看。

所以综上所述:golang里边使用go 这个非常关键的关键字,来触发协程调度。

相比python等语言对协程的支持,golang的支持是非常傻瓜友好的。比如python的

yield
await
run_until_complete

分分钟可以弄晕你。

希望这篇文章能对你有点小用处。向小白介绍goroutine的时候,我觉得可以这样:

goroutine有点像是light weight的线程。一个真正的线程可以调度很多goroutine,不同的goroutine可以被挂载在不同线程里边去执行。这些都是自动的,对程序员很友好。

题外话,我们可以设置系统里边只有一条线程,所有的goroutine都在这一条线程上面跑。那么我们可以省掉一个很恶心的东西:

对的,是sync.RWMutex.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK