Go并發編程之Channel和定時器

系統文件介紹

在程序啟動運行時,自動打開,運行結束,自動關閉。

  • 鍵盤(硬件)—— 標準輸入(文件)stdin —— 0
  • 屏幕(硬件)—— 標準輸出(文件)stdout —— 1
  • 屏幕(硬件)—— 標準錯誤(文件)stderr —— 2

channel

1. channel簡介

channel是Go語言中的一個核心類型,可以把它看成管道。并發核心單元通過它就可以發送或者接收數據進行通訊,這在一定程度上又進一步降低了編程的難度。

channel是一個數據類型,主要用來解決協程的同步問題以及協程之間數據共享(數據傳遞)的問題。

goroutine運行在相同的地址空間,因此訪問共享內存必須做好同步。goroutine 奉行通過通信來共享內存,而不是共享內存來通信

引?類型 channel可用于多個 goroutine 通訊。其內部實現了同步,確保并發安全。

2. 定義channel變量

和map類似,channel也一個對應make創建的底層數據結構的引用。

當我們復制一個channel或用于函數參數傳遞時,我們只是拷貝了一個channel引用,因此調用者和被調用者將引用同一個channel對象。和其它的引用類型一樣,channel的零值也是nil。

定義一個channel時,也需要定義發送到channel的值的類型。channel可以使用內置的make()函數來創建:

make(chan Type)  //等價于make(chan Type, 0)
make(chan Type, capacity)

chan是創建channel所需使用的關鍵字。Type 代表指定channel收發數據的類型。

當參數capacity= 0 時,channel 是無緩沖阻塞讀寫的;當capacity > 0 時,channel 有緩沖、是非阻塞的,直到寫滿 capacity個元素才阻塞寫入。

channel非常像生活中的管道,一邊可以存放東西,另一邊可以取出東西。channel通過操作符<-來接收和發送數據,發送和接收數據語法:

channel <- value        //發送value到channel
<-channel               //接收并將其丟棄
x := <-channel          //從channel中接收數據,并賦值給x
x, ok := <-channel      //功能同上,同時檢查通道是否已關閉或者是否為空

默認情況下,channel接收和發送數據都是阻塞的,除非另一端已經準備好,這樣就使得goroutine同步變的更加的簡單,而不需要顯式的lock

示例代碼:

package main

import (
    "fmt"
)

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

    go func() {
        defer fmt.Println("子協程結束")

        fmt.Println("子協程正在運行……")

        c <- 666 //666發送到c
    }()

    num := <-c //從c中接收數據,并賦值給num

    fmt.Println("num = ", num)
    fmt.Println("main協程結束")
}

3. 無緩沖的channel

無緩沖的通道(unbuffered channel)是指在接收前沒有能力保存任何值的通道

這種類型的通道要求發送goroutine和接收goroutine同時準備好,才能完成發送和接收操作。否則,通道會導致先執行發送或接收操作的 goroutine 阻塞等待。

這種對通道進行發送和接收的交互行為本身就是同步的。其中任意一個操作都無法離開另一個操作單獨存在。

阻塞:由于某種原因數據沒有到達,當前協程(線程)持續處于等待狀態,直到條件滿足,才接觸阻塞。

同步:在兩個或多個協程(線程)間,保持數據內容一致性的機制。

無緩沖的channel創建格式:

make(chan Type)   //等價于make(chan Type, 0)

示例代碼:

package main

import (
    "fmt"
    "time"
)

func main() {
    c := make(chan int, 0) //創建無緩沖的通道 c 

    //內置函數 len 返回未被讀取的緩沖元素數量,cap 返回緩沖區大小
    fmt.Printf("len(c)=%d, cap(c)=%d\n", len(c), cap(c))

    go func() {
        defer fmt.Println("子協程結束")

        for i := 0; i < 3; i++ {
            c <- i
            fmt.Printf("子協程正在運行[%d]: len(c)=%d, cap(c)=%d\n", i, len(c), cap(c))
        }
    }()

    time.Sleep(2 * time.Second) //延時2s

    for i := 0; i < 3; i++ {
        num := <-c //從c中接收數據,并賦值給num
        fmt.Println("num = ", num)
    }

    fmt.Println("main協程結束")
}

4. 有緩沖的channel

有緩沖的通道(buffered channel)是一種在被接收前能存儲一個或者多個數據值的通道。

這種類型的通道并不強制要求 goroutine 之間必須同時完成發送和接收。通道會阻塞發送和接收動作的條件也不同。

只有通道中沒有要接收的值時,接收動作才會阻塞。

只有通道沒有可用緩沖區容納被發送的值時,發送動作才會阻塞。

這導致有緩沖的通道和無緩沖的通道之間的一個很大的不同:無緩沖的通道保證進行發送和接收的 goroutine
會在同一時間進行數據交換;有緩沖的通道沒有這種保證。

有緩沖的channel創建格式:

make(chan Type, capacity)

如果給定了一個緩沖區容量,通道就是異步的。只要緩沖區有未使用空間用于發送數據,或還包含可以接收的數據,那么其通信就會無阻塞地進行。

示例代碼:

func main() {
    c := make(chan int, 3) //帶緩沖的通道

    //內置函數 len 返回未被讀取的緩沖元素數量, cap 返回緩沖區大小
    fmt.Printf("len(c)=%d, cap(c)=%d\n", len(c), cap(c))

    go func() {
        defer fmt.Println("子協程結束")

        for i := 0; i < 3; i++ {
            c <- i
            fmt.Printf("子協程正在運行[%d]: len(c)=%d, cap(c)=%d\n", i, len(c), cap(c))
        }
    }()

    time.Sleep(2 * time.Second) //延時2s
    for i := 0; i < 3; i++ {
        num := <-c //從c中接收數據,并賦值給num
        fmt.Println("num = ", num)
    }
    fmt.Println("main協程結束")
}

5. 關閉channel

如果發送者知道,沒有更多的值需要發送到channel的話,那么讓接收者也能及時知道沒有多余的值可接收將是有用的,因為接收者可以停止不必要的接收等待。這可以通過內置的close函數來關閉channel實現。

示例代碼:

package main

import (
    "fmt"
)

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

    go func() {
        for i := 0; i < 5; i++ {
            c <- i
        }
        //把 close(c) 注釋掉,程序會一直阻塞在 if data, ok := <-c; ok 那一行
        close(c)
    }()

    for {
        //ok為true說明channel沒有關閉,為false說明管道已經關閉
        if data, ok := <-c; ok {
            fmt.Println(data)
        } else {
            break
        }
    }

    fmt.Println("Finished")
}

注意:

  • channel不像文件一樣需要經常去關閉,只有當你確實沒有任何發送數據了,或者你想顯式的結束range循環之類的,才去關閉channel;
  • 關閉channel后,無法向channel 再發送數據(引發 panic 錯誤后導致接收立即返回零值);
panic: send on closed channel
  • 關閉channel后,可以繼續從channel接收數據;
  • 對于nil channel,無論收發都會被阻塞。

可以使用 range 來迭代不斷操作channel:

package main

import (
    "fmt"
)

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

    go func() {
        for i := 0; i < 5; i++ {
            c <- i
        }
        //把 close(c) 注釋掉,程序會一直阻塞在 for data := range c 那一行
        close(c)
    }()

    for data := range c {
        fmt.Println(data)
    }
    fmt.Println("Finished")
}

6. 單向channel及應用

默認情況下,通道channel是雙向的,也就是,既可以往里面發送數據也可以同里面接收數據。

但是,我們經常見一個通道作為參數進行傳遞而值希望對方是單向使用的,要么只讓它發送數據,要么只讓它接收數據,這時候我們可以指定通道的方向。

單向channel變量的聲明非常簡單,如下:

var ch1 chan int       // ch1是一個正常的channel,是雙向的
var ch2 chan<- float64 // ch2是單向channel,只用于寫float64數據
var ch3 <-chan int     // ch3是單向channel,只用于讀int數據
  • chan<- 表示數據進入管道,要把數據寫進管道,對于調用者就是輸出。
  • <-chan 表示數據從管道出來,對于調用者就是得到管道的數據,當然就是輸入。

可以將 channel 隱式轉換為單向隊列,只收或只發,不能將單向 channel 轉換為普通 channel

c := make(chan int, 3)
    var send chan<- int = c // send-only
    var recv <-chan int = c // receive-only
    send <- 1
    //<-send //invalid operation: <-send (receive from send-only type chan<- int)
    <-recv
    //recv <- 2 //invalid operation: recv <- 2 (send to receive-only type <-chan int)

    //不能將單向 channel 轉換為普通 channel
    d1 := (chan int)(send) //cannot convert send (type chan<- int) to type chan int
    d2 := (chan int)(recv) //cannot convert recv (type <-chan int) to type chan int

示例代碼:

// chan<- //只寫
func counter(out chan<- int) {
    defer close(out)
    for i := 0; i < 5; i++ {
        out <- i //如果對方不讀 會阻塞
    }
}

// <-chan //只讀
func printer(in <-chan int) {
    for num := range in {
        fmt.Println(num)
    }
}

func main() {
    c := make(chan int) //   chan   //讀寫

    go counter(c) //生產者
    printer(c)    //消費者

    fmt.Println("done")
}

7. 生產者消費者模型

單向channel最典型的應用是“生產者消費者模型”。

所謂“生產者消費者模型”:

某個模塊(函數等)負責產生數據,這些數據由另一個模塊來負責處理(此處的模塊是廣義的,可以是類、函數、協程、線程、進程等)。產生數據的模塊,就形象地稱為生產者;而處理數據的模塊,就稱為消費者。

單單抽象出生產者和消費者,還夠不上是生產者/消費者模型。該模式還需要有一個緩沖區處于生產者和消費者之間,作為一個中介。生產者把數據放入緩沖區,而消費者從緩沖區取出數據。

那么,這個緩沖區有什么用呢?為什么不讓生產者直接調用消費者的某個函數,直接把數據傳遞過去,而畫蛇添足般的設置一個緩沖區呢?

緩沖區的好處大概如下:

1. 解耦: 生產者和消費者之間降低耦合度,任意一方修改,不會直接影響對端。

假設生產者和消費者分別是兩個類。如果讓生產者直接調用消費者的某個方法,那么生產者對于消費者就會產生依賴(也就是耦合)。將來如果消費者的代碼發生變化,可能會直接影響到生產者。而如果兩者都依賴于某個緩沖區,兩者之間不直接依賴,耦合度也就相應降低了。

2. 處理并發: 借助異步通信機制,在生產者、消費者等多個go程間實現并行通信。

生產者直接調用消費者的某個方法,還有另一個弊端。由于函數調用是同步的(或者叫阻塞的),在消費者的方法沒有返回之前,生產者只好一直等在那邊。萬一消費者處理數據很慢,生產者只能無端浪費時間。

使用了生產者/消費者模式之后,生產者和消費者可以是兩個獨立的并發主體。生產者把制造出來的數據往緩沖區一丟,就可以再去生產下一個數據。基本上不用依賴消費者的處理速度。

其實最當初這個生產者消費者模式,主要就是用來處理并發問題的。

3. 緩存: 借助緩沖區,緩存數據,從而提高生產者、消費者效率。

如果生產者制造數據的速度時快時慢,緩沖區的好處就體現出來了。當數據制造快的時候,消費者來不及處理,未處理的數據可以暫時存在緩沖區中。等生產者的制造速度慢下來,消費者再慢慢處理掉。

示例代碼:

package main

import "fmt"

// 此通道只能寫,不能讀。
func producer(out chan<- int) {
    for i := 0; i < 10; i++ {
        out <- i * i // 將 i*i 結果寫入到只寫channel
    }
    close(out)
}

// 此通道只能讀,不能寫
func consumer(in <-chan int) {
    for num := range in { // 從只讀channel中獲取數據
        fmt.Println("num =", num)
    }
}

func main() {
    ch := make(chan int) // 創建一個雙向channel

    // 新建一個groutine, 模擬生產者,產生數據,寫入 channel
    go producer(ch) // channel傳參, 傳遞的是引用。
    
    // 主協程,模擬消費者,從channel讀數據,打印到屏幕
    consumer(ch) // 與 producer 傳遞的是同一個 channel
}

簡單說明:首先創建一個雙向的channel,然后開啟一個新的goroutine,把雙向通道作為參數傳遞到producer方法中,同時轉成只寫通道。子協程開始執行循環,向只寫通道中添加數據,這就是生產者。主協程,直接調用consumer方法,該方法將雙向通道轉成只讀通道,通過循環每次從通道中讀取數據,這就是消費者。

注意:channel作為參數傳遞,是引用傳遞

定時器

1. time.Timer

Timer是一個定時器。代表未來的一個單一事件,你可以告訴timer你要等待多長時間。

type Timer struct {   
      C <-chan Time  
      r runtimeTimer
}

它提供一個channel,在定時時間到達之前,沒有數據寫入timer.C會一直阻塞。直到定時時間到,向channel寫入值,阻塞解除,可以從中讀取數據。

示例代碼:

package main

import (
    "fmt"
    "time"
)

func main() {
    //創建定時器,2秒后,定時器就會向自己的C字節發送一個time.Time類型的元素值
    timer1 := time.NewTimer(time.Second * 2)
    t1 := time.Now() //當前時間
    fmt.Printf("t1: %v\n", t1)

    t2 := <-timer1.C
    fmt.Printf("t2: %v\n", t2)

    //如果只是想單純的等待的話,可以使用 time.Sleep 來實現
    timer2 := time.NewTimer(time.Second * 2)
    <-timer2.C
    fmt.Println("2s后")

    time.Sleep(time.Second * 2)
    fmt.Println("再一次2s后")

    <-time.After(time.Second * 2)
    fmt.Println("再再一次2s后")

    timer3 := time.NewTimer(time.Second)
    go func() {
        <-timer3.C
        fmt.Println("Timer 3 expired")
    }()

    stop := timer3.Stop() //停止定時器
    if stop {
        fmt.Println("Timer 3 stopped")
    }

    fmt.Println("before")
    timer4 := time.NewTimer(time.Second * 5)    //原來設置3s
    timer4.Reset(time.Second * 1)               //重新設置時間
    <-timer4.C
    fmt.Println("after")
}

定時器的常用操作:

  • 實現延遲功能
<-time.After(2 * time.Second) //定時2s,阻塞2s,2s后產生一個事件,往channel寫內容
fmt.Println("時間到")
time.Sleep(2 * time.Second)
fmt.Println("時間到")
timer := time.NewTimer(2 * time.Second)
<- timer.C
fmt.Println("時間到")
  • 定時器停止
  timer := time.NewTimer(3 * time.Second)
    go func() {
        <-timer.C
        fmt.Println("子協程可以打印了,因為定時器的時間到")
    }()
    timer.Stop() //停止定時器

    for {
    }
  • 定時器重置
    timer := time.NewTimer(3 * time.Second)
    ok := timer.Reset(1 * time.Second) //重新設置為1s
    fmt.Println("ok = ", ok)
    <-timer.C
    fmt.Println("時間到")

2. time.Ticker

Ticker是一個周期觸發定時的計時器,它會按照一個時間間隔往channel發送系統當前時間,而channel的接收者可以以固定的時間間隔從channel中讀取事件。

type Ticker struct {   
      C <-chan Time     // The channel on which the ticks are delivered.   
      r runtimeTimer
 }

示例代碼:

package main

import (
    "fmt"
    "time"
)

func main() {
    //創建定時器,每隔1秒后,定時器就會給channel發送一個事件(當前時間)
    ticker := time.NewTicker(time.Second * 1)

    i := 0
    go func() {
        for { //循環
            <-ticker.C
            i++
            fmt.Println("i = ", i)

            if i == 5 {
                ticker.Stop() //停止定時器
            }
        }
    }() //別忘了()

    //死循環,特地不讓main goroutine結束
    for {
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,963評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,348評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,083評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,706評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,442評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,802評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,795評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,983評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,542評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,287評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,486評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,030評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,710評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,116評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,412評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,224評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,462評論 2 378