Go并发(1)
来源:互联网 发布:ps mac 无法安装 编辑:程序博客网 时间:2024/05/09 03:45
Go从语言本身支持并发,而不是由某个库或者模块来实现并发,可谓天生丽质。goroutine从根本上与线程不同,goroutine更加轻量化。
看下面这个常见的网络模型:
package mainimport ( "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 mainimport ( "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 mainimport ( "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 mainimport "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 mainimport ( "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 // 单个Channelfunc 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 mainimport "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 idefault: // 当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
- Go并发(1)
- Go 并发
- go并发
- go-并发
- Go并发
- go context专题(一)- go 并发编程基础设施
- Go语言并发编程(一)
- Go语言并发编程(二)
- Go并发编程基础(译)
- Go学习笔记六(并发编程)
- Go编程基础—并发(concurrency)
- Go?GO!(三) Go的面向对象技术、并发和包的简单介绍
- GO语言的并发
- Go-简洁的并发
- go语言并发
- GO语言学习-并发
- go并发学习
- Go-简洁的并发
- 为什么要使用RTP
- 数据库SQL性能查询
- 1、action方法使用
- MyEclipse中SVN的使用方法
- 使用 AngularJS & NodeJS 实现基于 token 的认证应用
- Go并发(1)
- 设计模式之工厂模式与反射
- Android 解决程序启动时的黑屏问题
- 【BeiJing2011】【BZOJ2351】Matrix
- 2、接收参数: ModelDriven接口接收参数和(常规)action的属性接收参数
- Android ApplicationContext和Context
- iOS 框架介绍
- 使用asm.jar将Android手机屏幕投影到电脑
- Eclipse中setter/getter方法自动添加属性注释