Goroutine

  goroutine是一种轻量级的"线程",go是Go语言中的关键字,使用go就会在一个新的goroutine中的并发执行.当返回数据的时候,goroutine自动结束.例如下面代码:

package main

import "fmt"

func Add(x int) {
    j := x * x
    y := x + j
    fmt.Println("x=",x," x*x=:",y," x + x * x = ",y)
}

func main() {
    for i := 0; i < 5; i++ {
        // 并发执行Add()函数
        go Add(i)
    }
}

  当执行程序并没有任何打印,这是因为Go语言程序执行的机制.当Go程序执行会初始化package main包并执行main()开始,当main()函数返回时程序退出.并不会等待其他goroutine结束.main()也是一个goroutine.

所有 goroutinemain() 函数结束时会一同结束.

  要想查看其他goroutine打印的结果可以使main()睡眠等待.修改main():

func main() {
    for i := 0; i < 5; i++ {
        // 并发执行Add()函数
        go Add(i)
    }
    // 添加等待时间 2秒
    time.Sleep(time.Second * 2)
}

  执行打印结果

x= 3  x*x=: 12  x + x * x =  12
x= 0  x*x=: 0  x + x * x =  0
x= 4  x*x=: 20  x + x * x =  20
x= 1  x*x=: 2  x + x * x =  2
x= 2  x*x=: 6  x + x * x =  6

并发通信

  两种常见的并发通信模型,即为 共享数据和消息.其中共享数据可以有很多方式.例如内存,磁盘文件,网络数据.最常见的是共享内存.

  Go语言通性方式为消息机制,消息机制认为每个并发单元都是独立的个体都有自己的变量,但在不同并发单元之间这些变量是不会共享的.每个并发单元的输入和输出只有一种,那就是消息.可以理解为进程.每个进程不会被其他的进程打扰,只做自己的工作.不同进程间靠消息通信且不会共享内存.

  在Go语言中消息通信机制为channel.让我们`不要通过共享内存来通信,而应该通过通信来共享内存.

Channel

介绍

  channelgoroutine之间的通信机制,一个goroutine可以通过channel给另一个goroutine发送消息。每一个channel都有一个特殊的类型,例如发送int类型数据的channel写作chan int.

  使用make函数创建一个channel,通过以下生命的channel是双向的,可以想channel发送数据也可以接受数据.,

// 通过make构建channel
ch := make(chan int) 
// 通过var声明
var ch chan int

  类似于map,channel也对应一个make创建的底层数据结构的引用。当复制一个额channel或用于函数参数传递时,只是靠背了一个channel的引用,因此调用者和被调用者将引用同一个channel的对象。

channel的零值为nil

发送与接收

向channel发送数据必须要接受者.

  channel有发送和接受两个主要操作,一个发送语句将值从goroutine通过channel发送到另一个执行接受操作的goroutine。发送和接受两个操作都使用<-运算符:

ch <- x // channel 接受数据 x

x <- ch // channel发送数据并赋值为x

<- ch // channel 发送数据,忽略接收者

  如下对channel发送值,在goroutine中获取channel的值:

package main

import (
    "fmt"
    "time"
)

func acceptGo(c chan int){ // 死循环接受channel消息
    for {
       n := <- c // 接受channel传如的消息.赋值为n
       fmt.Println(n)
    }
}

func myChanDemo(){
    c := make(chan int) // 构建一个channel
    go acceptGo(c) // 启动一个goroutine接受channel的消息并打印
    c <- 1 // 向channel发送数据 1
    c <- 2 // 向channel发送数据 2
    time.Sleep(time.Second * 2) // 等待2秒防止goroutine过早结束. 
}

func main(){
    myChanDemo()
}

缓冲channel

  通过上述的介绍已经知道如何创建无缓存的channel,需要注意:无缓存channel的发送操作会导致发送者goroutine阻塞,直到另一个goroutine在相同的channel上执行接受操作.当发送的channel成功传输后,两个goroutine可以继续执行后面语句。反之如果接收者先执行接受操作,接收者也会发生阻塞,知道有另一个goroutine在相同的channel上执行发送操作.

  对于有缓冲的channel可以不需要有接收者,而无缓冲的channel必须要有接收者,如下:

package main

func main() {
    c := make(chan string) // 创建接受无缓冲channel
    // 发送消息
    c <- "hello"
}

  出现错误

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:

  当生成的channel是有缓冲的可以不需要接收者,如下代码执行没有错误:

package main

func main() {
    c := make(chan string, 3) // 创建接受三个消息的缓冲channel
    // 发送消息
    c <- "hello"
    c <- "qvbilam"
    c <- "love you"
}

  需要注意的是缓冲区接受多少消息只能发送不超过缓冲数量的消息,当上述例子发送第四个消息的时候出现错误.

可以通过len(channel)查看通道的大小

关闭

  内建close方法可以用来关闭channel.

关闭channel,无法继续发送数据.如果通过range读区数据,会跳出循环

c := make(chan int,10)
close(c) // 关闭channel

  在接受消息的时候可以使用第二个参数来判断channel的状态.如下

package main

import (
    "fmt"
    "time"
)

func main() {
    c := make(chan string, 3) // 创建接受三个消息的缓冲channel
    // 发送消息
    go func(c chan string) {
        for {
            message, ok := <-c // 接受消息
            if !ok { // 判断通道是否正常
                fmt.Println("channel close")
                return
            }
            // 打印消息
            fmt.Println(message)
        }
    }(c)
    c <- "hello"
    c <- "qvbilam"
    close(c) // 关闭channel
    time.Sleep(time.Second * 3)
}

串联channel

  channel可以用于将多个goroutine连接在一起,一个channel的输出作为下一个channel的输入.这种串联的channel即是管道(pipeline).

示例

需求

  • 生成三个goroutine
    • Counter:第一个goroutine生成0,1,2...数列
    • Squarer:第二个goroutine将每个整数求平方
    • Printer:第三个goroutine打印每个整数
  • 创建两个channel
    • 第一个channel将Counter的数列发送给Squarer
    • 第二个channel将Squarer的结果发送给Printer

实现

package main

import (
    "fmt"
)

func main() {
    input := make(chan int, 10)  // 输入通道
    output := make(chan int, 10) // 输出通道
    go Counter(input)
    go Squarer(input, output)
    Printer(output)
}

// 生成数列
func Counter(input chan int) {
    for x := 2; x < 10; x++ {
        input <- x // 向输入通道发送消息 
    }
    close(input) // 关闭第一个channel
}

// 求平方和
func Squarer(input chan int, output chan int) {
    for x := range input {
        output <- x * x // 向输出通道发送消息
    }
    close(output) // 关闭第二个通道
}

// 打印结果
func Printer(output chan int) {
    for v := range output { // 直到输出通道关闭为止
        fmt.Println(v) // 打印输出通道的消息
    }
}

单向channel

  通过上述例子可以看到两个channel(input,output)是相同的类型,但是使用的方式是相反的:input只用于输入,output只用于输出,但是无法保证Squarer函数向input进行输入.当余个channel作为函数参数时,它一般总是被专门用于只发送或只接受

  类型chan <- int表示只发送int的channel,只能发送不能接受,<-chan int表示只接受不能发送.修改原先示例:

package main

import (
    "fmt"
)

func main() {
    input := make(chan int, 10)  // 输入通道
    output := make(chan int, 10) // 输出通道
    go Counter(input)
    go Squarer(input, output)
    Printer(output)
}

// 生成数列
func Counter(input chan<- int) {
    for x := 2; x < 10; x++ {
        input <- x // 向输入通道发送消息
    }
    close(input) // 关闭第一个channel
}

// 求平方和
func Squarer(input <-chan int, output chan<- int) {
    for x := range input {
        output <- x * x // 向输出通道发送消息
    }
    close(output) // 关闭第二个通道
}

// 打印结果
func Printer(output <-chan int) {
    for v := range output { // 直到输出通道关闭为止
        fmt.Println(v) // 打印输出通道的消息
    }
}

select

  select是Go中的控制结构,类似于switch语句,但是在select中每一个case必须是一个通信的操作,即发送或接收.

select随机执行可运行的case,没有case会阻塞,直到有case可运行为止,default子句应总是可运行的

  如下简单的调度,通过死循环不间断的获取消息,当有消息的时候会执行case里的内容,当不存在会执行default.需要注意的是,当通道关闭后应该跳出死循环.

package main

import "fmt"

func main() {
    c := make(chan int)
    go func(c chan int) { // 向通道传输5个消息
        for i := 0; i < 5; i++ {
            c <- i
        }
        close(c) // 传输关闭关闭通道
    }(c)
    for { // 死循环执行select
        select {
        case res, ok := <-c: // 接受消息
            if !ok { // 通过ok判断通道是否关闭
                fmt.Println("结束")
                goto end // 跳转结束
            }
            fmt.Println(res)
        default:
            fmt.Println("别爱我,没结果")  // 没有消息的时候执行
        }
    }
    end: // 结束
    fmt.Println("欢迎下次再来")
}

  下面的例子当有多个通道,在接受数据的时候不管是哪个通道,谁先发送消息就接受谁的消息,可以使用select进行任务调度:

package main

import (
    "fmt"
    "time"
)

func myChan() chan int {
    out := make(chan int, 10)
    go func() {
        for i := 0; i < 10; i++ {
            time.Sleep(time.Microsecond)
            out <- i
        }
    }()
    return out
}

func main() {
    var c1, c2 = myChan(), myChan()
    var res int
    for {
        select {
        case res = <-c1:
            fmt.Println("c1:", res)
        case res = <-c2:
            fmt.Println("c2:", res)
        default:
            // todo 不用管
        }
    }
}

  打印结果为乱序的,不管是哪个渠道,只要是先输入就先打印,没次的结果都不一样的~如下执行结果

c1: 0
c2: 0
c2: 1
c1: 1
c1: 2
c1: 3
c2: 2
c2: 3
c1: 4
c2: 4
c1: 5
c2: 5
c1: 6
c2: 6
c1: 7
c2: 7
c1: 8
c1: 9
c2: 8
c2: 9
Last modification:August 18th, 2020 at 11:34 am