o 語言有一個很重要的特性就是 goroutine, 我們可以使用 goroutine 結合 channel 來開發並發程序。
並發程序指的是可以同時運行多個任務的程序,這里的同時運行並不一定指的是同一時刻執行,在單核CPU的機器下,在同一時刻只可能有一個任務在執行,但是由於CPU的速度很快,在不斷的切換着多個任務,讓它們交替的執行,因此宏觀上看起來就像是同時在運行; 而在多核的機器上,並發程序中的多個任務是可以實現在同一時刻執行多個的,此時並發的多個任務是在並行執行的。
goroutine
goroutine 是 go 語言中的並發執行單元,我們可以將多個任務分別放在多個 goroutine 中,來實現並發程序。下面先看一個例子:
package main
import "fmt"
func hello() {
fmt.Println("Hello World!!!")
}
func main() {
go hello()
fmt.Println("Bye!!!")
var input string
fmt.Scanln(&input)
}
上述程序的執行結果如下:
Bye!!!
Hello World!!!
上面這個例子展示了使用 goroutine 的幾個要點:
- 程序啟動時,我們的主函數 main 也是在一個單獨的 goroutine 中運行的。
go hello()
就是用於創建一個 goroutine, 即 go 關鍵字加上 要在 goroutine 中執行的函數(也可以是匿名函數,不過必須是調用的形式)- 最后兩句是用於將 main 函數阻塞在這里,直到我們按下回車鍵,之所以這么做是因為,我們不知道新創建的 goroutine 和 main goroutine 的執行順序,有可能主程序先執行完成,此時主程序結束,我們就看不到新 goroutine 的執行效果了。(通常不會使用這種方法)
以上就是 goroutine 的基本用法
channels
前面我們學習了怎樣創建並行的執行單元,但是每個執行單元之間是完全獨立的,如果我們想在運行期間交換數據,即進行通信,此時就得依靠另一個概念 - channels, 即通道,這個名字十分貼切,就像在不同的並發執行單元之間連接了一根管道,然后通過這跟管道來發送和接收數據。
goroutine 和 channel 經常結合在一起使用,下面學習一些 channel 的用法:
-
創建 channel
ch1 := make(chan int)
channel 也需要使用 make 函數來創建,也就是說 channel 也是一種引用類型(make函數會返回低層數據結構的引用給channel)
-
向 channel 中讀寫數據
前面說了 channel 是用於 goroutine 之間通信的, 自然能夠從 channel 中寫入和讀取數據,使用的都是
<-
操作符ch := make(chan int) ch<- 1 // 向 channel 中寫入數據 var a int = <-ch // 從 channel 中讀取數據
-
關閉 channel
在我們使用完一個 channel 之后,可以調用 close() 方法來關閉一個 channel, 關閉之后的通道,不能夠再進行數據的寫操作, 但是仍然可以讀取之前寫入成功的數據(如果沒有數據了,將返回零值)。
channel 的基本操作就是上面這么多,不過實際上,channel 是有兩種的: 無緩沖的 和 有緩沖的。上面我們創建的是無緩存的,有緩存的創建方式是 ch := make(chan int, 2)
, 二者的區別是:
- 無緩沖的 channel 的發送操作將導致發送者的 goroutine 阻塞,直到在另一個 goroutine 上對其進行接收操作。如果先發生的是接收操作,那么接收者將被阻塞,直到在另一個 goroutine 上對其進行發送操作。
- 帶緩存的 channel 可以緩存多個數據,因此不會立即阻塞,只有當緩存滿了之后,發送者才可能會被阻塞,並且只有到緩存為空時,接收者才可能被阻塞
例1: 通道用於傳遞消息
package main
import "fmt"
func main() {
message := make(chan string) // 創建一個用於傳遞字符串的通道
go func() {
message <- "This is a message." // 向 channel 寫入數據
}()
msg := <- message // 從 channel 讀取數據
fmt.Println(msg)
}
例2: 利用通道進行同步
package main
import "fmt"
func hello() {
fmt.Println("Hello World!!!")
done <- true
}
func main() {
done := make(chan bool)
go hello()
fmt.Println("Bye!!!")
<-done // 這里會阻塞住,直到在另一個 goroutine 中對 done 進行寫入操作之后
}
單向 channel
當使用 channel 作為參數,我們可以指定 channel 為單向的,即讓通道在函數中只能發送,或者只能接收數據,以此來提高程序的安全性.
語法:
<-chan type
表示一個只能接收數據的通道chan<- type
表示一個只能發送數據的通道
例子:
package main
import "fmt"
// 這里的 message 在函數 send 中就是一個只能發送數據的通道
func send(msg string, message chan<- string) {
message<- msg
}
// 這里的 message 在函數 receive 中就是一個只能發送數據的通道
func receive(message <-chan string) string {
msg := <- message
return msg
}
func main() {
message := make(chan string)
go send("hello", message)
fmt.Println(receive(message))
}
輸出結果是 hello
, 此時在函數 send 中,message 通道就只能用於發送數據,而在函數 receive 中通道只能接收數據,通過參數的限制使其在函數內部成為了單向的通道。
select
go語言提供了一個 select 關鍵字,可以使用它來等待多個通道的操作,以實現多路復用。語法:
select {
case <-ch1:
...
case ch2 <- value:
...
default:
...
}
其中的每個 case 表示一個 channel 的操作,當case語句后面指定通道的操作可以執行時,select 才會執行 case 之后的語句。此時其他的語句都不會被執行。
例子: 超時處理
package main
import "time"
import "fmt"
func main() {
ch1 := make(chan string, 1)
go func() {
time.Sleep(time.Second * 2)
ch1 <- "result 1"
}()
select {
case res := <- ch1:
fmt.Println(res)
case <-time.After(time.Second * 1):
fmt.Println("timeout 1")
}
ch2 := make(chan string, 1)
go func() {
time.Sleep(time.Second * 2)
ch2 <- "result 2"
}()
select {
case res := <-ch2:
fmt.Println(res)
case <-time.After(time.Second * 3):
fmt.Println("timeout 2")
}
}
上面的例子中我們定義了兩個通道和兩個select結構,是為了進行對比,第一個channel會在等待兩秒之后被寫入數據,而在 select 中,第二個case語句只會等待一秒,然后就會執行,因此就會執行超時操作。而在第二個 select 中,第二個 case 語句會等待三秒。所以上述程序的結果如下:
timeout 1
result 2