開源庫「go home」聚焦Go語言技術棧與面試題,以協助Gopher登上更大的舞台,歡迎
go home
~
背景介紹
大家都知道進程是操作系統資源分配的基本單位,有獨立的內存空間,線程可以共享同一個進程的內存空間,所以線程相對輕量,上下文切換開銷也小。雖然線程已經比較輕量了,但還是占近1M的內存,而今天介紹的有“輕量級線程”之稱的Goroutine,可以小至幾十K甚至幾K,切換的開銷更小。
除此之外,在傳統Socket編程時,需要維護一個線程池來為每個Socket收發包分配線程,而且需要將CPU與線程數建立對應關系,確保每個任務都能被及時分配給CPU,而Go程序可以智能地將goroutine中的任務分配到CPU。
如何使用
我們現在假設一個場景,你是一家公司老總,每天要花兩小時處理郵件,六小時開會,那么,程序可以這樣編寫。
func main() {
time.Sleep(time.Hour * 2) //處理郵件
time.Sleep(time.Hour * 6) //開會
fmt.Println("工作完成了")
}
運行一下
果然,要8小時才能完成工作,那么怎么簡化工作呢?沒錯,請一個助理小姐姐幫忙,就讓她來處理郵件,這樣你就只需6小時開會就行了。
開始寫代碼吧,先來定義一個助理函數。
func assistant() {
time.Sleep(time.Hour * 2)
}
然后在主函數用一條神器的命令go
調用它,這樣助理的耗時久不再占用你的時間了。
func main() {
go assistant()
time.Sleep(time.Hour * 6) //開會
fmt.Println("工作完成了")
}
運行一下
真的只花了六個小時就完成工作了。各位看官們,看到沒,這就是協程,只需要go
命令加上函數名,就這么簡單。
但是我知道勤奮的你是不會滿足於現狀的。
匿名函數
另外,既然goroutine支持普通函數,當然也就支持匿名函數。
go func() {
time.Sleep(time.Hour * 2)
}()
協程間如何通訊
雖然我們可以輕松地創建一堆協程,但是不能通信的協程是沒有靈魂的。假如助理正在幫你處理郵件時,你突然想請她喝奶茶,那是不是要通知她?
那怎么通知呢?這就引出了大名鼎鼎的channel,漢譯“通道”,顧名思義它的作用就是在協程之間建立通道,一端可以將數據源源不斷地傳送到通道的另一端。
而聲明方式也非常簡單,只需要make一下。拿下方代碼為例,它代表初始化一個通道類型變量,並且通道里只能存放string類型的數據。
ch := make(chan string)
初始化完成后,要想與協程函數建立連接,得先把chan變量傳給協程函數。
go assistant(ch)
當然,協程函數要能接收chan才行,我們縱深到函數內部,看看都干了些什么。
func assistant(ch chan string) {
go func() {
for {
fmt.Println("看了一封郵件")
time.Sleep(time.Second * 1)
}
}()
msg := <-ch
if msg == "喝奶茶去唄" {
ch <- "好啊"
}
}
函數內部又起了一個協程專門處理郵件,同時另外一邊等待老板通知。細心的你應該看出如何取通道數據了,沒錯,只需要在通道變量前加上<-
符號就可以將值取出,同樣的,符號加在后面就是往通道塞數據。
ch <- "pingyeaa"
<- ch
如果通道沒有數據,消費端就會一直阻塞,直到有數據為止。當然編譯器是很聰明的,在編譯的時候如果發現沒有地方往通道里塞數據,它就會panic,提示死鎖。
fatal error: all goroutines are asleep - deadlock!
繼續來看代碼,大致意思就是老板如果發“喝奶茶去唄”,就返回“好啊”,因為通道里一開始是沒數據的,所以該協程會一直阻塞,直到主函數往通道中寫入了消息。
現在來看下主函數的實現邏輯,聲明通道和傳入通道變量就不再贅述了,我們只需要等待5秒鍾之后往通道里寫入喝奶茶消息即可。因為剛才assistant協程接收到消息后會往ch寫入“好啊”消息,所以主函數在發完請求之后應該再讀取從助理那邊傳遞來的消息。
ch := make(chan string)
go assistant(ch)
time.Sleep(time.Second * 5)
ch <- "喝奶茶去唄"
resp := <-ch
fmt.Println(resp)
同樣,主函數的<-ch
也會一直阻塞,直到助理回復消息。另外有兩點需要注意,第一,如果main函數趕在goroutine之前執行完畢,那么goroutine也會銷毀;第二,main也是goroutine。
最后,關閉通道,其實通道關閉不是必須的,它與文件不同,如果沒有goroutine使用到channel,就會自動銷毀,而close的作用是用來通知通道的另一端不再發送消息了,另一端可以通過<-ch的第二個參數來獲取通道關閉情況。
close(ch)
data, ok := <-ch
通道的多路復用select
剛才的示例中的<-ch只能讀取通道的一條消息,如果通道里不止一條消息,該怎么讀取呢?
應該很多同學跟我一樣想到的是遍歷,沒錯,遍歷確實可以拿到通道數據。
for {
fmt.Println(<-ch)
}
也可以這么遍歷。
for d := range ch {
fmt.Println(d)
}
但是,如果需要同時接收多個通道數據該怎么辦?循環中接收兩個通道變量?
for {
data, ok := <-ch1
data, ok := <-ch2
}
這種方式雖然可以取出數據,但是性能較差,官方給我們提供的select關鍵詞就是專門用來解決多通道數據讀取問題的,語法與switch非常相似。select會將多個通道傳來的數據分發到不同的處理邏輯中。
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
for {
select {
case d := <-ch1:
fmt.Println("ch1", d)
case d := <-ch2:
fmt.Println("ch2", d)
}
}
}()
ch1 <- 1
ch1 <- 2
ch2 <- 2
ch1 <- 3
}
模擬超時
除此之外,有些情況下我們不希望通道阻塞太久,假設5秒鍾還取不出通道的數據,就超時退出,那我們可以使用time.After方法來實現。time.After會返回一個通道類型,它的作用是傳入一個目標時間(比如5s),我們在5秒后就可以通過通道獲取預設置的超時通知,這樣就達到了定時器的目的。
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
for {
select {
case d := <-ch1:
fmt.Println("ch1", d)
case d := <-ch2:
fmt.Println("ch2", d)
case <-time.After(time.Second * 5):
fmt.Println("接收超時")
}
}
}()
time.Sleep(time.Second * 6)
}
通道關閉延伸閱讀
已關閉的通道再發送數據會觸發panic
ch := make(chan int)
close(ch)
ch <- 1
panic: send on closed channel
通道設置長度
可以通過make方法設置通道長度,作為緩沖區,通道滿時生產者端會阻塞,通道取空后消費端會阻塞。
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 2
ch <- 2
fmt.Println(len(ch))
已關閉的通道依然可以讀取數據
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 2
close(ch)
for d := range ch {
fmt.Println(d)
}
感謝大家的觀看,如果覺得文章對你有所幫助,歡迎關注公眾號「平也」,聚焦Go語言與技術原理。