Golang 并發(fā)之三 ( go channel 和 gorountine)

Channel 是什么?

channel,通道,本質(zhì)上是一個(gè)通信對(duì)象,goroutine 之間可以使用它來(lái)通信。從技術(shù)上講,通道是一個(gè)數(shù)據(jù)傳輸管道,可以向通道寫(xiě)入或從中讀取數(shù)據(jù)。

定義一個(gè)channel

Go 規(guī)定: 使用 chan 關(guān)鍵字來(lái)創(chuàng)建通道,使用make關(guān)鍵字初始化通道,一個(gè)通道只能傳輸一種類(lèi)型的數(shù)據(jù)。

零值通道

上面的程序定義一個(gè)通道c變量,它可以傳輸類(lèi)型為int的數(shù)據(jù)。上面的程序打印 <nil>, 是因?yàn)橥ǖ赖牧阒凳?nil。但是 nil 通道不能傳輸數(shù)據(jù)。因此,我們必須使用 make 函數(shù)來(lái)創(chuàng)建一個(gè)可用的通道。

make創(chuàng)建可用通道

我們使用簡(jiǎn)寫(xiě)語(yǔ)法 :=make 函數(shù)創(chuàng)建通道。上述程序產(chǎn)生以下結(jié)果:

type of `c` is chan int
value of `c` is 0xc0420160c0

注意通道的值 c,看起來(lái)它是一個(gè)內(nèi)存地址。

通道是指針類(lèi)型。某些場(chǎng)景下,goroutine 之間通信時(shí),會(huì)將通道作為參數(shù)傳遞給函數(shù)或方法,當(dāng)程序接收該通道作為參數(shù)時(shí),無(wú)需取消引用它即可從該通道推送或拉取數(shù)據(jù)。

通道數(shù)據(jù)讀寫(xiě)

Go規(guī)定:使用左箭頭語(yǔ)法 <- 從通道讀取和寫(xiě)入數(shù)據(jù)。

  • 寫(xiě)入數(shù)據(jù)
c <- data

上面的示例表示:將data發(fā)送到通道 c 中。看箭頭的方向,它從data指向c, 因此可以想象”我們正在嘗試將data推送到 c”。

  • 讀取數(shù)據(jù)
<- c

上述示例表示:從通道 c 讀取數(shù)據(jù)。看箭頭方向,從 c 通道開(kāi)始。

該語(yǔ)句不會(huì)將數(shù)據(jù)推存到任何內(nèi)容中,但它仍然是一個(gè)有效的語(yǔ)句。

如果您有一個(gè)變量data,可以保存來(lái)自通道的數(shù)據(jù),則可以使用以下語(yǔ)法:

var data int
data = <- c

現(xiàn)在來(lái)自通道 c 的 int 類(lèi)型的數(shù)據(jù)可以存儲(chǔ)到變量data中, data 必須也是int類(lèi)型。

上面的語(yǔ)法可以使用簡(jiǎn)寫(xiě)語(yǔ)法重寫(xiě),如下所示

data := <- c

golang 自動(dòng)識(shí)別c中的數(shù)據(jù)類(lèi)型,并將data設(shè)置為同樣的類(lèi)型

以上所有通道操作都是阻塞的 在上一課中,我們使用 time.Sleep 阻塞了goroutine,從而調(diào)度了其它的goroutine。 而通道操作本質(zhì)上也是阻塞的,當(dāng)向通道寫(xiě)入數(shù)據(jù)時(shí),當(dāng)前goroutine 會(huì)被阻塞,直到其它goroutine 從該通道讀取數(shù)據(jù)。我們?cè)诓l(fā)章節(jié)中看到,當(dāng)前的goroutine阻塞后, 調(diào)度器會(huì)調(diào)度其它空閑的goroutine繼續(xù)工作,從而保證程序不會(huì)永遠(yuǎn)阻塞,這是用channel來(lái)做的。通道的這個(gè)特性在 goroutines 通信中非常有用,因?yàn)樗梢苑乐刮覀兙帉?xiě)手動(dòng)鎖和 hack 來(lái)使它們彼此協(xié)同工作。

channel 實(shí)踐

goroutine 和 channel

下面我們一步一步的講講解上面程序的執(zhí)行過(guò)程:

  • 首先聲明了greet函數(shù),它接受字符串類(lèi)型的通道c。greeter這個(gè)函數(shù)從通道 c 讀取數(shù)據(jù),并打印到控制臺(tái)。
  • 在 main 函數(shù)中,第一條語(yǔ)句: 打印 "main started" 到控制臺(tái)
  • main函數(shù)第二條語(yǔ)句:使用 make 初始化字符串類(lèi)型的通道 c
  • main 函數(shù)第三條語(yǔ)句中: 將通道c 傳遞給 greet 函數(shù),但使用 go 關(guān)鍵字將其作為 goroutine 執(zhí)行。
  • 此時(shí),進(jìn)程有 2 個(gè) goroutine,而活動(dòng)goroutine 是 main goroutine(查看上一課就知道它是什么)。然后控制權(quán)轉(zhuǎn)到下一行代碼。
  • main 第四行語(yǔ)句:將字符串值 "John" 發(fā)送到通道 c。此時(shí),goroutine 被阻塞,直到某個(gè) goroutine 讀取它。 Go調(diào)度器調(diào)度greet goroutine,它按照第一點(diǎn)中提到的那樣執(zhí)行。
  • 然后 main goroutine 激活并執(zhí)行最后的語(yǔ)句,打印 "main stopped"。

deadlock

前面我們說(shuō)了“通道本質(zhì)上是阻塞的”,當(dāng)在通道寫(xiě)入或讀取數(shù)據(jù)時(shí),該 goroutine 會(huì)阻塞,而且會(huì)一直阻塞,控制權(quán)被傳遞給其他可用的 goroutine。如果沒(méi)有其他可用的 goroutine 怎么辦,程序無(wú)法向下執(zhí)行了,這就產(chǎn)生死鎖錯(cuò)誤,導(dǎo)致整個(gè)程序崩潰。

當(dāng)嘗試從某個(gè)通道讀取數(shù)據(jù),但通道沒(méi)有可用的值時(shí),會(huì)期望其它 goroutine 推送值到該通道, 而阻塞當(dāng)前goroutine,交出控制權(quán),因此,此讀取操作將是阻塞的。同樣,如果要向通道發(fā)送數(shù)據(jù),會(huì)交出控制權(quán)到其它goroutine,直到某個(gè) goroutine 從中讀取數(shù)據(jù)。因此,此發(fā)送操作將被阻塞。

死鎖的一個(gè)簡(jiǎn)單示例是只有主 goroutine 執(zhí)行一些通道操作。

死鎖

上面的程序?qū)⒃谶\(yùn)行時(shí)死鎖了, 拋出以下錯(cuò)誤:

main() started
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
        program.Go:10 +0xfd
exit status 2

fatal error: all goroutines are asleep - deadlock!。似乎所有 goroutine 都處于睡眠狀態(tài),或者根本沒(méi)有其他 goroutine 可用于調(diào)度。

關(guān)閉channel

一個(gè)通道可以關(guān)閉,這樣就不能再通過(guò)它發(fā)送數(shù)據(jù)了。接收器 goroutine 可以使用 val, ok := <- c 語(yǔ)法找出通道的狀態(tài),如果通道打開(kāi)或可以執(zhí)行讀取操作,則 ok 為真,如果通道關(guān)閉且無(wú)法執(zhí)行更多讀取操作,則為 false 。

可以使用 close(channel)close 內(nèi)置函數(shù)關(guān)閉通道。

讓我們看一個(gè)簡(jiǎn)單的例子。

關(guān)閉channel

只是為了幫助你理解阻塞的概念,首先發(fā)送操作 c <- "John" 是阻塞的,一些 goroutine 必須從通道讀取數(shù)據(jù),因此greet goroutine 是由 Go調(diào)度器調(diào)度的。然后第一次讀取操作 <-c 是非阻塞的,因?yàn)閿?shù)據(jù)存在于通道 c 中以供讀取。第二次讀取操作 <-c 將被阻塞,因?yàn)橥ǖ?c 沒(méi)有任何數(shù)據(jù)可供讀取,因此 Go 調(diào)度器激活 main goroutine 并且程序從 close(c) 函數(shù)開(kāi)始執(zhí)行。

從上面的錯(cuò)誤中,是由嘗試在關(guān)閉的通道上發(fā)送數(shù)據(jù)引起的。為了更好地理解關(guān)閉通道的可用性,讓我們看看 for 循環(huán)。

for loop

for{} 的無(wú)限循環(huán)語(yǔ)法可用于讀取通過(guò)通道發(fā)送的多個(gè)值。

for循環(huán)讀取channel

在上面的例子中,我們正在創(chuàng)建 goroutine squares,它一個(gè)一個(gè)地返回從 0 到 9 的數(shù)字的平方。在main goroutine 中,我們正在無(wú)限循環(huán)中讀取這些數(shù)字。

在無(wú)限 for 循環(huán)中,由于我們需要一個(gè)條件來(lái)在某個(gè)時(shí)刻中斷循環(huán),因此我們使用語(yǔ)法 val, ok := <-c 從通道讀取值。在這里,當(dāng)通道關(guān)閉時(shí),ok 會(huì)給我們額外的信息。因此,在 squares 協(xié)程中,在寫(xiě)完所有數(shù)據(jù)后,我們使用語(yǔ)法 close(c) 關(guān)閉通道。當(dāng) ok 為真時(shí),程序打印 val 中的值和通道狀態(tài) ok。當(dāng)它為假時(shí),我們使用 break 關(guān)鍵字跳出循環(huán)。因此,上述程序產(chǎn)生以下結(jié)果:

main() started
0 true
1 true
4 true
9 true
16 true
25 true
36 true
49 true
64 true
81 true
0 false <-- loop broke!
main() stopped

當(dāng)通道關(guān)閉時(shí),goroutine 讀取的值為通道數(shù)據(jù)類(lèi)型的零值。在這種情況下,由于 channel 正在傳輸 int 數(shù)據(jù)類(lèi)型,因此可以從結(jié)果中看到它是 0。與向通道讀取或?qū)懭胫挡煌P(guān)閉通道不會(huì)阻塞當(dāng)前的 goroutine。

為了避免手動(dòng)檢查通道關(guān)閉條件的痛苦,Go 提供了更簡(jiǎn)單的for range循環(huán),它會(huì)在通道關(guān)閉時(shí)自動(dòng)關(guān)閉。

讓我們修改我們之前的上述程序。

range讀取channel

在上面的程序中,我們使用了 for val := range c 而不是 for{}。 range 將一次從通道讀取一個(gè)值,直到它關(guān)閉。因此,上述程序產(chǎn)生以下結(jié)果

main() started
0
1
4
9
16
25
36
49
64
81
main() stopped

如果不關(guān)閉 for range 循環(huán)中的通道,程序?qū)⒃谶\(yùn)行時(shí)拋出死鎖致命錯(cuò)誤。所以,數(shù)據(jù)發(fā)送完成時(shí),要記得關(guān)閉通道;還可以使用select,default來(lái)避免這個(gè)問(wèn)題。

緩沖區(qū)通道和通道容量

正如我們所見(jiàn),每一個(gè)通道寫(xiě)入操作都會(huì)阻塞當(dāng)前goroutine。但是到目前位置,在創(chuàng)建channel時(shí),我們都沒(méi)有給make第二個(gè)參數(shù)。這第二個(gè)參數(shù)就是通道容量,或緩沖區(qū)。默認(rèn)為0的通道成為無(wú)緩沖區(qū)通道。

當(dāng)通道的緩沖區(qū)大于0時(shí),緩沖區(qū)被填滿之前, goroutine不會(huì)被阻塞。緩沖區(qū)滿后, 只有當(dāng)通道的最后數(shù)據(jù)被讀取,新的值才能被添加。有一個(gè)問(wèn)題是通道讀取是饑渴操作,這表示讀取一旦開(kāi)始,只要緩沖區(qū)有數(shù)據(jù), 讀取就會(huì)一直進(jìn)行。技術(shù)上來(lái)講,緩沖區(qū)為空時(shí),讀取操作才是阻塞的。

我們使用下面的語(yǔ)法定義帶緩沖的通道:

c := make(chan Type, n)

上面會(huì)創(chuàng)建一個(gè)數(shù)據(jù)類(lèi)型為 Type 且緩沖區(qū)大小為 n 的通道。直到 channel 收到 n+1 個(gè)發(fā)送操作,它才會(huì)阻塞當(dāng)前的 goroutine。

buffered channel

在上面的程序中,通道 c 的緩沖區(qū)容量為 3。這意味著它可以容納 3 個(gè)值,在第20行,由于緩沖區(qū)沒(méi)有溢出(因?yàn)槲覀儧](méi)有推送任何新值),main goroutine 不會(huì)阻塞,運(yùn)行后退出,并不會(huì)調(diào)度到squares goroutine。

讓我們?cè)诎l(fā)送一個(gè)額外的值:

buffer channel2

如我們前面討論, 通道 c <- 4 發(fā)送操作超出了channel的容量,阻塞了main goroutine, squares goroutine 獲得控制權(quán),并讀取通道內(nèi)的所有值。

一個(gè)通道的長(zhǎng)度和容量怎么計(jì)算呢

與切片類(lèi)似,緩沖通道具有長(zhǎng)度和容量。通道的長(zhǎng)度是通道緩沖區(qū)中值的數(shù)量,而通道的容量是緩沖區(qū)大小,創(chuàng)建時(shí)n的值。計(jì)算長(zhǎng)度,我們使用len函數(shù),而找出容量,我們使用cap函數(shù),就像切片一樣

length and capacity of channel

如果你想知道為什么上面的程序運(yùn)行良好并且沒(méi)有拋出死鎖錯(cuò)誤。這是因?yàn)椋捎谕ǖ廊萘繛?3 并且緩沖區(qū)中只有 2 個(gè)值可用,Go 沒(méi)有嘗試通過(guò)阻止主 goroutine 執(zhí)行來(lái)調(diào)度另一個(gè) goroutine。如果需要,您可以簡(jiǎn)單地在main goroutine 中讀取這些值,因?yàn)榧词咕彌_區(qū)未滿,也不會(huì)阻止您從通道讀取值。

另外一個(gè)例子:

example

使用多個(gè) goroutine

下面我們創(chuàng)建2個(gè)goroutines, 一個(gè)計(jì)算整數(shù)的平方, 一個(gè)計(jì)算整數(shù)的立方

https://play.golang.org/p/6wdhWYpRfrX

下面分析一下程序的執(zhí)行過(guò)程:

  1. 首先創(chuàng)建了兩個(gè)函數(shù), squarecube, 兩個(gè)函數(shù)都使用 c chan int channel 作為參數(shù), 函數(shù)從c中讀取整數(shù), 計(jì)算完成后,寫(xiě)回c
  2. 在main goroutinue 中,我們創(chuàng)建了兩個(gè)int 類(lèi)型的 channel : squareChancubeChan
  3. 使用go關(guān)鍵字, 以goroutine的方式 square 和 cube
  4. 此時(shí)控制權(quán)還在 main goroutine中, 我們個(gè)變量 testNum 一個(gè)值3
  5. 此時(shí)我們把testNum 發(fā)送到channel squareChancubeChan, main goroutine 將被阻塞,直到這些channel的數(shù)據(jù)被讀取。一旦chanel中的數(shù)據(jù)被讀取,main goroutine 將繼續(xù)執(zhí)行。
  6. 此時(shí)在main goroutine中, 嘗試從squareChancubeChan 讀取數(shù)據(jù), 這依然是阻塞操作, 直到這些channel在他們各自的goroutine被寫(xiě)入數(shù)據(jù), main gorounine 才能繼續(xù)執(zhí)行

上圖中程序的執(zhí)行結(jié)果如下:

[main] main() started
[main] sent testNum to squareChan
[square] reading
[main] resuming
[main] sent testNum to cubeChan
[cube] reading
[main] resuming
[main] reading from channels
[main] sum of square and cube of 3  is 36
[main] main() stopped

單向通道

至此, 我們所操作的channel都是雙向的, 既能讀取,也能寫(xiě)入。我們也可以創(chuàng)建單向操作的channel, 只讀channel:只能讀取數(shù)據(jù);只寫(xiě)chanel:只能寫(xiě)入數(shù)據(jù)。

單向chanel創(chuàng)建依然使用make 函數(shù),只是額外添加了單向箭頭(<-)語(yǔ)法:

roc := make(<-chan int)         // read-only chan
soc := make(chan<- int)         // send-only chan

在上面的程序中, roc是只讀channel, make函數(shù)中的箭頭方向是遠(yuǎn)離chan(<-chan); soc是只寫(xiě)channel, make函數(shù)中箭頭方向,指向chan(chan<-), 他們是兩個(gè)不同的類(lèi)型

https://play.golang.org/p/JZO51IoaMg8

但是單向信道有什么用呢?使用單向通道增加了程序的類(lèi)型安全性。可減少程序出錯(cuò)概率。

假如有如下場(chǎng)景: 假如你有一個(gè)goroutine, 你只需要在其中讀取channel中的數(shù)據(jù), 但是main goroutine需要在同一個(gè)channle讀取和寫(xiě)入數(shù)據(jù),該怎么做呢?

幸運(yùn)的是go 提供了簡(jiǎn)單的語(yǔ)法, 把雙向的channle,改為單向

https://play.golang.org/p/k3B3gCelrGv

如上述示例所示, 我們只需要在 greet 函數(shù)中, 將接收參數(shù)修改為單向channel即可,現(xiàn)在我們?cè)趃reet中,對(duì)channel的操作,只能讀了, 任何寫(xiě)造作都會(huì)導(dǎo)致 fatal 錯(cuò)誤 "invalid operation: roc <- "some text" (send to receive-only type <-chan string)"。

匿名goroutine

在 goroutines 章節(jié)中,我們學(xué)習(xí)了匿名 goroutines。我們也可以與他們一起實(shí)施渠道。讓我們修改前面的簡(jiǎn)單示例,在匿名 goroutine 中實(shí)現(xiàn) channel。

這是我們之前的例子

https://play.golang.org/p/c5erdHX1gwR

下面是一個(gè)修改后的例子,我們將 greet goroutine 變成了一個(gè)匿名 goroutine。

https://play.golang.org/p/cM5nFgRha7c

channel 作為 channel 的數(shù)據(jù)類(lèi)型

如標(biāo)題所示, channel 作為 golang中類(lèi)型中國(guó)的一等公民, 可以像其他值一樣在任何地方使用: 作為結(jié)構(gòu)的元素, 函數(shù)參數(shù),返回值,甚至是另外一個(gè)通道的類(lèi)型。下面的例子中,我們使用一個(gè)通道作為另外一個(gè)通道的數(shù)據(jù)類(lèi)型。

https://play.golang.org/p/xVQvvb8O4De

select

select 就像沒(méi)有任何輸入?yún)?shù)的 switch 一樣,但它只用于通道操作。 select 語(yǔ)句用于僅對(duì)多個(gè)通道中的一個(gè)執(zhí)行操作,由 case 塊有條件地選擇。

我們先看一個(gè)例子:

https://play.golang.org/p/ar5dZUQ2ArH

從上面的程序中,可以看到select語(yǔ)句就像switch一樣,但不是布爾操作,而是通道操作。 select 語(yǔ)句是阻塞的,除非它有default項(xiàng)。 一旦滿足條件之一, 它將解除阻塞。 那么它什么時(shí)候滿足條件呢?

如果所有 case 語(yǔ)句(通道操作)都被阻塞,則 select 語(yǔ)句將等待,直到其中一個(gè) case 語(yǔ)句(其通道操作)解除阻塞,然后執(zhí)行該 case。如果部分或全部通道操作是非阻塞的,則將隨機(jī)選擇非阻塞情況之一并立即執(zhí)行。

為了解釋上面的程序,我們啟動(dòng)了 2 個(gè)具有獨(dú)立通道的 goroutine。然后啟動(dòng)了 2 個(gè)case的 select 語(yǔ)句。一種情況從 chan1 讀取值,另一種情況從 chan2 讀取值。由于這些通道是無(wú)緩沖的,讀操作將被阻塞(寫(xiě)操作也是如此)。所以這兩種選擇的情況都是阻塞的。因此 select 將等待,直到其中一種情況變?yōu)榉亲枞?/p>

當(dāng)程序運(yùn)行到 select 代碼段時(shí), main goroutine 會(huì)阻塞, 然后它將調(diào)度 select 語(yǔ)句中存在的所有 goroutine, 每次一個(gè),這個(gè)例子里面是service1service2 對(duì)應(yīng)的goroutine,service1 將會(huì)等待3s,然后, 寫(xiě)入一條數(shù)據(jù)到 chan1 解除阻塞, service2 等待5s,寫(xiě)入一條數(shù)據(jù)到chan2, 然后解除阻塞。由于 service1 比 service2 更早解除阻塞,case 1 將首先解除阻塞,因此將執(zhí)行該 case,而其他 case(此處為 case 2)將被忽略。完成案例執(zhí)行后,主函數(shù)的執(zhí)行將繼續(xù)進(jìn)行。

上面的程序模擬了真實(shí)世界的 Web 服務(wù),其中負(fù)載均衡器收到數(shù)百萬(wàn)個(gè)請(qǐng)求,并且必須從可用服務(wù)之一返回響應(yīng)。使用 goroutines、channels 和 select,我們可以向多個(gè)服務(wù)請(qǐng)求響應(yīng),并且可以使用快速響應(yīng)的服務(wù)。

為了模擬所有情況何時(shí)都阻塞并且響應(yīng)幾乎同時(shí)可用,我們可以簡(jiǎn)單地刪除 Sleep 調(diào)用。

https://play.golang.org/p/giSkkqt8XHb

上述程序產(chǎn)生以下結(jié)果(您可能會(huì)得到不同的結(jié)果):

main() started 0s
service2() started 481μs
Response from service 2 Hello from service 2 981.1μs
main() stopped 981.1μs

有時(shí)候也可能是:

main() started 0s
service1() started 484.8μs
Response from service 1 Hello from service 1 984μs
main() stopped 984μs

發(fā)生這種情況是因?yàn)?chan1 和 chan2 操作幾乎同時(shí)發(fā)生,但執(zhí)行和調(diào)度仍然存在一些時(shí)間差。

default case

和 switch 語(yǔ)句一樣,select 語(yǔ)句也有 default 項(xiàng)。 default 是非阻塞的:default case 使得 select 語(yǔ)句總是非阻塞的。這意味著,任何通道(緩沖或非緩沖)上的發(fā)送和接收操作始終是非阻塞的。

如果某個(gè)值在任何通道上可用,則 select 將執(zhí)行該情況。如果沒(méi)有,它將立即執(zhí)行默認(rèn)情況。

https://play.golang.org/p/rFMpc80EuT3

在上面的程序中,由于通道是無(wú)緩沖的,并且兩個(gè)通道操作的值都不是立即可用的,因此將執(zhí)行默認(rèn)情況。如果上面的 select 語(yǔ)句沒(méi)有 default case,select 就會(huì)阻塞并且響應(yīng)會(huì)有所不同。

由于有default的情況下select 是非阻塞的,main goroutine 不會(huì)阻塞,調(diào)度程序也不會(huì)調(diào)用其他 goroutine, service1 和 service2 并不會(huì)執(zhí)行。但是我們可以手動(dòng)調(diào)用 time.Sleep 來(lái)阻塞 main goroutine。這樣,所有其他的 goroutine 都會(huì)執(zhí)行并死亡,將控制權(quán)返回給 main goroutine,它會(huì)在一段時(shí)間后喚醒。當(dāng)main gorountine喚醒時(shí),通道將立即有可用的值。

上述程序的執(zhí)行結(jié)果:

main() started 0s
service1() started 0s
service2() started 0s
Response from service 1 Hello from service 1 3.0001805s
main() stopped 3.0001805s

也可能是:

main() started 0s
service1() started 0s
service2() started 0s
Response from service 2 Hello from service 2 3.0000957s
main() stopped 3.0000957s

死鎖

當(dāng)沒(méi)有通道可用于發(fā)送或接收數(shù)據(jù)時(shí),default case很有用。為了避免死鎖,我們可以使用 default case。這是可能的,因?yàn)槟J(rèn)情況下的所有通道操作都是非阻塞的,如果數(shù)據(jù)不是立即可用,Go 不會(huì)安排任何其他 goroutine 將數(shù)據(jù)發(fā)送到通道。

https://play.golang.org/p/S3Wxuqb8lMF

與接收類(lèi)似,在發(fā)送操作中,如果其他 goroutine 處于休眠狀態(tài)(未準(zhǔn)備好接收值),則執(zhí)行 default case。

nil channel

眾所周知,通道的默認(rèn)值為 nil。因此我們不能在 nil 通道上執(zhí)行發(fā)送或接收操作。一旦在 select 語(yǔ)句中使用 nil 通道時(shí),它會(huì)拋出以下錯(cuò)誤之一或兩個(gè)錯(cuò)誤。

https://play.golang.org/p/uhraFubcF4S

從上面的結(jié)果我們可以看出,select(no cases)意味著select語(yǔ)句實(shí)際上是空的,因?yàn)楹雎粤藥в衝il channel的cases。但是由于空的 select{} 語(yǔ)句阻塞了主 goroutine 并且 service goroutine 被安排在它的位置,nil 通道上的通道操作會(huì)拋出 chan send (nil chan) 錯(cuò)誤。為了避免這種情況,我們default情況。

https://play.golang.org/p/upLsz52_CrE

上面的程序不僅忽略了 case 塊,而且立即執(zhí)行了 default 語(yǔ)句。因此調(diào)度程序沒(méi)有時(shí)間來(lái)調(diào)度 service goroutine。但這真是糟糕的設(shè)計(jì)。應(yīng)該始終檢查通道的 nil 值。

添加超時(shí)

上面的程序不是很有用,因?yàn)橹粓?zhí)行default case。但有時(shí),我們想要的是任何可用的服務(wù)都應(yīng)該在理想的時(shí)間內(nèi)做出響應(yīng),如果沒(méi)有,則應(yīng)該執(zhí)行 default case。這可以通過(guò)使用在定義的時(shí)間后解除阻塞的通道操作的情況來(lái)完成。此通道操作由時(shí)間包的 After 函數(shù)提供。讓我們看一個(gè)例子。

https://play.golang.org/p/mda2t2IQK__X

上面的程序,在 2 秒后產(chǎn)生以下結(jié)果。

main() started 0s
No response received 2.0010958s
main() stopped 2.0010958s

在上面的程序中, <-time.After(2 * time.Second) 2s 后解除阻塞,并返回時(shí)間,但在這里,我們對(duì)其返回值不感興趣。由于它也像一個(gè) goroutine,我們有 3 個(gè) goroutine, time.After 是第一個(gè)解除阻塞的channel。因此,對(duì)應(yīng)于該 goroutine 操作的 case 被執(zhí)行。

這很有用,因?yàn)槟幌氲却齺?lái)自可用服務(wù)的響應(yīng)太長(zhǎng)時(shí)間,因?yàn)橛脩舯仨毜却荛L(zhǎng)時(shí)間才能從服務(wù)中獲取任何信息。如果我們?cè)谏厦娴睦又刑砑?10 * time.Second,來(lái)自 service1 的響應(yīng)將被打印出來(lái),我想現(xiàn)在很明顯了。

empty select

與 for{} 空循環(huán)一樣,空的 select{} 語(yǔ)法也是有效的,但有一個(gè)問(wèn)題。{}正如我們所知,select 語(yǔ)句會(huì)被阻塞,直到其中一個(gè) case 解除阻塞,并且由于沒(méi)有可用的 case 語(yǔ)句來(lái)解除阻塞,主 goroutine 將永遠(yuǎn)阻塞,從而導(dǎo)致死鎖。

https://play.golang.org/p/-pBd-BLMFOu

在上面的程序中,我們知道 select 會(huì)阻塞 main goroutine,調(diào)度器會(huì)調(diào)度另一個(gè)可用的 goroutine,即 service。但在那之后,它會(huì)死掉,調(diào)度必須調(diào)度另一個(gè)可用的 goroutine,但由于主例程被阻塞,沒(méi)有其他 goroutine 可用,導(dǎo)致死鎖。

main() started
Hello from service!
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [select (no cases)]:
main.main()
        program.Go:16 +0xba
exit status 2

waitgroup

讓我們想象一種情況,您需要知道所有 goroutine 是否都完成了它們的工作。這與 select 你只需要一個(gè)條件為真的情況有點(diǎn)相反,但在這里你需要所有條件都為真才能解除main goroutine 的阻塞。這里的條件是通道操作成功。

WaitGroup 是一個(gè)帶有計(jì)數(shù)器值的結(jié)構(gòu)體,它跟蹤產(chǎn)生了多少 goroutine 以及有多少已經(jīng)完成了它們的工作。當(dāng)這個(gè)計(jì)數(shù)器達(dá)到零時(shí),意味著所有的 goroutine 都完成了他們的工作。

讓我們深入研究一個(gè)示例:

https://play.golang.org/p/8qrAD9ceOfJ

在上面的程序中,創(chuàng)建了一個(gè)類(lèi)型為 sync.WaitGroup 的空結(jié)構(gòu)(帶有零值字段)wg。 WaitGroup 結(jié)構(gòu)體有禁止導(dǎo)出的字段,例如 noCopy、state1 和 sema,我們不需要知道它們的內(nèi)部實(shí)現(xiàn)。這個(gè)結(jié)構(gòu)有三個(gè)方法,即 Add, WaitDone

Add 方法需要一個(gè) int 參數(shù),它是 WaitGroup 計(jì)數(shù)器的增加量。計(jì)數(shù)器只不過(guò)是一個(gè)默認(rèn)值為 0 的整數(shù)。它保存了正在運(yùn)行的 goroutine 的數(shù)量。當(dāng) WaitGroup 創(chuàng)建時(shí),它的計(jì)數(shù)器值為 0,我們可以通過(guò)使用 Add 方法傳遞 delta 作為參數(shù)來(lái)增加它。請(qǐng)記住,當(dāng) goroutine 啟動(dòng)時(shí),計(jì)數(shù)器不會(huì)遞增,因此我們需要手動(dòng)遞增它。

Wait 方法用于阻塞當(dāng)前的gorountine。一旦計(jì)數(shù)器達(dá)到 0,該 goroutine 將解除阻塞。因此,我們需要一些東西來(lái)減少計(jì)數(shù)器。

Done 方法就是用來(lái)遞減計(jì)數(shù)器的。它不接受任何參數(shù),因此它只將計(jì)數(shù)器減 1。

在上面的程序中,創(chuàng)建 wg 后,我們運(yùn)行了 3 次 for 循環(huán)。在每一輪中,我們啟動(dòng)了 1 個(gè) goroutine 并將計(jì)數(shù)器加 1。這意味著,現(xiàn)在我們有 3 個(gè) goroutine 等待執(zhí)行,WaitGroup 計(jì)數(shù)器為 3。請(qǐng)注意,我們?cè)?goroutine 中傳遞了一個(gè)指向 wg 的指針。這是因?yàn)樵?goroutine 中,一旦我們完成了 goroutine 應(yīng)該做的任何事情,我們需要調(diào)用 Done 方法來(lái)遞減計(jì)數(shù)器。如果 wg 作為值傳遞,則 main 中的 wg 不會(huì)遞減。這是很明顯的。

在 for 循環(huán)執(zhí)行完畢后,我們?nèi)匀粵](méi)有將控制權(quán)交給其他 goroutine。這是通過(guò)調(diào)用wg 的 Wait方法來(lái)完成的。這將阻塞 main goroutine,直到計(jì)數(shù)器達(dá)到 0。一旦計(jì)數(shù)器達(dá)到 0,因?yàn)閺?3 個(gè)協(xié)程開(kāi)始,我們?cè)?wg 上調(diào)用了 Done 方法 3 次,主協(xié)程將解除阻塞并開(kāi)始執(zhí)行進(jìn)一步的代碼。

上面的程序?qū)a(chǎn)生如下結(jié)果:

main() started
Service called on instance 2
Service called on instance 3
Service called on instance 1
main() stopped

以上結(jié)果對(duì)你們來(lái)說(shuō)可能不同,因?yàn)?goroutine 的執(zhí)行順序可能會(huì)有所不同。

添加方法接受 int 類(lèi)型,這意味著 delta 也可以是負(fù)數(shù)。要了解更多信息,請(qǐng)查看官方文檔[https://golang.org/pkg/sync/#WaitGroup.Add]

worker pool

顧名思義,worker pool:即工作池,是并發(fā)模式協(xié)同完成同樣工作一些列 goroutine 的集合。在前面 WaitGroup 中,我們看到一組 goroutines 并發(fā)工作,但它們沒(méi)有特定的工作內(nèi)容。一旦將channel加入其中,讓它們有相同的工作去完成,這些goroutines就會(huì)成為一個(gè)工作池。

因此,worker pool 背后的概念是維護(hù)一個(gè) worker goroutines 池,它接收一些任務(wù)并返回結(jié)果。一旦他們都完成了他們的工作,我們就會(huì)收集結(jié)果。所有這些 goroutine 都出于各自的目的使用相同的通道。

讓我們看一個(gè)帶有兩個(gè)通道的簡(jiǎn)單示例,即 tasks 和 results

https://play.golang.org/p/IYiMV1I4lCj

這個(gè)程序發(fā)生了什么呢?

  • sqrWorker 是一個(gè)工作函數(shù), 它接收三個(gè)參數(shù) tasks channel, results channel 和 id , 這個(gè)gorountine的任務(wù)是接收 tasks中的數(shù)據(jù),計(jì)算平方,并把結(jié)果發(fā)送到results中。
  • 在 main 函數(shù)中, 創(chuàng)建了緩沖容量為10的tasksresults channel,因此,在tasks緩沖滿之前, 發(fā)送操作都是非阻塞的。因此,設(shè)置大的緩沖值是一個(gè)是一個(gè)好主意。
  • 然后我們生成多個(gè) sqrWorker goroutines實(shí)例,把前面建立的兩個(gè)channel 和 id 作為參數(shù),其中id是作為標(biāo)記,標(biāo)識(shí)是哪個(gè)gorountine正在執(zhí)行任務(wù)。
  • 然后我們將 5 個(gè)job傳遞給非阻塞的tasks channel。
  • 完成了發(fā)送job到tasks后,我們關(guān)閉了它。這并不是必需的,但是如果出現(xiàn)一些錯(cuò)誤,它將在將來(lái)節(jié)省大量時(shí)間。
  • 然后使用 for, 循環(huán) 5 次,我們從results channel中提取數(shù)據(jù)。由于對(duì)空緩沖區(qū)的讀取操作是阻塞的,因此將從工作池中調(diào)度一個(gè) goroutine。在 goroutine 返回一些結(jié)果之前,main goroutine 將被阻塞。
  • 因?yàn)樵?worker goroutine 中模擬阻塞操作,調(diào)度程序?qū)⒄{(diào)用另一個(gè)可用的 goroutine,直到work gorountine變得可用時(shí),它會(huì)將計(jì)算結(jié)果寫(xiě)入results channel。由于在緩沖區(qū)滿之前,寫(xiě)入通道是非阻塞的,因此在此處寫(xiě)入results通道是非阻塞的。此外,雖然當(dāng)前的 worker goroutine 不可用,但執(zhí)行了多個(gè)其他 worker goroutines,消耗了tasks緩沖區(qū)中的值。在所有worker goroutine 消耗完tasks后,tasks通道緩沖區(qū)為空,for range 循環(huán)結(jié)束。當(dāng)tasks通道關(guān)閉時(shí),它不會(huì)拋出死鎖錯(cuò)誤。
  • 有時(shí),所有工作協(xié)程都可能處于休眠狀態(tài),因此主協(xié)程將喚醒并工作,直到結(jié)果通道緩沖區(qū)再次為空。
  • 在所有工作 goroutine 死后,main goroutine 將重新獲得控制權(quán)并從結(jié)果通道打印剩余的結(jié)果并繼續(xù)執(zhí)行。

上面的例子解釋了多個(gè) goroutines 如何可以在同一個(gè)通道上提供數(shù)據(jù)并優(yōu)雅地完成工作。當(dāng)worker被阻塞時(shí),goroutines 很靈活的解決了這個(gè)問(wèn)題。如果刪除 time.Sleep() 的調(diào)用,則只有一個(gè) goroutine 獨(dú)自完成該作業(yè),因?yàn)樵?for range 循環(huán)完成且 goroutine 終止之前不會(huì)調(diào)度其他 goroutine。

運(yùn)行系統(tǒng)速度不同, 您可能得到與上述例子不同的結(jié)果,因?yàn)槿绻械膅orountine即使是被阻塞很短的時(shí)間, main gorountine也會(huì)被喚醒執(zhí)行程序。

現(xiàn)在,讓我們使用sync.WaitGroup 實(shí)現(xiàn)相同的效果,但更優(yōu)雅。

https://play.golang.org/p/0rRfchn7sL1

上面的結(jié)果看起來(lái)很整潔,因?yàn)閙ain goroutine 中results channel 上的讀取操作是非阻塞的,而results channel 開(kāi)始讀取前已經(jīng)完成結(jié)果填充,而main goroutine 被 wg.Wait() 調(diào)用阻塞。使用 waitGroup,我們可以防止大量(不必要的)上下文切換(調(diào)度),這里是 7,而前面的例子是 9。但是有一個(gè)犧牲,因?yàn)槟惚仨毜鹊剿械墓ぷ鞫纪瓿伞?/p>

metux

Mutex【鎖】 是 Go 中最簡(jiǎn)單的概念之一。但在解釋之前,讓我們先了解什么是競(jìng)態(tài)條件。 goroutines 有它們獨(dú)立的堆棧,因此它們之間不共享任何數(shù)據(jù)。但是可能存在堆中的某些數(shù)據(jù)在多個(gè) goroutine 之間共享的情況。在這種情況下,多個(gè) goroutine 試圖在同一內(nèi)存位置操作數(shù)據(jù),從而導(dǎo)致意外結(jié)果。下面展示一個(gè)簡(jiǎn)單的例子:

https://play.golang.org/p/MQNepChxiEa

在上面的程序中,我們生成了 1000 個(gè) goroutines,它們?cè)黾恿顺跏紴?0 的全局變量 i 的值。由于我們正在實(shí)現(xiàn) WaitGroup,我們希望所有 1000 個(gè) goroutines 一一增加 i 的值,從而得到 i 的最終值為 1000。當(dāng)主 goroutine 在 wg.Wait() 調(diào)用后再次開(kāi)始執(zhí)行時(shí),我們正在打印 i。讓我們看看最終的結(jié)果。

value of i after 1000 operations is 937

什么?為什么我們不到1000?看起來(lái)有些 goroutines 不起作用。但實(shí)際上,我們的程序存在競(jìng)爭(zhēng)條件。讓我們看看可能發(fā)生了什么。

i = i + 1 calculation has 3 steps

-(1) 獲取 i 當(dāng)前值
-(2) 將i的值增加 1
-(3) 使用新值替換 i

讓我們想象一個(gè)場(chǎng)景,在這些步驟之間安排了不同的 goroutine。例如,讓我們考慮 1000 個(gè) goroutine 池中的 2 個(gè) goroutine,即:G1 和 G2。

當(dāng) i 為 0 時(shí) G1 首先啟動(dòng),運(yùn)行前 2 個(gè)步驟,現(xiàn)在 i 現(xiàn)在為 1。但在 G1 更新步驟 3 中 i 的值之前,新的 goroutine G2 被調(diào)度并運(yùn)行所有步驟。但是在 G2 的情況下,i 的值仍然是 0,因此在它執(zhí)行第 3 步之后,i 將是 1。現(xiàn)在 G1 再次被安排完成第 3 步并從第 2 步更新 i 的值為 1。在 goroutines 的完美世界中在完成所有 3 個(gè)步驟后安排,2 個(gè) goroutines 的成功操作會(huì)產(chǎn)生 i 的值是 2 但這里不是這種情況。因此,我們幾乎可以推測(cè)為什么我們的程序沒(méi)有將 i 的值變?yōu)?1000。

到目前為止,我們了解到 goroutine 是協(xié)作調(diào)度的。除非一個(gè) goroutine 在并發(fā)課程中提到的條件之一阻塞,否則另一個(gè) goroutine 不會(huì)取代它。既然 i = i + 1 沒(méi)有阻塞,為什么 Go 調(diào)度器會(huì)調(diào)度另一個(gè) goroutine?

你絕對(duì)應(yīng)該在stackoverflow上查看這個(gè)答案。在任何情況下,您都不應(yīng)該依賴 Go 的調(diào)度算法并實(shí)現(xiàn)自己的邏輯來(lái)同步不同的 goroutine。

確保一次只有一個(gè) goroutine 完成上述所有 3 個(gè)步驟的一種方法是實(shí)現(xiàn)互斥鎖。 Mutex(互斥)是編程中的一個(gè)概念,其中一次只有一個(gè)例程(線程)可以執(zhí)行多個(gè)操作。這是通過(guò)一個(gè)例程獲取對(duì)值的鎖定,對(duì)它必須執(zhí)行的值進(jìn)行任何操作,然后釋放鎖定來(lái)完成的。當(dāng)值被鎖定時(shí),沒(méi)有其他例程可以讀取或?qū)懭胨?/p>

在 Go 中,互斥鎖數(shù)據(jù)結(jié)構(gòu)(本質(zhì)上是個(gè)map)由sync包提供的。在 Go 中,在對(duì)可能導(dǎo)致競(jìng)爭(zhēng)條件的值執(zhí)行任何操作之前,我們使用 mutex.Lock() 方法獲取鎖,然后是操作代碼。一旦我們完成了操作,在上面的程序 i = i + 1 中,我們使用 mutext.Unlock() 方法解鎖它。當(dāng)任何其他 goroutine 在鎖存在時(shí)嘗試讀取或?qū)懭?i 的值時(shí),該 goroutine 將阻塞,直到操作從第一個(gè) goroutine 解鎖。因此只有 1 個(gè) goroutine 可以讀取或?qū)懭?i 的值,避免競(jìng)爭(zhēng)條件。請(qǐng)記住,在整個(gè)操作被解鎖之前,鎖定和解鎖之間的操作中存在的任何變量將不可用于其他 goroutine。

讓我們用互斥鎖修改前面的例子。

https://play.golang.org/p/xVFAX_0Uig8

在上面的程序中,我們創(chuàng)建了一個(gè)互斥鎖 m 并將指向它的指針傳遞給所有生成的 goroutine。在開(kāi)始對(duì) i 進(jìn)行操作之前,我們使用 m.Lock() 語(yǔ)法獲取了互斥鎖 m 上的鎖,并且在操作之后,我們使用 m.Unlock() 語(yǔ)法將其解鎖。以上程序產(chǎn)生以下結(jié)果

value of i after 1000 operations is 1000

從上面的結(jié)果可以看出,互斥鎖幫助我們解決了競(jìng)態(tài)條件。但是第一條規(guī)則是避免 goroutine 之間共享資源。

您可以在運(yùn)行 Go run -race program.Go 之類(lèi)的程序時(shí)使用種族標(biāo)志測(cè)試 Go 中的競(jìng)爭(zhēng)條件。在此處閱讀有關(guān)比賽檢測(cè)器的更多信息。

Concurrency Patterns 【并發(fā)模式】

通俗來(lái)講,就是日常使用的常用范式。
以下是一些可以使程序更快更可靠的概念和方法。

    1. Generator 【生成器模式】

使用通道,可以實(shí)現(xiàn)更好實(shí)現(xiàn)生成器。
比如斐波那契數(shù)列,計(jì)算上開(kāi)銷(xiāo)很大, 我們可以提前計(jì)算好結(jié)果,并放入channel中, 等待程序執(zhí)行到此,直接取結(jié)果即可, 而不必等待。

https://play.golang.org/p/1_2MDeqQ3o5

此圖中,調(diào)用fib 函數(shù),返回了一個(gè)channel,通過(guò)循環(huán)channel,可以接收到的計(jì)算好的數(shù)據(jù)。在 fib 函數(shù)內(nèi)部,必須返回一個(gè)只接收通道,我們創(chuàng)建一個(gè)有buffer的通道,并在函數(shù)最后返回它。fib的返回值會(huì)將這個(gè)雙向通道轉(zhuǎn)換為單向只接收通道。在匿名 goroutine 中,使用 for 循環(huán)將斐波那契數(shù)推送到此通道,完成后,關(guān)閉此通道。在主協(xié)程中,使用 fib 函數(shù)調(diào)用的范圍,我們可以直接訪問(wèn)這個(gè)通道。

    1. fan-in & fan-out 【多路復(fù)用】

fan-in 是一種多路復(fù)用策略,將過(guò)個(gè)輸入通道組合,以產(chǎn)生輸出通道。fan-out 是將單個(gè)通道拆分為多個(gè)通道的解復(fù)用策略。

package main

import (
    "fmt"
    "sync"
)
// return channel for input numbers
func getInputChan() <-chan int {
    // make return channel
    input := make(chan int, 100)

    // sample numbers
    numbers := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

    // run goroutine
    go func() {
        for num := range numbers {
            input <- num
        }
        // close channel once all numbers are sent to channel
        close(input)
    }()

    return input
}

// returns a channel which returns square of numbers
func getSquareChan(input <-chan int) <-chan int {
    // make return channel
    output := make(chan int, 100)

    // run goroutine
    go func() {
        // push squares until input channel closes
        for num := range input {
            output <- num * num
        }

        // close output channel once for loop finishesh
        close(output)
    }()

    return output
}

// returns a merged channel of `outputsChan` channels
// this produce fan-in channel
// this is veriadic function
func merge(outputsChan ...<-chan int) <-chan int {
    // create a WaitGroup
    var wg sync.WaitGroup
    
    // make return channel
    merged := make(chan int, 100)
    
    // increase counter to number of channels `len(outputsChan)`
    // as we will spawn number of goroutines equal to number of channels received to merge
    wg.Add(len(outputsChan))
    
    // function that accept a channel (which sends square numbers)
    // to push numbers to merged channel
    output := func(sc <-chan int) {
        // run until channel (square numbers sender) closes
        for sqr := range sc {
            merged <- sqr
        }
        // once channel (square numbers sender) closes,
        // call `Done` on `WaitGroup` to decrement counter
        wg.Done()
    }
    
    // run above `output` function as groutines, `n` number of times
    // where n is equal to number of channels received as argument the function
    // here we are using `for range` loop on `outputsChan` hence no need to manually tell `n`
    for _, optChan := range outputsChan {
        go output(optChan)
    }
    
    // run goroutine to close merged channel once done
    go func() {
        // wait until WaitGroup finishesh
        wg.Wait()
        close(merged)
    }()

    return merged
}

func main() {
    // step 1: get input numbers channel
    // by calling `getInputChan` function, it runs a goroutine which sends number to returned channel
    chanInputNums := getInputChan()
    
    // step 2: `fan-out` square operations to multiple goroutines
    // this can be done by calling `getSquareChan` function multiple times where individual function call returns a channel which sends square of numbers provided by `chanInputNums` channel
    // `getSquareChan` function runs goroutines internally where squaring operation is ran concurrently
    chanOptSqr1 := getSquareChan(chanInputNums)
    chanOptSqr2 := getSquareChan(chanInputNums)
    
    // step 3: fan-in (combine) `chanOptSqr1` and `chanOptSqr2` output to merged channel
    // this is achieved by calling `merge` function which takes multiple channels as arguments
    // and using `WaitGroup` and multiple goroutines to receive square number, we can send square numbers
    // to `merged` channel and close it
    chanMergedSqr := merge(chanOptSqr1, chanOptSqr2)
    
    // step 4: let's sum all the squares from 0 to 9 which should be about `285`
    // this is done by using `for range` loop on `chanMergedSqr`
    sqrSum := 0
    
    // run until `chanMergedSqr` or merged channel closes
    // that happens in `merge` function when all goroutines pushing to merged channel finishes
    // check line no. 86 and 87
    for num := range chanMergedSqr {
        sqrSum += num
    }
    
    // step 5: print sum when above `for loop` is done executing which is after `chanMergedSqr` channel closes
    fmt.Println("Sum of squares between 0-9 is", sqrSum)
}

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,143評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 175,553評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,620評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,416評(píng)論 6 405
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 54,940評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,170評(píng)論 0 287
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,709評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,597評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,784評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評(píng)論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,029評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,407評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,663評(píng)論 1 280
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,403評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,746評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容

  • 如果必須選擇 Go 的一項(xiàng)偉大功能,那么它必須是內(nèi)置的并發(fā)模型。它不僅支持并發(fā),而且使它變得更好。 Go Conc...
    癩痢頭閱讀 1,483評(píng)論 0 1
  • Channel 單純地將函數(shù)并發(fā)執(zhí)行是沒(méi)有意義地,函數(shù)與函數(shù)需要交換數(shù)據(jù)才能體現(xiàn)并發(fā)執(zhí)行函數(shù)地意義。Go語(yǔ)言的并發(fā)...
    TZX_0710閱讀 333評(píng)論 0 0
  • Go 并發(fā)編程 選擇 Go 編程的原因可能是看中它簡(jiǎn)單且強(qiáng)大,那么你其實(shí)可以選擇C語(yǔ)言;除此之外,我看中 Go 的...
    PRE_ZHY閱讀 900評(píng)論 1 6
  • CSP 并發(fā)模型 CSP(Communicating Sequential Processes),是用于描述兩個(gè)獨(dú)...
    朱建濤閱讀 703評(píng)論 0 2
  • 開(kāi)發(fā)go程序的時(shí)候,時(shí)常需要使用goroutine并發(fā)處理任務(wù),有時(shí)候這些goroutine是相互獨(dú)立的,而有的時(shí)...
    駐馬聽(tīng)雪閱讀 2,459評(píng)論 0 21