goroutines + channel
channel
是golang里面一个比较有意思的东西,可以把它看成是一个semaphone(无缓存版队列)或者FIFO(有缓存版队列)。这篇文章只是把最
近用到的一些东西归纳了一下,就算是给自己留份存档吧。
channel
是需要和goroutines
一起使用的。
goroutines
先说goroutines
吧。golang的并行模型使用的是CSP(官方的说法是Newsqueak-Alef-Limbo,和原始的CSP有一些区别
。具体的差别还没有细查。参见此张幻灯片 ),go routines
是一个独立运行的函数,不是进程也不是线程。它在被调用或者说被启动后直接返回,而不用等待函数运行结束。这是官方对它的一段[简要介绍](htt
p://talks.golang.org/2012/concurrency.slide#17)
- What is a goroutine? It’s an independently executing function, launched by a go statement.
- It has its own call stack, which grows and shrinks as required.
- It’s very cheap. It’s practical to have thousands, even hundreds of thousands of goroutines.
- It’s not a thread.
- There might be only one thread in a program with thousands of goroutines.
- Instead, goroutines are multiplexed dynamically onto threads as needed to keep all the goroutines running.
- But if you think of it as a very cheap thread, you won’t be far off.
goroutines
使用go
关键字来创建,创建完后就自己蹲一边运行去了,主程序也不用理它,自己往下走就行。光看文字太枯燥,读程序看输出吧。
package main
import "fmt"
import "time"
func main() {
fmt.Println("START 1")
for i := 0; i < 3; i++ {
foo(i)
}
fmt.Println("END 1")
fmt.Println("START 2")
for i := 0; i < 3; i++ {
go foo(i)
}
fmt.Println("END 2")
time.Sleep(1 * time.Second)
}
func foo(i int) {
time.Sleep(1)
fmt.Printf("Call index: %d\n", i)
}
程序的输出是这样的:
START 1
Call index: 0
Call index: 1
Call index: 2
END 1
START 2
END 2
Call index: 0
Call index: 2
Call index: 1
代码中,main函数中用了两个循环,分别调用了3次foo()函数,但是两次调用的方式不同,第一次是直接调用,第二次是使用了goroutine
。从输出看,
前段输出是正常的顺序,但是从后一段输出可以看到,函数调用是立即返回的,而且最后输出的顺序也是不固定的(3次可能看到的效果比较一致,如果把循环次数涨到10次就
能看的比较明显了)。在第19行加上sleep的原因是因为main函数在执行完成后会直接退出,不会等待所有goroutines
执行完毕,所以此处需要让程序
等一等。不方便在本机运行的可以见这里
channel
拿sleep来不让main函数退出也不是个事,毕竟时间不好控制,睡多了睡少了都不行,所以这里就引入了channel
。
就像文章开头说的,channel
就像是一个Semaphore或者FIFO的东西,更通俗的理解就是一条传送带或者管道,用于在goroutines
之间传
递消息。把上面的程序改一下(顺序调用的那个就先去掉了,减少长度,嗯):
package main
import "fmt"
func main() {
foo_channel := make(chan string)
channel_cnt := 3
fmt.Println("Start launching goroutines")
for i := 0; i < channel_cnt; i++ {
go foo(i, foo_channel)
}
fmt.Println("Finish launching goroutines")
for i := 0; i < channel_cnt; i++ {
fmt.Println(<-foo_channel)
}
}
func foo(i int, foo_channel chan string) {
s := fmt.Sprintf("Call index %d", i)
foo_channel <- s
}
输入是介样的:
Start launching goroutines
Finish launching goroutines
Call index 0
Call index 1
Call index 2
在这个程序里面,没有使用sleep
,程序也做到了等待所有goroutines
运行完毕才退出这个需求,就是靠的main函数最后几行。blog上不知道为
啥没有行号了,程序见这里吧。在第7行的时候创建了一个channel
,并把它
传入了foo()
函数中。而foo()
函数也不像上一次那样直接输出一个字符串,它把需要输出的字符串放到一个string中,然后通过channel
传
了回来。这条语句:
foo_channel <- s
做的就是把字符串放到channel中去,而这条语句:
fmt.Println(<-foo_channel)
做的就是把字符串从channel中取出来,并把它输出。在执行<-channel
时,如果channel
上没有数据,这条语句是被blocked的,这样就
能保证main函数在所有goroutines执行完毕前不会退出。而且当channel中有值而且没有被读出的时候,对这个channel的写操作也是被block
ed的。这时的channel,可以用来做Semaphore。
上面介绍的channel是没有缓存的,golang中还有一种channel是可以加上缓存的,[官方文档说这个就像是Erlang的mailboxes](htt
p://talks.golang.org/2012/concurrency.slide#22A),因为不懂Erlang,所以只好把话贴在这以后两手一摊。目前
在项目中还没有用到带缓存的channel,暂时先不写了,估计在不远的将来还会有一篇关于select
的文章,目前时间未定,因为还没用到~