蓝天,小湖,湖水中一方小筑

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的文章,目前时间未定,因为还没用到~