一文带你学会Go select语句轻松实现高效并发

作者:陈明勇 时间:2024-05-22 10:29:50 

前言

Go 语言中,GoroutineChannel 是非常重要的并发编程概念,它们可以帮助我们解决并发编程中的各种问题。关于它们的基本概念和用法,前面的文章 一文初探 Goroutine 与 channel 中已经进行了介绍。而本文将重点介绍 select,它是协调多个 channel 的桥梁。

select 介绍

什么是 select

selectGo 语言中的一种控制结构,用于在多个通信操作中选择一个可执行的操作。它可以协调多个 channel 的读写操作,使得我们能够在多个 channel 中进行非阻塞的数据传输、同步和控制。

为什么需要 select

Go 语言中的 select 语句是一种用于多路复用通道的机制,它允许在多个通道上等待并处理消息。相比于简单地使用 for 循环遍历通道,使用 select 语句能够更加高效地管理多个通道。

以下是一些 select 语句的使用场景:

1.等待多个通道的消息(多路复用)

当我们需要等待多个通道的消息时,使用 select 语句可以非常方便地等待这些通道中的任意一个通道有消息到达,从而避免了使用多个goroutine进行同步和等待。

2.超时等待通道消息

当我们需要在一段时间内等待某个通道有消息到达时,使用 select 语句可以与 time 包结合使用实现定时等待。

3.在通道上进行非阻塞读写

在使用通道进行读写时,如果通道没有数据,读操作或写操作将会阻塞。但是使用 select 语句结合 default 分支可以实现非阻塞读写,从而避免了死锁或死循环等问题。

因此,select 的主要作用是在处理多个通道时提供了一种高效且易于使用的机制,简化了多个 goroutine 的同步和等待,使程序更加可读、高效和可靠。

select 基础

语法

select {
   case <- channel1:
       // channel1准备好了
   case data := <- channel2:
       // channel2准备好了,并且可以读取到数据data
   case channel3 <- data:
       // channel3准备好了,并且可以往其中写入数据data
   default:
       // 没有任何channel准备好了
}

其中, <- channel1 表示读取 channel1 的数据,data <- channel2 表示用 data 去接收数据;channel3 <- data 表示往 channel3 中写入数据。

select 的语法形式类似于 switch,但是它只能用于 channel 操作。在 select 语句中,我们可以定义多个 case,每个 case 都是一个 channel 操作,用于读取或写入数据。如果有多个 case 同时可执行,则会随机选择其中一个。如果没有任何可执行的 case,则会执行 default 分支(如果存在),或者阻塞等待直到至少有一个 case 可执行为止。

基本用法

package main

import (
  "fmt"
  "time"
)

func main() {
  ch1 := make(chan int)
  ch2 := make(chan int)

go func() {
     time.Sleep(1 * time.Second)
     ch1 <- 1
  }()

go func() {
     time.Sleep(2 * time.Second)
     ch2 <- 2
  }()
  for i := 0; i < 2; i++ {
     select {
     case data, ok := <-ch1:
        if ok {
           fmt.Println("从 ch1 接收到数据:", data)
        } else {
           fmt.Println("通道已被关闭")
        }
     case data, ok := <-ch2:
        if ok {
           fmt.Println("从 ch2接收到数据: ", data)
        } else {
           fmt.Println("通道已被关闭")
        }
     }
  }

select {
  case data, ok := <-ch1:
     if ok {
        fmt.Println("从 ch1 接收到数据:", data)
     } else {
        fmt.Println("通道已被关闭")
     }
  case data, ok := <-ch2:
     if ok {
        fmt.Println("从 ch2接收到数据: ", data)
     } else {
        fmt.Println("通道已被关闭")
     }
  default:
     fmt.Println("没有接收到数据,走 default 分支")
  }
}

执行结果

从 ch1 接收到数据: 1
从 ch2接收到数据:  2
没有接收到数据,走 default 分支

上述示例中,首先创建了两个 channelch1ch2,分别在不同的 goroutine 中向两个 channel 中写入数据。然后,在主 goroutine 中使用 select 语句监听两个channel,一旦某个 channel 上有数据流动,就打印出相应的数据。由于 ch1 中的数据比 ch2 中的数据先到达,因此首先会打印出 "从 ch1 接收到数据: 1",然后才打印出 "从 ch2接收到数据: 2"

为了方便测试 default 分支,我写了两个 select 代码块,执行到第二个 select 代码块的时候,由于 ch1ch2 都没有数据了,因此执行 default 分支,打印 "没有接收到数据,走 default 分支"

一些使用 select 与 channel 结合的场景

实现超时控制

package main

import (
  "fmt"
  "time"
)

func main() {
  ch := make(chan int)
  go func() {
     time.Sleep(3 * time.Second)
     ch <- 1
  }()

select {
  case data, ok := <-ch:
     if ok {
        fmt.Println("接收到数据: ", data)
     } else {
        fmt.Println("通道已被关闭")
     }
  case <-time.After(2 * time.Second):
     fmt.Println("超时了!")
  }
}

执行结果为:超时了!

在这个例子中,程序将在 3 秒后向 ch 通道里写入数据,而我在 select 代码块里设置的超时时间为 2 秒,如果在 2 秒内没有接收到数据,则会触发超时处理。

实现多任务并发控制

package main

import (
  "fmt"
)

func main() {
  ch := make(chan int)

for i := 0; i < 10; i++ {
     go func(id int) {
        ch <- id
     }(i)
  }

for i := 0; i < 10; i++ {
     select {
     case data, ok := <-ch:
        if ok {
           fmt.Println("任务完成:", data)
        } else {
           fmt.Println("通道已被关闭")
        }
     }
  }
}

执行结果(每次执行的顺序都会不一致):

任务完成: 1
任务完成: 5
任务完成: 2
任务完成: 3
任务完成: 4
任务完成: 0
任务完成: 9
任务完成: 6
任务完成: 7
任务完成: 8

在这个例子中,启动了 10 个 goroutine 并发执行任务,并使用一个 channel 来接收任务的完成情况。在主函数中,使用 select 语句监听这个 channel,每当接收到一个完成的任务时,就进行处理。

监听多个通道的消息

package main

import (
  "fmt"
  "time"
)

func main() {
  ch1 := make(chan int)
  ch2 := make(chan int)

// 开启 goroutine 1 用于向通道 ch1 发送数据
  go func() {
     for i := 0; i < 5; i++ {
        ch1 <- i
        time.Sleep(time.Second)
     }
  }()

// 开启 goroutine 2 用于向通道 ch2 发送数据
  go func() {
     for i := 5; i < 10; i++ {
        ch2 <- i
        time.Sleep(time.Second)
     }
  }()

// 主 goroutine 从 ch1 和 ch2 中接收数据并打印
  for i := 0; i < 10; i++ {
     select {
     case data := <-ch1:
        fmt.Println("Received from ch1:", data)
     case data := <-ch2:
        fmt.Println("Received from ch2:", data)
     }
  }

fmt.Println("Done.")
}

执行结果(每次执行程序打印的顺序都不一致):

Received from ch2: 5
Received from ch1: 0
Received from ch1: 1
Received from ch2: 6
Received from ch1: 2
Received from ch2: 7
Received from ch1: 3
Received from ch2: 8
Received from ch1: 4
Received from ch2: 9
Done.

该示例代码中,通过使用 select 多路复用,可以同时监听多个通道的数据,并避免了使用多个 goroutine 进行同步和等待的问题。

使用 default 实现非阻塞读写

import (
  "fmt"
  "time"
)

func main() {
  ch := make(chan int, 1)

go func() {
     for i := 1; i <= 5; i++ {
        ch <- i
        time.Sleep(1 * time.Second)
     }
     close(ch)
  }()

for {
     select {
     case val, ok := <-ch:
        if ok {
           fmt.Println(val)
        } else {
           ch = nil
        }
     default:
        fmt.Println("No value ready")
        time.Sleep(500 * time.Millisecond)
     }
     if ch == nil {
        break
     }
  }
}

执行结果(每次执行程序打印的顺序都不一致):

No value ready
1
No value ready
2
No value ready
No value ready
3
No value ready
No value ready
4
No value ready
No value ready
5
No value ready
No value ready

这个代码中,使用了 default 分支来实现非阻塞的通道读取和写入操作。在 select 语句中,如果有通道已经准备好进行读写操作,那么就会执行相应的分支。但是如果没有任何通道准备好读写,那么就会执行 default 分支中的代码。

select 的注意事项

以下是关于 select 语句的一些注意事项:

  • select 语句只能用于通信操作,如 channel 的读写,不能用于普通的计算或函数调用。

  • select 语句会阻塞,直到至少有一个 case 语句满足条件。 如果有多个 case 语句满足条件,则会随机选择一个执行。

  • 如果没有 case 语句满足条件,并且有 default 语句,则会执行 default 语句。

  • select 语句中使用 channel 时,必须保证 channel 是已经初始化的。

  • 如果一个通道被关闭,那么仍然可以从它中读取数据,直到它被清空,此时会返回通道元素类型的零值和一个布尔值,指示通道是否已关闭。

总之,在使用 select 语句时,要仔细考虑每个 case 语句的条件和执行顺序,避免死锁和其他问题。

来源:https://juejin.cn/post/7210005376653377597

标签:Go,select
0
投稿

猜你喜欢

  • python第三方库visdom的使用入门教程

    2021-12-08 22:32:51
  • 删除python pandas.DataFrame 的多重index实例

    2022-11-10 17:25:12
  • 基于JS判断iframe是否加载成功的方法(多种浏览器)

    2023-08-24 04:14:52
  • SQL查询服务器下所有数据库及数据库的全部表

    2024-01-18 22:55:01
  • pytorch MSELoss计算平均的实现方法

    2021-07-31 18:44:15
  • Go语言开发中redis的使用详解

    2024-02-14 12:58:27
  • Python操作redis和mongoDB的方法

    2023-01-01 17:06:34
  • XML与HTML的结合(上)

    2008-09-05 17:19:00
  • oracle group by语句实例测试

    2024-01-26 16:11:40
  • vue-cli3.0 脚手架搭建项目的过程详解

    2023-07-02 17:05:09
  • jQuery事件的绑定、触发、及监听方法简单说明

    2024-02-24 12:50:53
  • vue多次循环操作示例

    2024-05-28 15:48:20
  • Python脚本完成post接口测试的实例

    2022-01-01 15:40:22
  • asp自动采集程序

    2009-02-04 10:11:00
  • vue实现鼠标滑动展示tab栏切换

    2023-07-02 16:38:05
  • asp常用的SQL命令操作

    2007-09-29 12:21:00
  • MySQL最新漏洞分析

    2012-07-11 15:41:10
  • Python实现的爬取百度文库功能示例

    2022-09-08 22:34:46
  • python入门之井字棋小游戏

    2021-12-11 11:35:36
  • 利用python判断字母大小写的几种方法小结

    2022-05-10 16:41:49
  • asp之家 网络编程 m.aspxhome.com