Go并发(1)

kzq_qmi · · 1730 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

  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


有疑问加站长微信联系(非本文作者)

本文来自:CSDN博客

感谢作者:kzq_qmi

查看原文:Go并发(1)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1730 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传