阅读 398

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")
    }
}
复制代码