欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Go:23---select多路复用

程序员文章站 2022-06-13 12:37:48
...

一、一个例子(火箭发射程序)

  • 假设现在我们还有一个这样的火箭发生倒计时程序:程序中创建了一个管道,管道定期发送数据,当接收到10次数据之后发射火箭
package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("Commencing countdown.")

    // time.Tick返回一个管道, 管道命名为tick
    tick := time.Tick(1 * time.Second)

    // 循环从管道中接收10次数据
    for countdown := 10; countdown > 0; countdown-- {
        fmt.Println(countdown)
        <- tick
    }
    
    // 循环完之后执行火箭发射函数
    launch() 
}

func launch() {
    fmt.Println("Lift off!")
}

Go:23---select多路复用

  • 现在我们在该程序中新增一个功能:上面的程序没有中断功能,此时我们想添加一个功能,能够在倒计时的时候按下回车键来取消火箭的发射,改进后的代码如下所示,我们新建一个abort管道,当按下回车之后发送一个数据到abort管道中
// 创建一个管道
abort := make(chan struct{})

// 开启一个goroutine, 如果按下了回车键, 那么发送一个数据到abort管道中(下面介绍)
go func() {
    os.Stdin.Read(make([]byte, 1))
    abort <- struct{}{}
}
  • 此处我们的程序中有2个管道,并且每个管道执行不同的功能,一个用来发送火箭,一个用来中止火箭发送。为了方便管理这些管道,我们可以使用select进行管理,见下面介绍

二、select简介

  • select 是 Go 中的一个控制结构,类似于用于通信的 switch 语句。每个case必须是一个通信操作,要么是发送要么是接收
  • select随机执行一个可运行的case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。一个默认的子句应该总是可运行的

语法格式

  • select的格式如下:
select {
    case communication clause  :
       statement(s);      
    case communication clause  :
       statement(s); 
    /* 你可以定义任意数量的 case */
    default : /* 可选 */
       statement(s);
}
  • 相关语法:
    • 每个case都必须是一个通信
    • 所有channel表达式都会被求值
    • 所有被发送的表达式都会被求值
    • 如果任意某个通信可以进行,它就执行,其他被忽略
    • 如果有多个case都可以运行,select 会随机公平地选出一个执行。其他不会执行
    • 如果没有通道可以执行,则:
      • 如果有default子句,则执行该语句
      • 如果没有default子句,select 将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值

三、使用select重构火箭发射程序

实现1

  • 有了select之后,我们可以将火箭发射程序进行重构,代码如下:程序中select检测两个管道,如果10秒内接收到abort的数据则终止程序,如果10秒内什么也没接收到,那么10秒之后time.After()返回一个管道数据,从而结束main函数
package main

import (
    "fmt"
    "time"
    "os"
)

func main() {
    // 创建一个管道
    abort := make(chan struct{})

    // 开启后台goroutine, 如果从标准输入接收到数据, 向abort管道发送数据
    go func() {
        os.Stdin.Read(make([]byte, 1))
        abort <- struct{}{}
    }()

    fmt.Println("Commencing countdown.  Press return to abort.")

    select {
        case <-time.After(10 * time.Second):  // 10秒返回一个管道数据
            // 什么也不做
        case <-abort:                         // 如果接收到终止, 退出程序
            return
    }

    launch()
}

func launch() {
    fmt.Println("Lift off!")
}
  • 如果10秒内不回车,那么程序正常结束,代表火箭发射成功

Go:23---select多路复用

  • 如果10秒内回车,那么程序终止,代表火箭发射终止:

Go:23---select多路复用

实现2:打印倒计时

  • 现在我们改写火箭发射程序,让其可以打印倒计时。代码如下:
package main

import (
    "fmt"
    "time"
    "os"
)

func main() {
    // 中止管道
    abort := make(chan struct{})

    go func() {
        os.Stdin.Read(make([]byte, 1))
        abort <- struct{}{}
    }()

    // tick为一个管道
    tick := time.Tick(1 * time.Second)

    fmt.Println("Commencing countdown.  Press return to abort.")
    for countdown := 10; countdown > 0; countdown-- {
        // 打印信息
        fmt.Println(countdown)

        select {
            case <-tick:
                // 什么也不做
            case <-abort:
                return
        }
    }

    launch()
}

func launch() {
    fmt.Println("Lift off!")
}

Go:23---select多路复用

goroutine泄漏(time.Tick()与time.NewTicker())

  • time.Tick()可能会带来goroutine泄漏,time.Tick()函数创建了一个goroutine在循环里面调用time.Sleep,当time.Sleep返回时向管道发送一个数据
  • 在上面的"实现2"代码中,如果上面的代码不是运行在一个main函数里面,假设运行在funcTest()函数中,其中tick就是一个管道,当函数funcTest()结束之后,time.Tick()后台创建的goroutine还没有结束,而funcTest()函数已经结束,那么就没有人能从goroutine中接收数据了,导致后台goroutine一直阻塞运行,此时造成goroutine泄漏
func funcTest() {
    // time.Tick后台会创建一个goroutine
    tick := time.Tick(1 * time.Second)


    for countdown := 10; countdown > 0; countdown-- {
        fmt.Println(countdown)

        select {
            case <-tick:
                // 什么也不做
            case <-abort:
                return
        }
    }

} // 当funcTest函数结束之后, time.Tick()创建的goroutine仍在运行, 但是没有人从该goroutine中创建的管道上接收数据了, 造成goroutine泄漏
  • 一个解决办法就是使用time.NewTicker()函数
ticker := time.NewTicker(1 * time.Second)
<-ticker.C    // 从管道中接收数据
ticker.Stop() //不使用时可以关闭, 后台goroutine也结束
  • 因为time.Tick()函数适用于整个生命周期中都要使用它的情况,否则就使用上面的接口 

四、演示案例

  • 下面的例子中用到了缓冲通道,通道ch的缓冲区大小为1,它要么是空的,要么是慢的,因此只有在其中一个状况下可以执行:i是偶数时发送,i是奇数时接收
  • 代码如下:
package main

import "fmt"

func main() {
    // 创建一个缓冲区大小为1的管道
    ch := make(chan int, 1)

    for i := 0; i < 10; i++ {
        select {
            case x := <-ch:     // 从管道中接收数据
                fmt.Println(x)
            case ch <- i:       // 向管道发送数据
        }    
    }
}

Go:23---select多路复用

五、select的非阻塞操作(default)

  • 在使用select的时候,我们不想在通道没有准备好的情况下被阻塞——非阻塞通信,这个时候可以使用sleect的default语句
  • 例如,下面的代码尝试从abort通道上接收一个值,如果什么也没有则什么也不做,重复对通道的轮询
select {
    case <-abort:
        fmt.Println("Launch aborted!")
        return
    default:
        // 什么也不做
}

六、nil通道

  • 通道的零值是nil
  • 令人惊讶的是,nil通道有时候很有用。因为在nil通道上发送和接收将永远阻塞
  • 对于select语句中的情况,如果其通道是nil,它将永远不会被选择
  • 这次让我们用nil来开启或禁用特性所对应的情况,比如超时处理或者取消操作,响应其他的输入事件或者发送时间
  • 在下面的"并发目录遍历"演示案例中使用到了nil通道,见下面代码

七、演示案例(并发目录遍历)

  • 待续