Goroutine和Channel详解
Go语言中并发程序可以用两种方式来实现。一种是goroutine和channel,其支持“顺序进程通信”(communicating sequential processes)或被简称为CSP。CSP是一个现代的并发编程模型,在这种编程模型中值会在不同的运行实例(goroutine)中传递,尽管大多数情况下被限制在单一实例中。另一种是传统的并发模型,多线程共享内存(基于共享变量的并发),会在后续单独阐述。
goroutine
goroutine是一个轻量级的执行线程(又称协程),它与线程的区别是线程是操作系统中对于一个独立运行实例的描述,不同的操作系统中,线程的实现也不尽相同;对于goroutine,操作系统并不知道它的存在,goroutine的调度是Go语言的运行时进行管理的。启动线程虽然比进程使用的资源少,但依然需要上下文切换等大量工作,Go语言有自己的调度器,许多goroutine的数据都是共享的,因此goroutine之间的切换会快很多,启动goroutine所耗费的资源也很少。
package main import ( "fmt" "strconv" ) func Info(name string) { for i := 0; i < 3; i++ { fmt.Println(name + ":" + strconv.Itoa(i)) } } func main() { Info("info") go Info("goroutine1") go func(name string) { fmt.Println(name) }("goroutine2") var input string fmt.Scanln(&input) fmt.Println("done") } 复制代码
channel
channel被称为通道,是连接并发goroutine的管道,可以从一个goroutine向通道发送值,并在另一个goroutine中接收到这些值。每个channel都有一个特殊的类型,也就是channel可发送数据的类型。一个可以发送int类型数据的channel一般写为chan int。
一个channel有发送和接收两个主要操作,都是通信行为。一个发送语句将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine。发送和接收两个操作都是用 <-
运算符。在发送语句中, <-
运算符分割channel和要发送的值;在接收语句中, <-
运算符写在channel对象之前,一个不使用接收结果的接收操作也是合法的。
channel的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的channel上执行接收操作,当发送的值通过channel成功传输之后,两个goroutine可以继续执行后面的语句。反之,如果接收操作先发生,那么接收者goroutine也将阻塞,直到有另一个goroutine在相同的Channels上执行发送操作。
package main import ( "fmt" "strconv" ) func Info(i int, ch chan string) { msg := "数据" + strconv.Itoa(i) ch <- msg } func main() { chs := make([]chan string, 3) for i := 0; i < 3; i++ { chs[i] = make(chan string) go Info(i, chs[i]) } for num, ch := range chs { msg := <- ch fmt.Println(num, msg) } fmt.Println("Done") } 复制代码
channel还支持close操作,用于关闭channel,随后对基于该channel的任何发送操作都将导致Panic异常。对一个已经被close过的channel接收操作依然可以接受到之前已经成功发送的数据;如果channel中已经没有数据的话将产生一个零值(nil)的数据。
//使用内置的close函数就可以关闭一个channel: close(ch) 复制代码
带缓存的channel
带缓存的channel内部有一个元素队列。队列的最大容量是在调用make函数创建channel时通过第二个参数指定的。下面的语句创建了一个可以持有三个字符串元素的带缓存channel。
ch = make(chan string, 3) 复制代码
向缓存channel的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部删除元素。如果内部缓存队列是满的,那么发送操作将阻塞直到另一个goroutine执行接收操作而释放了新的队列空间。相反,如果channel是空的,接收操作将阻塞直到有另一个goroutine执行发送操作而向队列插入元素。
package main import ( "fmt" ) func main() { ch := make(chan string, 3) ch <- "A" ch <- "B" ch <- "C" fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) } 复制代码
select
Go语言的select的功能和select、poll、epoll相似,就是监听IO操作,当IO操作发生时,触发响应的动作。select语句的用法和switch相似,也会有几个case和default分支,每一个case代表一个通信操作(在某个channel上进行发送或者接收)并且会包含一些语句组成一个语句块。
package main import ( "fmt" "strconv" ) func main() { ch1 := make(chan string) ch2 := make(chan string) go func() { ch1 <- "I am from ch1" }() go func() { ch2 <- "I am from ch2" }() for i := 0; i < 2; i++ { select { case msg1 := <- ch1: fmt.Println("received-"+strconv.Itoa(i), msg1) case msg2 := <- ch2: fmt.Println("received-"+strconv.Itoa(i), msg2) } } } 复制代码
超时控制
select是用来让我们的程序监听多个文件句柄的状态变化的处理机制。当发起一些阻塞的请求后,可以用select机制轮询扫描文件句柄,直到被监视的文件句柄有一个或多个发生了状态改变。channel在系统层面来说也是个文件描述符,在Go语言中我们可以用goroutine并发执行任务,接着使用select来监视每个任务的channel情况。如果这几个任务都长时间没有回复channel信息,并且我们又有超时的需求,那么我们可以使用一个goroutine来设置超时机制,具体做法就是启动sleep并且在sleep之后回复channel信号。
package main import ( "fmt" "time" ) func main() { ch := make(chan string) timeout := make(chan bool) go func() { time.Sleep(5 * time.Second) timeout <- true }() go func() { time.Sleep(10 * time.Second) ch <- "Hello World" }() select { case msg := <- ch: fmt.Println(msg) case <- timeout: fmt.Println("task is timeout") } } 复制代码
欢迎关注我们的微信公众号,每天学习Go知识