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

0 0
原创粉丝点击