编程 Go 语言中的 `select` 使用及基本实现

2024-11-18 22:38:30 +0800 CST views 427

Go 语言中的 select 使用及基本实现

1.1 select 简介:

在 Go 语言中,select 语句用于处理多个通道操作,简化并发编程中的通信和同步问题。select 类似于 switch 语句,但它的每个 case 都必须是一个通道操作。

基本语法如下:

select {
case <-chan1:
    // 当 chan1 有数据可以接收时执行
case chan2 <- value:
    // 当可以向 chan2 发送数据时执行
default:
    // 当没有满足条件的 case 时执行
}

使用场景

  • 多通道选择:同时等待多个通道操作,当任一通道准备好时执行相应的 case
  • 超时处理:结合 time.After 实现超时机制。
  • 非阻塞通信:使用 default 子句实现非阻塞的通道操作。

1.2 select 与通道

switch 不同,select 中的每个 case 都必须与 Channel 的读写操作有关,避免等待某个阻塞的 Channel 导致死锁。

示例 1:从多个通道接收数据

package main

import (
    "fmt"
    "time"
)

func main() {
    chan1 := make(chan string)
    chan2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        chan1 <- "Message from channel 1"
    }()

    go func() {
        time.Sleep(2 * time.Second)
        chan2 <- "Message from channel 2"
    }()

    select {
    case msg1 := <-chan1:
        fmt.Println(msg1)
    case msg2 := <-chan2:
        fmt.Println(msg2)
    }
}

在此示例中,select 等待第一个准备好的通道并接收消息,然后输出相应信息。

示例 2:处理超时

package main

import (
    "fmt"
    "time"
)

func main() {
    chan1 := make(chan string)
    chan2 := make(chan string)

    go func() {
        time.Sleep(3 * time.Second)
        chan1 <- "Message from channel 1"
    }()

    go func() {
        time.Sleep(2 * time.Second)
        chan2 <- "Message from channel 2"
    }()

    select {
    case msg1 := <-chan1:
        fmt.Println(msg1)
    case msg2 := <-chan2:
        fmt.Println(msg2)
    case <-time.After(1 * time.Second):
        fmt.Println("Timeout, no channel was ready within 1 second")
    }
}

此示例展示了如何结合超时机制处理多个通道操作。

示例 3:非阻塞通信

package main

import (
    "fmt"
)

func main() {
    chan1 := make(chan string)
    select {
    case msg := <-chan1:
        fmt.Println(msg)
    default:
        fmt.Println("Default case: Channel is not ready")
    }
}

当通道未准备好时,default 子句会被执行,避免阻塞。

1.3 select 的实现

1.3.1 数据结构

select 没有直接对应的结构体,但在 Go 语言源码中,它通过 runtime.scase 结构体表示 select 控制结构中的 case

type scase struct {
    c    *hchan         // 通道
    elem unsafe.Pointer // 数据元素
}

1.3.2 常见流程

编译器会将所有的 case 转换为 runtime.scase 结构体,并调用 runtime.selectgo 函数来从多个通道中选择一个。

1.3.3 随机轮询与加锁顺序

runtime.selectgo 函数中,首先确定轮询顺序和加锁顺序。轮询顺序通过 runtime.cheaprandn 函数引入随机性,而加锁顺序则按通道的地址排序,防止死锁。

func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
    ...
    j := cheaprandn(uint32(norder + 1)) // 引入随机性
    pollorder[norder] = pollorder[j]
    pollorder[j] = uint16(i)
    ...
    sellock(scases, lockorder) // 加锁顺序
}

func sellock(scases []scase, lockorder []uint16) {
    for _, o := range lockorder {
        c := scases[o].c
        lock(&c.lock) // 按顺序锁定通道
    }
}

随机轮询可以避免通道饥饿问题,加锁顺序避免死锁。

1.4 性能考量

  • 注册和选择开销:每次 select 操作都涉及将通道操作注册到调度队列,并从中选择可执行的通道。
  • 随机选择开销:涉及随机数生成和对就绪通道的扫描。
  • 阻塞和唤醒开销:阻塞和唤醒 goroutine 也会引入上下文切换开销。

1.5 I/O 多路复用

Go 语言的 select 类似于操作系统中的 select 系统调用。比如 C 语言中的 select 可以同时监听多个文件描述符的状态,Go 的 select 则用于监听多个通道的状态。

1.6 总结

表面来看,Go 的 select 语句是一种用于 Channel 操作的专用语句,深层次理解,它是 Go 在语言层面提供的 I/O 多路复用机制。

更多信息请参考文章:

复制全文 生成海报 Go语言 并发编程 编程技巧

推荐文章

初学者的 Rust Web 开发指南
2024-11-18 10:51:35 +0800 CST
使用Python实现邮件自动化
2024-11-18 20:18:14 +0800 CST
Vue3中如何处理SEO优化?
2024-11-17 08:01:47 +0800 CST
15 个 JavaScript 性能优化技巧
2024-11-19 07:52:10 +0800 CST
55个常用的JavaScript代码段
2024-11-18 22:38:45 +0800 CST
最全面的 `history` 命令指南
2024-11-18 21:32:45 +0800 CST
MySQL设置和开启慢查询
2024-11-19 03:09:43 +0800 CST
php获取当前域名
2024-11-18 00:12:48 +0800 CST
Vue3中的v-slot指令有什么改变?
2024-11-18 07:32:50 +0800 CST
内网穿透技术详解与工具对比
2025-04-01 22:12:02 +0800 CST
支付宝批量转账
2024-11-18 20:26:17 +0800 CST
使用xshell上传和下载文件
2024-11-18 12:55:11 +0800 CST
Vue3中如何进行异步组件的加载?
2024-11-17 04:29:53 +0800 CST
Go 协程上下文切换的代价
2024-11-19 09:32:28 +0800 CST
Elasticsearch 的索引操作
2024-11-19 03:41:41 +0800 CST
Nginx 防盗链配置
2024-11-19 07:52:58 +0800 CST
程序员茄子在线接单