Go从语言本身支持并发,而不是由某个库或者模块来实现并发,可谓天生丽质。goroutine从根本上与线程不同,goroutine更加轻量化。
看下面这个常见的网络模型:
package main
import (
"fmt"
"net"
)
func manageClient(conn net.Conn) {
conn.Write([]byte("Hi!"))
conn.Close()
//do something with the client
}
func main() {
//we are creating a server her that listenson port 1337.
listener, err := net.Listen("tcp", ":1337")
for {
//accept a connection
connection, _ := listener.Accept()
go manageClient(connection)
}
}
在main函数调用net.Listen方法进行监听,该方法会返回两个值,一个是服务器连接,另一个是错误数据。然后,进入到服务的主循环部分,程序调用server.Accept方法,然后等待请求。该方法调用后,程序会被挂起,直到有有一个客户端的连接出现。一旦有个连接出现,我们将connection对象传值到manageClient方法中,由于通过goroutine的方式调用manageClient,所以主程序会继续等待处理下一个客户端连接请求。
上面的代码清晰明了,Go允许使用go语句开启一个新的运行期线程,即 goroutine,以一个不同的、新创建的goroutine来执行一个函数。同一个程序中的所有goroutine共享同一个地址空间。
Goroutine非常轻量,除了为之分配的栈空间,其所占用的内存空间微乎其微。并且其栈空间在开始时非常小,之后随着堆存储空间的按需分配或释放而变化。内部实现上,goroutine会在多个操作系统线程上多路复用。如果一个goroutine阻塞了一个操作系统线程,例如:等待输入,这个线程上的其他goroutine就会迁移到其他线程,这样能继续运行。
让我们接着来看下面这个例子:
package main
import (
"fmt"
)
func sayHello() {
fmt.Println("Hello, world!")
}
func main() {
//run a goroutine that says hello
go sayHello()
}
上述程序会输出什么?什么也不会输出,因为sayHello这个goroutine还没来得及跑,主函数已经退出了。
在C++/Java/Python里面,都有类似Join的东东来等待子线程,而go里面是用Channels来实现的,Channels是一种goroutine之间或者goroutine和主进程之间的通信机制。
把上述程序改为:
package main
import (
"fmt"
)
var eventChannel chan int = make(chan int)
func sayHello() {
fmt.Println("Hello, world!")
//pass a message through the eventChannel
eventChannel <- 1
}
func main() {
//run a goroutine that says hello
go sayHello()
//read the eventChannel
//this call blocks so it waits until sayHello() is done
<- eventChannel
}
默认情况下,信道的存数据和取数据都是阻塞的 (无缓冲Channel)。如果channel中没有数据的情况下,从channel中读数据会被阻塞,一直阻塞到可以从channel中读到数据。反之,如果无缓冲Channel中数据未被取出,存数据也会阻塞直到数据被取走。
所以,无缓冲channel是在多个goroutine之间同步很棒的工具。通过这种机制,上面这个程序就可以输出 “Hello, world”了。
这里,操作符<-用于指定管道的方向,发送或接收。如果未指定方向,则为双向管道。
chan Sushi // 可用来发送和接收Sushi类型的值
chan<- float64 // 仅可用来发送float64类型的值
<-chan int // 仅可用来接收int类型的值
管道是引用类型,基于make函数来分配:
ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})
如果通过管道发送一个值,则将<-作为二元操作符使用。通过管道接收一个值,则将其作为一元操作符使用:
ch <- v // 发送v到channel ch.
v := <-ch // 从ch中接收数据,并赋值给v
OK,有无缓冲channel,当然也有缓冲channel的缓冲,其实就是个FIFO,可以把缓冲信道看作为一个线程安全的队列。
ch:= make(chan bool, 4),创建了可以存储4个元素的bool 型channel。在这个channel 中,前4个元素可以无阻塞的写入。当写入第5个元素时,代码将会阻塞,直到其他goroutine从channel 中读取一些元素,腾出空间。
ch := make(chan type, value)
//value == 0 ! 无缓冲(阻塞)
//value > 0 ! 缓冲(非阻塞,直到value 个元素)
看下面这个例子:
package main
import "fmt"
func main() {
c := make(chan int, 2)//修改2为1就报死锁的错误(很好理解),修改2为3可以正常运行
c <- 1
c <- 2
fmt.Println(<-c)
fmt.Println(<-c)
}
如果你需要不断从Channel中取数据,上面的代码一个一个地去读取Channel真真太烦了,range闪亮登场了,Go语言允许我们使用range来读取信道。
看下面这个例子:
package main
import (
"fmt"
)
func fibonacci(n int, c chan int) {
x, y := 1, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x + y
}
close(c) //去掉此句会报死锁错误
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}
for i := range c能够不断的读取channel里面的数据,直到该channel被显式的关闭。上面代码我们看到可以显式的关闭channel,生产者通过内置函数close关闭channel。被关闭的信道会禁止数据流入, 是只读的。我们仍然可以从关闭的信道中取出数据,但是不能再写入数据了。
在消费方可以通过语法v, ok := <-ch测试channel是否被关闭。如果ok返回false,那么说明channel已经没有任何数据并且已经被关闭。
记住应该在生产者的地方关闭channel,而不是消费的地方去关闭它,这样容易引起panic。
如果主线程要等待多个goroutine,怎么同步?看完了上面的Channel你肯定有想法了吧。
有两个方案:
1)只使用单个无缓冲Channel阻塞主线
2)使用容量为goroutines数量的缓冲Channel
对于方案1:
var quit chan int // 单个Channel
func foo(id int) {
fmt.Println(id)
quit <- 0 // ok, finished
}
func main() {
count := 1000
quit = make(chan int) // 无缓冲
for i := 0; i < count; i++ {
go foo(i)
}
for i := 0; i < count; i++ {
<- quit
}
}
对于方案2, 把Channel换成缓冲1000的:
quit = make(chan int, count) // 容量1000
对于这个场景而言,两者都是可以的。
但是如果存数据和取数据时间相差较大,如果不在输出层面加一个缓存,用无缓冲Channel对性能影响较大,这时候就用Buffered Channel吧。
上面都是只有一个channel的情况,那么如果存在多个channel的时候,我们该如何操作呢,Go里面提供了一个关键字select,通过select可以监听多个channel上的数据流动。
select默认是阻塞的,只有当监听的channel中有发送或接收可以进行时才会运行,当多个channel都准备好的时候,select是随机的选择一个执行的。
package main
import "fmt"
func fibonacci(c, quit chan int) {
x, y := 1, 1
for {
select {
case c <- x:
x, y = y, x + y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
在select里面还有default语法,select其实就是类似switch的功能,default就是当监听的channel都没有准备好的时候,默认执行的(select不再阻塞等待channel)。
select {
case i := <-c:
// use i
default:
// 当c阻塞的时候执行这里
}
有时候会出现goroutine阻塞的情况,那么我们如何避免整个程序进入阻塞的情况呢?我们可以利用select来设置超时,通过如下的方式实现:
func main() {
c := make(chan int)
o := make(chan bool)
go func() {
for {
select {
case v := <- c:
println(v)
case <- time.After(5 * time.Second):
println("timeout")
o <- true
break
}
}
}()
<- o
}
参考:
https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/02.7.md#select
有疑问加站长微信联系(非本文作者)