goroutine,channel

来源:https://www.cnblogs.com/itbsl/archive/2019/09/03/11456432.html
-Advertisement-
Play Games

Go語言中有個概念叫做goroutine, 這類似我們熟知的線程,但是更輕。 以下的程式,我們串列地去執行兩次loop函數: go package main import "fmt" func main() { loop() loop() } func loop() { for i := 0; i ...


Go語言中有個概念叫做goroutine, 這類似我們熟知的線程,但是更輕。
以下的程式,我們串列地去執行兩次loop函數:

package main

import "fmt"

func main() {
    loop()
    loop()
}

func loop() {
    for i := 0; i < 10; i++ {
        fmt.Printf("%d ", i)
    }
}

毫無疑問,輸出會是這樣的:

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 

下麵我們把一個loop放在一個goroutine里跑,我們可以使用關鍵字go來定義並啟動一個goroutine,多次運行:

0 1 2 3 4 5 6 7 8 9 
//或有可能是下麵這樣
0 1 2 3 4 5 6 7 8 9 0 1 2 
//亦或是下麵這樣
0 1 2 3 4 5 6 7 8 9 0 

我們反覆運行上面的代碼會發現結果會類似於上面這樣,但是就是無法完整輸出兩遍0~9,明明我們主線跑了一趟,也開了一個goroutine來跑一趟啊。

原來,在goroutine還沒來得及跑loop的時候,主函數已經退出了,正所謂"皮(主線程)之不存,毛(子線程)將焉附"。

main函數退出地太快了,我們要想辦法阻止它過早地退出,一個辦法是讓main等待一下:

package main

import (
    "fmt"
    "time"
)

func main() {
    go loop() //啟動一個goroutine
    loop()
    time.Sleep(time.Second) //停頓一秒
}

func loop() {
    for i := 0; i < 10; i++ {
        fmt.Printf("%d ", i)
    }
}

這次確實輸出了兩趟,目的達到了。
可是採用等待的辦法並不好,如果goroutine在結束的時候,告訴下主線說“Hey, 我要跑完了!”就好了, 即所謂阻塞主線的辦法,回憶下我們Python裡面等待所有線程執行完畢的寫法:

for thread in threads:
    thread.join()

是的,我們也需要一個類似join的東西來阻塞住主線。那就是通道

通道

通道是什麼?簡單說,是goroutine之間互相通訊的東西。類似我們Unix上的管道(可以在進程間傳遞消息), 用來goroutine之間發消息和接收消息。其實,就是在做goroutine之間的記憶體共用。
使用make來建立一個通道:

var channel chan int = make(chan int)

那如何向通道存消息和取消息呢? 一個例子:

package main

import "fmt"

func main() {
    var messages = make(chan string)
    go func(message string) {
        messages <- message //存消息
    }("Ping!")

    fmt.Println(<-messages) //取消息
}

預設的,通道的存消息和取消息都是阻塞的 (叫做無緩衝的通道,不過緩衝這個概念稍後瞭解,先說阻塞的問題)。

也就是說, 無緩衝的通道在取消息和存消息的時候都會掛起當前的goroutine,除非另一端已經準備好。
比如以下的main函數和foo函數:

package main

var ch = make(chan int)

func foo() {
    ch <- 0 //向ch中加數據,如果沒有其他goroutine來取走這個數據,那麼掛起foo, 直到main函數把0這個數據拿走
}
func main() {
    go foo()
    <-ch //從ch取數據,如果ch中還沒放數據,那就掛起main線,直到foo函數中放數據為止
}

那既然通道可以阻塞當前的goroutine, 那麼回到上一部分「goroutine」所遇到的問題「如何讓goroutine告訴主線我執行完畢了」 的問題來, 使用一個通道來告訴主線即可:

package main

import "fmt"

var ch = make(chan int)

func loop() {
    for i := 0; i < 10; i++ {
        fmt.Printf("%d ", i)
    }
    ch <- 0 //執行完畢了,發個消息
}

func main() {

    go loop()
    <- ch //main在此阻塞住,直到線程跑完, 取到消息.
}

如果不用通道來阻塞主線的話,主線就會過早跑完,loop線都沒有機會執行

其實,無緩衝的通道永遠不會存儲數據,只負責數據的流通,為什麼這麼講呢?

  • 從無緩衝通道取數據,必須要有數據流進來才可以,否則當前線阻塞
  • 數據流入無緩衝通道, 如果沒有其他goroutine來拿走這個數據,那麼當前線阻塞

所以,你可以測試下,無論如何,我們測試到的無緩衝通道的大小都是0 (len(channel))
如果通道正有數據在流動,我們還要加入數據,或者通道乾澀,我們一直向無數據流入的空通道取數據呢? 就會引起死鎖
死鎖一個死鎖的例子:

package main

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

    <-ch //阻塞main goroutine,通道ch被鎖
}

執行這個程式你會看到Go報這樣的錯誤:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
    /Users/XXX/Go/src/main.go:6 +0x4d

Process finished with exit code 2

何謂死鎖? 操作系統有講過的,所有的線程或進程都在等待資源的釋放。如上的程式中, 只有一個goroutine, 所以當你向裡面加數據或者存數據的話,都會鎖死通道, 並且阻塞當前 goroutine, 也就是所有的goroutine(其實就main線一個)都在等待通道的開放(沒人拿走數據通道是不會開放的),也就是死鎖咯。
我發現死鎖是一個很有意思的話題,這裡有幾個死鎖的例子:
1.只在單一的goroutine里操作無緩衝通道,一定死鎖。比如你只在main函數里操作通道:

package main

import "fmt"

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

    ch <- 1 //1流入通道,堵塞當前線, 沒人取走數據通道不會打開
    fmt.Println("This line code won`t run") //在此行執行之前Go就會報死鎖
}

2.如下也是一個死鎖的例子:

package main

import "fmt"

var ch1 = make(chan int)
var ch2 = make(chan int)

func say(s string) {
    fmt.Println(s)
    ch1 <- <- ch2 //ch1等待ch2流出的數據
}

func main() {
    go say("hello")
    <-ch1 //堵塞主線
}

其中主線等ch1中的數據流出,ch1等ch2的數據流出,但是ch2等待數據流入,兩個goroutine都在等,也就是死鎖。

3.其實,總結來看,為什麼會死鎖?非緩衝通道上如果發生了流入無流出,或者流出無流入,也就導致了死鎖。或者這樣理解 Go啟動的所有goroutine里的非緩衝通道一定要一個線里存數據,一個線里取數據,要成對才行 。所以下麵的示例一定死鎖:

package main

func main() {
    c, quit := make(chan int), make(chan int)

    go func() {
        c <- 1 //c通道的數據沒有被其他goroutine讀取走,堵塞當前goroutine
        quit <- 0 //quit始終沒有辦法寫入數據
    }()
    
    <- quit //quit等待數據的寫
}

仔細分析的話,是由於:主線等待quit通道的數據流出,quit等待數據寫入,而func被c通道堵塞,所有goroutine都在等,所以死鎖。
簡單來看的話,一共兩個線,func線中流入c通道的數據並沒有在main線中流出,肯定死鎖。

但是,是否果真 所有不成對向通道存取數據的情況都是死鎖?

如下是個反例:

package main

func main() {

    c := make(chan int)

    go func() {
        c <- 1
    }()
}

程式正常退出了,很簡單,並不是我們那個總結不起作用了,還是因為一個讓人很囧的原因,main又沒等待其它goroutine,自己先跑完了, 所以沒有數據流入c通道,一共執行了一個goroutine, 並且沒有發生阻塞,所以沒有死鎖錯誤。

那麼死鎖的解決辦法呢?
最簡單的,把沒取走的數據取走,沒放入的數據放入, 因為無緩衝通道不能承載數據,那麼就趕緊拿走!
具體來講,就死鎖例子3中的情況,可以這麼避免死鎖:

package main

func main() {
    c, quit := make(chan int), make(chan int)

    go func() {
        c <- 1 //c通道的數據沒有被其他goroutine讀取走,堵塞當前goroutine
        quit <- 0 //quit始終沒有辦法寫入數據
    }()
    
    <- c    //取走c的數據
    <- quit //quit等待數據的寫
}

另一個解決辦法是緩衝通道, 即設置c有一個數據的緩衝大小:

c := make(chan int, 1)

這樣的話,c可以緩存一個數據。也就是說,放入一個數據,c並不會掛起當前線, 再放一個才會掛起當前線直到第一個數據被其他goroutine取走, 也就是只阻塞在容量一定的時候,不達容量不阻塞。

無緩衝通道的數據進出順序我們已經知道,無緩衝通道從不存儲數據,流入的數據必須要流出才可以。
觀察以下的程式:

package main

import "fmt"

var ch chan int = make(chan int)

func foo(id int) {
    ch <- id
}
func main() {

    //開啟5個routine
    for i := 0; i < 5; i++ {
        go foo(i)
    }

    //取出通道中的數據
    for i := 0; i < 5; i++ {
        fmt.Print(<- ch)
    }
}

們開了5個goroutine,然後又依次取數據。其實整個的執行過程細分的話,5個線的數據 依次流過通道ch, main列印之, 而巨集觀上我們看到的即 無緩衝通道的數據是先到先出,但是 無緩衝通道並不存儲數據,只負責數據的流通

緩衝通道

終於到了這個話題了, 其實緩存通道用英文來講更為達意: buffered channel.

緩衝這個詞意思是,緩衝通道不僅可以流通數據,還可以緩存數據。它是有容量的,存入一個數據的話 , 可以先放在通道里,不必阻塞當前線而等待該數據取走。

當緩衝通道達到滿的狀態的時候,就會表現出阻塞了,因為這時再也不能承載更多的數據了,「你們必須把 數據拿走,才可以流入數據」

在聲明一個通道的時候,我們給make以第二個參數來指明它的容量(預設為0,即無緩衝):

var ch chan int = make(chan int, 2) // 寫入2個元素都不會阻塞當前goroutine, 存儲個數達到2的時候會阻塞

如下的例子,緩衝通道ch可以無緩衝的流入3個元素:

package main

func main() {
    ch := make(chan int, 3)
    ch <- 1
    ch <- 2
    ch <- 3   
}

如果你再試圖流入一個數據的話,通道ch會阻塞main線, 報死鎖。
也就是說,緩衝通道會在滿容量的時候加鎖。

其實,緩衝通道是先進先出的,我們可以把緩衝通道看作為一個線程安全的隊列:

package main

import "fmt"

func main() {

    ch := make(chan int, 3)
    ch <- 1
    ch <- 2
    ch <- 3

    fmt.Println(<-ch) //1
    fmt.Println(<-ch) //2
    fmt.Println(<-ch) //3
}

通道數據讀取和通道關閉你也許發現,上面的代碼一個一個地去讀取通道簡直太費事了,Go語言允許我們使用range來讀取通道:

package main

import "fmt"

func main() {

    ch := make(chan int, 3)
    ch <- 1
    ch <- 2
    ch <- 3

    for v := range ch {
        fmt.Println(v)
    }
}

如果你執行了上面的代碼,會報死鎖錯誤的,原因是range不等到通道關閉是不會結束讀取的。也就是如果 緩衝通道乾涸了,那麼range就會阻塞當前goroutine, 所以死鎖咯。

那麼,我們試著避免這種情況,比較容易想到的是讀到通道為空的時候就結束讀取:

package main

import "fmt"

func main() {

    ch := make(chan int, 3)
    ch <- 1
    ch <- 2
    ch <- 3

    for v := range ch {
        fmt.Println(v)
        if len(ch) <= 0 { //如果現在數據量為0,跳出迴圈
            break
        }
    }
}

以上的方法是可以正常輸出的,但是註意檢查通道大小的方法不能在通道存取都在發生的時候用於取出所有數據,這個例子 是因為我們只在ch中存了數據,現在一個一個往外取,通道大小是遞減的。

另一個方式是顯式地關閉通道:

package main

import "fmt"

func main() {

    ch := make(chan int, 3)
    ch <- 1
    ch <- 2
    ch <- 3

    //顯式地關閉通道
    close(ch)

    for v := range ch {
        fmt.Println(v)
    }
}

被關閉的通道會禁止數據流入, 是只讀的。我們仍然可以從關閉的通道中取出數據,但是不能再寫入數據了。

等待多gorountine的方案那好,我們回到最初的一個問題,使用通道堵塞主線,等待開出去的所有goroutine跑完。

這是一個模型,開出很多小goroutine, 它們各自跑各自的,最後跑完了向主線報告。

我們討論如下2版本的方案:

  1. 只使用單個無緩衝通道阻塞主線
  2. 使用容量為goroutines數量的緩衝通道

對於方案1, 示例的代碼大概會是這個樣子:

package main

import "fmt"

var quit chan int //只開一個通道

func foo(id int) {
    fmt.Println(id)
    quit <- 0 //ok,finished
}

func main() {

    count := 1000
    quit = make(chan int) //無緩衝

    for i := 0; i < count; i++ {
        go foo(i)
    }

    for i := 0; i < count; i++ {
        <- quit
    }
}

對於方案2,把通道換成1000的:

quit = make(chan int, count) // 容量1000

其實區別僅僅在於一個是緩衝的,一個是非緩衝的。
對於這個場景而言,兩者都能完成任務, 都是可以的。

  • 無緩衝的通道是一批數據一個一個的「流進流出」
  • 緩衝通道則是一個一個存儲,然後一起流出去

轉載:
Go編程基礎—併發


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、方法 1.方法的基礎語法 (1)例子 上面的程式雖然實現了功能,但是程式的復用性很差,因此我們需要引入方法的概念(其實就是python中的函數功能) 我們編寫方法來重構上面的代碼 (2)方法的本質是什麼: 方法就是一段代碼片段,並且這段代碼片段可以完成某個特定的功能,並且可以被重覆利用。 (3) ...
  • 遞歸:方法定義中調用方法本身 如果是方法的嵌套調用 這不是遞歸基本思想就是“自己調用自己” 一個使用遞歸技術的方法將會直接或者間接的調用自己 幾個應註意的點: 遞歸一定要有出口否則就是死遞歸 遞歸的次數不能太多 否則就記憶體溢出 構造方法不能遞歸使用 例: 遞歸求n的階乘 有一對兔子,從出生後第3個月 ...
  • 本文完整代碼地址: "https://github.com/yu linfeng/BlogRepositories/tree/master/repositories/factorybean" 和 由於在命名上極其相似,一直以來困擾了不少的開發者。 ,耳熟能詳的Spring核心介面,提供IoC容器的最 ...
  • package com.atguigu;public class BubbleSortTest { public static void main(String[] args) { int[] arr=new int[]{43,32,76,-98,0,64,33,-21,32,99}; //冒泡排序 ...
  • package com.atguigu;public class Main { public static void main(String[] args) { //二分法查找 //前提:所查找的數組必須有序 int[] arr=new int[]{-98,-34,2,34,54,66,79,105 ...
  • package com.atguigu;public class Main { public static void main(String[] args) { String[] arr=new String[]{"JJ","DD","MM","BB","GG","AA"}; //線性查找 Stri ...
  • ArrayList有三種初始化方式: 1.指定大小初始化 public ArrayList(int initialCapacity) 2.傳入一個Collection對象初始化,並將對象中的數據添加到ArrayList中 public ArrayList(Collection<? extends E ...
  • package com.atguigu;public class Fanzhuan { public static void main(String[] args) { //數組的反轉 //方法一 String[] arr=new String[]{"JJ","DD","MM","BB","GG", ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...