成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

一文搞懂Go中select的隨機公平策略:并發(fā)編程的黃金法則

開發(fā) 后端
本文中,我們談到了 Go 語言里 select? 的基本特性和實現(xiàn),提到了select?與直接 Channel 操作的相似性,以及通過 default 支持非阻塞收發(fā)操作。

一、引言

今天呢,咱們來聊聊 Go 語言的那點事兒,尤其是咱們在并發(fā)處理中常用的 select 語句,它可是處理并發(fā)時的一把利劍!

Go 語言的 select 語句,仿佛是編程世界中的一位冷靜的裁判,當多個通道(channel)全都爭著搶話語權的時候,它就會站出來,公平地判決誰應當先發(fā)聲。

換句話說,select 可以在多個通道之間等待并選擇可用的通道執(zhí)行操作。

你得這么看select語句——它是并發(fā)編程領域里的一塊重要的拼圖,沒有這塊,你畫出的并發(fā)圖景就不完整。

首先,我們來看一個簡單的示例:

select {
case <-chan1:
 // 操作1
case data := <-chan2:
 // 操作2
case chan3 <- data:
 // 操作3
default:
 // 默認操作
}

還別說,這幾行代碼,簡單明了,但它背后可是隱藏著深邃的并發(fā)處理智慧:

  • select 可以在 channel 上進行非阻塞的收發(fā)操作;
  • 當存在可以收發(fā)的 channel 時,會直接處理該 channel 對應的 case;
  • 如果沒有任何通道準備好,它就會執(zhí)行 default 子句(如果有的話);
  • 如果連 default 都沒得,那它就會那么靜靜站著,不厭其煩地等待,直到有一個通道準備好。

優(yōu)雅!這是使用過 select 語句后,我心中的感嘆。就像你有了一塊功能強大的瑞士軍刀,可以靈活地應對各種野外求生的情況。

在代碼中,select 語句也可以靈活地處理多個通道的并發(fā)操作,避免使用復雜的同步工具實現(xiàn)并發(fā)操作。

二、select 機制

講科技,不能光有干巴巴的代碼堆砌,還得有歷史沉淀(反正以前歷史老師是這么教的 :)。

而我們現(xiàn)在探討的是 Go 語言里的 select 思想,它最初源自于網絡 IO 模型中的 select,其精華在于 IO 多路復用。

想象一下,有那么一刻,你需要同時傾聽來自世界各地的廣播,這可不是一件簡單的事兒。然而,這正是 go 中的 channel 和 select 的日常所在:致力于協(xié)調多個渠道的信息流,也只有在這里,才有 “通道爭鳴” 的景象。

1. Go select 特性

讓我們像切洋蔥一樣,一層層地剝開 select 神秘的外衣:

  • 每個案例必須是個通道:這是規(guī)定,沒得商量,像是一場形式各異的對話,總得有人發(fā)聲,對吧?
  • 所有被發(fā)送的通道表達式先被求值:這就像是通道們排著隊,等待裁判 select 逐一審視。
  • 如果有多個符合條件,select 公平地隨機挑一個執(zhí)行:這也是 select 魅力之一所在,我們下文會從代碼層面探討這個特性。
  • 如果沒有通道準備好,執(zhí)行 default 子句(如果有的話):和網絡選擇一樣,咱不能干等著,得找點事做。
  • 沒有 default,select 就等著,也許數(shù)秒,也許是永恒:如果沒有 default,那也只能干等著了,考驗裁判耐心的時刻。
  • 通道關閉時,讀取會導致死循環(huán):這像是一個已經倒閉的電臺,但你的收音機還在不斷嘗試調頻接收信號。
  • 空的 select 會造成死鎖:這就是沒有對話的對話框,靜默的無聲世界。

2. 特性驗證

接下來,咱們通過一系列實驗來檢驗真實世界中 select 的行為。

(1) select 已關閉通道和空通道場景

再來看以下代碼:

func main() {
    c1, c2, c3 := make(chan bool), make(chan bool), make(chan bool)
    go func() {
        for {
            select {
                // 保證c1一定不會關閉,否則會死循環(huán)此case
                case <-c1:
                    fmt.Println("case c1")
                // c2可以防止出現(xiàn)c1死循環(huán)的情況
                // 如果c2關閉了(ok==false),將其置為nil,下次就會忽略此case
                case _, ok := <-c2:
                if !ok {
                    fmt.Println("c2 is closed")
                    c2 = nil
                }
                // 如果c3已關閉,則panic(不能往已關閉的channel里寫數(shù)據(jù))
                // 如果c3為nil,則ignore該case
                case c3 <- true:
                    fmt.Println("case c3")
               case v <- c4:
                   fmt.Println(v)
            }
        }
    }()
    time.Sleep(10 * time.Second)
}

當 channel 關閉以后,case <- chan 會接收該通信對應數(shù)據(jù)類型的零值,所以會出現(xiàn)死循環(huán)。

(2) 帶 default 語句實現(xiàn)非阻塞讀寫

select {
   case <- c1:
   fmt.Println(":case c3")
   // 當c1沒有消息時,不會一直阻塞,而是進入default
   default:
   fmt.Println(":select default")
}

注意,Go 語言的 select 和 Java 或者 C 語言的 switch 還不太一樣:switch 中一般會帶有 default 判斷分支,但 select 使用時,外層的 for 循環(huán)和 default 不會同時出現(xiàn),否則會發(fā)生死鎖。

(3) select 實現(xiàn)定時任務

func main() {
   done := make(chan bool)
   var selectTest = func() {
      for {
         select {
            case <-time.After(1 * time.Second):
            fmt.Println("Working...")
            case <-done:
            fmt.Println("Job done!")
        }
    }
  }
   go selectTest()

   time.Sleep(3 * time.Second)
   done <- true
   time.Sleep(500 * time.Microsecond)
}

這個例子模擬的是一個簡易的定時器,每隔一秒鐘它都會打印 "Working..." 直到我們通過關閉 done 通道告訴它 "任務完成"。

這樣的模式在你需要定時檢查或者定時執(zhí)行一些任務時非常有用!

代碼運行結果:

Working...

Working...

Job done!

注意,如果定時器的另外 case 分支是上面已關閉 channel 場景,可能會出現(xiàn)異常,如下所示:

func main() {
    done := make(chan bool)
    t := time.Now()
    var selectTest = func() {
        for {
            select {
                case <-time.After(100 * time.Microsecond):
                    fmt.Println(time.Since(t), " time.After exec, return!")
                    return
                case <-done:
                    fmt.Println("over")
            }
    }
}
    // 關閉 chan
    close(done)
    go selectTest()
    time.Sleep(2 * time.Second)
}

我們在并發(fā)執(zhí)行之前就 close(done) 關閉了 Channel,不妨猜一下這段代碼會輸出什么,答案是:

...

over

over

over

601.3938ms  time.After exec, return!

這是因為:done 已經被關閉了,所以當執(zhí)行 case <-done 語句時會死循環(huán)此 case 分支。

但是,為什么還會執(zhí)行退出 case,而且 return 時,時間來到了 601.3938ms 呢?

從上面代碼中定時器 case 100 ms 執(zhí)行一次,我們不難得知,程序退出時是第 6 次執(zhí)行 select 語句,這里面究竟有什么魔法呢?

讓我們接著往下看!

3.多個 case 滿足讀寫條件

上文已經描述過,如果多個 case 滿足讀取條件時,select 會隨機選擇一個語句執(zhí)行。

讓我們用代碼來詳細描述一下:

func main() {
   done := make(chan int, 1024)
   tick := time.NewTicker(time.Second)
   var selectTest = func() {
      for i := 0; i < 10; i++ {
         select {
            case done <- i:
            fmt.Println(i, ", done exec")
            case <- tick.C:
            fmt.Println(i, ", time.After exec")
        }
         time.Sleep(500 * time.Millisecond)
    }
  }
   go selectTest()

   time.Sleep(5 * time.Second)
}

這個例子開啟了一個 goroutine 協(xié)程來運行 selectTest 函數(shù),在函數(shù)里面 for 循環(huán) 10 次執(zhí)行 select 語句。并且,select 的兩個分支 case done <- i 和 case <- tick.C 都是可以執(zhí)行的。

這時候,我們看一下執(zhí)行結果:

0 , done exec

1 , done exec

2 , time.After exec

3 , done exec

4 , time.After exec

5 , done exec

6 , done exec

7 , done exec

8 , time.After exec

9 , done exec

注意,以上結果多次運行的打印順序可能不一致,是正常現(xiàn)象!

我們可以發(fā)現(xiàn),原本寫入 done 通道的 2、4 和 8 不見了,說明在循環(huán)的過程中,select 的兩個分支 case done <- i 和 case <- tick.C 都是執(zhí)行了的。

因此,這就驗證了當多個 case 同時滿足時,select 會隨機選擇一個執(zhí)行。這個設計是為了避免某個 case 出現(xiàn)饑餓問題,保證公平競爭而引入的。

試想一下,如果某個 case 一直執(zhí)行,而某些 case 一直得不到執(zhí)行,這和 select 公平選擇的初衷就沖突了。

所以,Go 在十多年前新增 select 提交時就用了這種隨機策略并保留至今,雖然中途有過細微的變更,但整體語義一直沒有變化。

三、底層原理

Go語言中 select用于處理多個通道(channel)的發(fā)送和接收操作,但在 Go 語言的源代碼中沒有直接對應的結構體。

因此,select通過runtime.scase結構體表示其中的每個 case,該結構體包含指向通道和數(shù)據(jù)元素的指針:

type scase struct {
c    *hchan         // chan
elem unsafe.Pointer // data element
}

1.編譯器

編譯時,select 語句被轉換成 OSELECT 節(jié)點,持有 OCASE 節(jié)點集合,每個 OCASE 代表一個可能的操作,包括空(對應 default)。

根據(jù)情況不同,編譯器會優(yōu)化select的處理過程。優(yōu)化處理的情況分為:

  • 不存在任何 case 的 select:直接阻塞,使用 runtime.block 函數(shù)使當前協(xié)程(goroutine)進入永久休眠狀態(tài)。
  • 只有一個 case 的 select:改寫成單個 if 條件語句。
  • 有一個 case 是 default 的兩個 case 的 select:被視為非阻塞操作,分別優(yōu)化為非阻塞發(fā)送或接收。
  • 多個 case:通過運行時函數(shù) runtime.selectgo 處理,從幾個待執(zhí)行的 case 中選擇一個。

非阻塞操作進行相應的編譯器重寫,發(fā)送使用 runtime.selectnbsend 函數(shù)進行非阻塞發(fā)送,接收方面有兩種函數(shù)處理單值和雙值接收。

2.運行時

運行時,runtime.selectgo函數(shù)通過以下幾個步驟處理 select:

  • 初始化階段,決定 case 的處理順序。
  • 遍歷 case,查找立即就緒的 Channel,如果有則立即處理。
  • 如果沒有立即就緒的 Channel,將當前 Goroutine 加入到所有相關 Channel 的收發(fā)隊列中,并掛起。
  • 當某個 case 就緒時(Channel 收到數(shù)據(jù)或有空間發(fā)送數(shù)據(jù)),調度器喚醒掛起的 Goroutine,查找并處理對應的 case。

四、小結

本文中,我們談到了 Go 語言里 select 的基本特性和實現(xiàn),提到了select與直接 Channel 操作的相似性,以及通過 default 支持非阻塞收發(fā)操作。

我們還揭示了select 底層實現(xiàn)的復雜性——需要編譯器和運行時支持。

通過以上不難得知,Go 的 select 語句在不同場景下的行為和實現(xiàn)是比較奇妙的,這也是 Go 獨特的數(shù)據(jù)結構,其背后的設計與優(yōu)化策略都需要我們對 Go 底層有著比較完善的認知。

責任編輯:趙寧寧 來源: xin猿意碼
相關推薦

2023-05-22 13:27:17

2025-04-28 02:22:00

2022-07-15 08:16:56

Stream函數(shù)式編程

2024-01-29 12:22:07

設計模式策略模式

2023-07-04 08:56:07

指針類型Golang

2020-11-04 07:49:04

Select

2021-12-29 17:38:17

JavaScripttypeof前端

2023-03-14 09:03:20

Go語法腳本

2024-04-12 12:19:08

語言模型AI

2022-03-24 08:51:48

Redis互聯(lián)網NoSQL

2023-05-31 13:32:08

Javalambda函數(shù)

2020-08-10 07:54:28

編程并發(fā)模型

2022-05-05 16:47:24

Docker網絡空間容器

2020-05-15 16:37:13

PowerBI數(shù)據(jù)分析

2023-09-15 12:00:01

API應用程序接口

2023-09-08 08:20:46

ThreadLoca多線程工具

2021-03-22 10:05:59

netstat命令Linux

2025-03-12 02:00:00

經營分析模型策略

2019-11-13 09:27:55

Web密碼學數(shù)據(jù)

2023-11-25 09:41:34

GogRPCHandler
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 韩日精品在线观看 | 久草视频观看 | 久久久久国产一区二区三区不卡 | av色噜噜 | 日韩中文字幕视频在线 | 成人免费一区二区 | 美人の美乳で授乳プレイ | 国产专区在线 | 欧美成人自拍 | av一区二区三区四区 | 国产视频一二三区 | 一级做a爰片性色毛片16美国 | 91污在线 | 色婷婷综合久久久久中文一区二区 | 超碰婷婷 | 黄色成人免费在线观看 | 亚洲精品电影网在线观看 | 色五月激情五月 | 久久99精品久久久久久 | 国产99久久精品一区二区永久免费 | 男女免费视频网站 | 国产成人精品一区二区三区在线 | 国产一级片免费在线观看 | 日韩久草 | 视频一区二区在线 | 久久国色 | 国产黑丝av| 国产欧美日韩在线 | 国产成人精品亚洲日本在线观看 | 欧美一区二区视频 | 亚洲综合大片69999 | 91久久久久 | 97伦理影院 | 成人免费小视频 | 亚洲视频在线观看 | 欧美片网站免费 | 国产精品一区二区三区在线 | 亚洲国产欧美一区 | 久久y| 日韩成人在线观看 | 日韩中文字幕免费 |