GO語言的進階之路-goroutine(並發)
作者:尹正傑
版權聲明:原創作品,謝絕轉載!否則將追究法律責任。
有人把Go比作21世紀的C 語言,第一是因為 Go語言設計簡單,第二,21世紀最重要的就是並行程序設計,而GO 從語言層面就支持了並行。Go語言中最重要的一個特性,那就是 go 關鍵字。優雅的並發編程范式,完善的並發支持,出色的並發性能是Go語言區別於其他語言的一大特色。使用Go語言開發服務器程序時,就需要對它的並發機制有深入的了解。
一.並發基礎
回到在Windows和Linux出現之前的古老年代,程序員在開發程序時並沒有並發的概念,因為命令式程序設計語言是以串行為基礎的,程序會順序執行每一條指令,整個程序只有一個執行上下文,即一個調用棧,一個堆。並發則意味着程序在運行時有多個執行上下文,對應着多個調用棧。我們知道每一個進程在運行時,都有自己的調用棧和堆,有一個完整的上下文,而操作系統在調度進程的時候,會保存被調度進程的上下文環境,等該進程獲得時間片后,再恢復該進程的上下文到系統中。從整個操作系統層面來說,多個進程是可以並發的,那么並發的價值何在?下面我們先看以下幾種場景。
1>.一方面我們需要靈敏響應的圖形用戶界面,一方面程序還需要執行大量的運算或者IO密集操作,而我們需要讓界面響應與運算同時執行。
2>.當我們的Web服務器面對大量用戶請求時,需要有更多的“Web服務器工作單元”來分別響應用戶。
3>.我們的事務處於分布式環境上,相同的工作單元在不同的計算機上處理着被分片的數據。
4>.計算機的CPU從單內核(core)向多內核發展,而我們的程序都是串行的,計算機硬件的能力沒有得到發揮。
5>.我們的程序因為IO操作被阻塞,整個程序處於停滯狀態,其他IO無關的任務無法執行。
從以上幾個例子可以看到,串行程序在很多場景下無法滿足我們的要求。下面我們歸納了並發程序的幾條優點,讓大家認識到並發勢在必行:
a>.並發能更客觀地表現問題模型;
b>.並發可以充分利用CPU核心的優勢,提高程序的執行效率;
c>.並發能充分利用CPU與其他硬件設備固有的異步性。
現在我們已經意識到並發的好處了,那么到底有哪些方式可以實現並發執行呢?就目前而言,並發包含以下幾種主流的實現模型。
1>.多進程。多進程是在操作系統層面進行並發的基本模式。同時也是開銷最大的模式。在Linux平台上,很多工具鏈正是采用這種模式在工作。比如某個Web服務器,它會有專門的進程負責網絡端口的監聽和鏈接管理,還會有專門的進程負責事務和運算。這種方法的好處在於簡單、進程間互不影響,壞處在於系統開銷大,因為所有的進程都是由內核管理的。
2>.多線程。多線程在大部分操作系統上都屬於系統層面的並發模式,也是我們使用最多的最有效的一種模式。目前,我們所見的幾乎所有工具鏈都會使用這種模式。它比多進程 的開銷小很多,但是其開銷依舊比較大,且在高並發模式下,效率會有影響。
3>.基於回調的非阻塞/異步IO。這種架構的誕生實際上來源於多線程模式的危機。在很多高並發服務器開發實踐中,使用多線程模式會很快耗盡服務器的內存和CPU資源。而這種模式通過事件驅動的方式使用異步IO,使服務器持續運轉,且盡可能地少用線程,降低開銷,它目前在Node.js中得到了很好的實踐。但是使用這種模式,編程比多線程要復雜,因為它把流程做了分割,對於問題本身的反應不夠自然。
4>.協程。協程(Coroutine)本質上是一種用戶態線程,不需要操作系統來進行搶占式調度,且在真正的實現中寄存於線程中,因此,系統開銷極小,可以有效提高線程的任務並發性,而避免多線程的缺點。使用協程的優點是編程簡單,結構清晰;缺點是需要語言的支持,如果不支持,則需要用戶在程序中自行實現調度器。目前,原生支持協程的語言還很少。
接下來我們先詮釋一下傳統並發模型的缺陷,之后再講解goroutine並發模型是如何逐一解決這些缺陷的。
人的思維模式可以認為是串行的,而且串行的事務具有確定性。線程類並發模式在原先的確定性中引入了不確定性,這種不確定性給程序的行為帶來了意外和危害,也讓程序變得不可控。線程之間通信只能采用共享內存的方式。為了保證共享內存的有效性,我們采取了很多措施,比如加鎖等,來避免死鎖或資源競爭。實踐證明,我們很難面面俱到,往往會在工程中遇到各種奇怪的故障和問題。
我們可以將之前的線程加共享內存的方式歸納為“共享內存系統”,雖然共享內存系統是一種有效的並發模式,但它也暴露了眾多使用上的問題。計算機科學家們在近40年的研究中又產生了一種新的系統模型,稱為“消息傳遞系統”。
對線程間共享狀態的各種操作都被封裝在線程之間傳遞的消息中,這通常要求:發送消息時對狀態進行復制,並且在消息傳遞的邊界上交出這個狀態的所有權。從邏輯上來看,這個操作與共享內存系統中執行的原子更新操作相同,但從物理上來看則非常不同。由於需要執行復制操作,所以大多數消息傳遞的實現在性能上並不優越,但線程中的狀態管理工作通常會變得更為簡單。
最早被廣泛應用的消息傳遞系統是由C. A. R. Hoare在他的Communicating Sequential Processes中提出的。在CSP系統中,所有的並發操作都是通過獨立線程以異步運行的方式來實現的。這些線程必須通過在彼此之間發送消息,從而向另一個線程請求信息或者將信息提供給另一個線程。使用類似CSP的系統將提高編程的抽象級別。
隨着時間的推移,一些語言開始完善消息傳遞系統,並以此為核心支持並發,比如Erlang。
二.協程
再說協成之前,我們需要了解兩個概念,即用戶態和內核態。
1.什么是用戶態;
官方解釋:用戶態(user mode)在計算機結構指兩項類似的概念。在CPU的設計中,用戶態指非特權狀態。在此狀態下,執行的代碼被硬件限定,不能進行某些操作,比如寫入其他進程的存儲空間,以防止給操作系統帶來安全隱患。在操作系統的設計中,用戶態也類似,指非特權的執行狀態。內核禁止此狀態下的代碼進行潛在危險的操作,比如寫入系統配置文件、殺掉其他用戶的進程、重啟系統等。
應用程序在用戶態下運行,僅僅只能執行cpu整個指令集的一個子集,該子集中不包含操作硬件功能的部分,因此,一般情況下,在用戶態中有關I/O和內存保護(操作系統占用的內存是受保護的,不能被別的程序占用)。
如果感興趣的朋友可以參考:https://baike.baidu.com/item/%E7%94%A8%E6%88%B7%E6%80%81/9548791?fr=aladdin
2.什么是內核態;
內核態也叫和核心態。
官方解釋:在處理器的存儲保護中,主要有兩種權限狀態,一種是核心態(管態),也被稱為特權態;一種是用戶態(目態)。核心態是操作系統內核所運行的模式,運行在該模式的代碼,可以無限制地對系統存儲、外部設備進行訪問。
操作系統在內核態運行情況下可以訪問硬件上所有的內容。
如果感興趣的朋友可以參考:https://baike.baidu.com/item/%E6%A0%B8%E5%BF%83%E6%80%81/6845908?fr=aladdin
3.什么是協程;
官方解釋:一個程序可以包含多個協程,可以對比與一個進程包含多個線程,因而下面我們來比較協程和線程。我們知道多個線程相對獨立,有自己的上下文,切換受系統控制;而協程也相對獨立,有自己的上下文,但是其切換由自己控制,由當前協程切換到其他協程由當前協程來控制。
執行體是個抽象的概念,在操作系統層面有多個概念與之對應,比如操作系統自己掌管的進程(process)、進程內的線程(thread)以及進程內的協程(coroutine,也叫輕量級線程)。與傳統的系統級線程和進程相比,協程的最大優勢在於其“輕量級”,可以輕松創建上百萬個而不會導致系統資源衰竭,而線程和進程通常最多也不能超過1萬個。這也是協程也叫輕量級線程的原因。
多數語言在語法層面並不直接支持協程,而是通過庫的方式支持,但用庫的方式支持的功能也並不完整,比如僅僅提供輕量級線程的創建、銷毀與切換等能力。如果在這樣的輕量級線程中調用一個同步 IO 操作,比如網絡通信、本地文件讀寫,都會阻塞其他的並發執行輕量級線程,從而無法真正達到輕量級線程本身期望達到的目標。
Go 語言在語言級別支持輕量級線程,叫goroutine。Go 語言標准庫提供的所有系統調用操作(當然也包括所有同步 IO 操作),都會出讓 CPU 給其他goroutine。這讓事情變得非常簡單,讓輕量級線程的切換管理不依賴於系統的線程和進程,也不依賴於CPU的核心數量。協程(coroutine)是Go語言中的輕量級線程實現,由Go運行時(runtime)管理。在一個函數調用前加上go關鍵字,這次調用就會在一個新的goroutine中並發執行。當被調用的函數返回時,這個goroutine也自動結束。需要注意的是,如果這個函數有返回值,那么這個返回值會被丟棄。協成工作在用戶態,它類似於現場的運行方式可以並行處理任務。
三.goroutine
goroutine不同於thread,threads是操作系統中的對於一個獨立運行實例的描述,不同操作系統,對於thread的實現也不盡相同;但是,操作系統並不知道goroutine的存在,goroutine的調度是有Golang運行時進行管理的。啟動thread雖然比process所需的資源要少,但是多個thread之間的上下文切換仍然是需要大量的工作的(寄存器/Program Count/Stack Pointer/...),Golang有自己的調度器,許多goroutine的數據都是共享的,因此goroutine之間的切換會快很多,啟動goroutine所耗費的資源也很少,一個Golang程序同時存在幾百個goroutine是很正常的。goroutine是Go語言中的輕量級線程實現,由Go運行時(runtime)管理.goroutine 比thread 更易用、更高效、更輕便。
1.創建一個goroutine
goroutine 是通過 Go 的 runtime管理的一個線程管理器。通過關鍵字go 就啟動了一個 goroutine。我們來看一個例子:
1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:y1053419035@qq.com 6 */ 7 8 package main 9 10 import ( 11 "time" 12 "fmt" 13 ) 14 15 func MyEcho(s string) { 16 for i := 0; i < 5; i++ { 17 time.Sleep(100*time.Millisecond) //表示每次循環后都要休息100毫秒。 18 fmt.Println(s) 19 } 20 } 21 22 func main() { 23 go MyEcho("尹正傑") //在函數執行前加個go,表示單獨起了一個協程,表示和當前主協程(main)並駕齊驅運行代碼。 24 MyEcho("Golang") 25 } 26 27 28 29 30 31 #以上代碼執行結果如下:(需要注意的是,他們輸出的順序是不確定的喲~) 32 Golang 33 尹正傑 34 Golang 35 尹正傑 36 尹正傑 37 Golang 38 Golang 39 尹正傑 40 尹正傑 41 Golang
2.goroutine的局限性
Go程序從初始化 main package 並執行 main() 函數開始,當 main() 函數返回時,程序退出,且程序並不等待其他goroutine(非主goroutine)結束。光這樣說大家可能不是很理解,接下來我們就用實際代碼來說明,下面的一段代碼使對切片“yzj”的元素進行排序,而主程序運行時間是12秒,我們可以清楚的看到“yzj”這個切片的長度是13,這意味着需要開啟13個goroutine,而我們定義的主函數的運行的時間是12秒。這意味着12秒之后,不管有多少個goroutine在運行程序都會自動結束。這也是為什么我們沒有看到“yzj”這個切片數字的另外兩個元素輸出,即都是int型的15和17。因為這2個goroutine執行完畢的時間是15秒和17秒,而程序的最長允許的運行時間是12秒。
1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:y1053419035@qq.com 6 */ 7 8 package main 9 10 import ( 11 "time" 12 "fmt" 13 ) 14 15 func main() { 16 yzj := []int{2,7,1,6,4,3,11,15,17,5,8,9,12} 17 fmt.Println("該切片的長度是:",len(yzj)) 18 for _,n := range yzj{ 19 go func(n int) { //定義一個匿名函數,並對該函數開啟協程,每次循環都會開啟一個協成,也就是說它開啟了13個協程。 20 time.Sleep(time.Duration(n) * time.Second) //表示每循環一次就需要睡1s,睡的總時間是由n來控制的,總長度是由s切片數組中最大的一個數字決定,也就是說這個協成最少需要17秒才會結束喲。 21 fmt.Println(n) 22 }(n) //由於這個函數是匿名函數,所以調用方式就直接:(n)調用,不用輸入函數名。 23 } 24 time.Sleep(12*time.Second) //主進程要執行的時間是12秒. 25 } 26 27 28 29 #以上代碼執行結果如下: 30 該切片的長度是: 13 31 1 32 2 33 3 34 4 35 5 36 6 37 7 38 8 39 9 40 11 41 12
通過上面這段代碼我們可以明顯的知道這個程序是有bug的,因為我們的要求是對切片“yzj”順序的從小到大的排序。但是“yzj”這個切片中的元素“15”和“17”是沒有輸出出來的。當然從上面的分析你會立馬找出解決方案,比如說將主程序的運行時間從12秒改成大於或等於17秒不就得了。good,這種改法的確是可以針對這個程序是有效的。但是你沒有發現這個效率很低嗎?那么我們是不是有一種機制可以讓goroutine和main()進行通信呢?要讓主函數等待所有goroutine退出后再返回,如何知道goroutine都退出了呢?這就引出了多個goroutine之間通信的問題。
實現一個如此簡單的功能,卻寫出如此臃腫而且難以理解的代碼。想象一下,在一個大的系統中具有無數的鎖、無數的共享變量、無數的業務邏輯與錯誤處理分支,那將是一場噩夢。這噩夢就是眾多C/C++開發者正在經歷的,其實Java和C#開發者也好不到哪里去。
Go語言既然以並發編程作為語言的最核心優勢,當然不至於將這樣的問題用這么無奈的方式來解決。Go語言提供的是另一種通信模型,即以消息機制而非共享內存作為通信方式。消息機制認為每個並發單元是自包含的、獨立的個體,並且都有自己的變量,但在不同並發單元間這些變量不共享。每個並發單元的輸入和輸出只有一種,那就是消息。這有點類似於進程的概念,每個進程不會被其他進程打擾,它只做好自己的工作就可以了。不同進程間靠消息來通信,它們不會共享內存。
Go語言提供的消息通信機制被稱為channel,接下來我們將詳細介紹channel。現在,讓我們用Go語言社區的那句著名的口號來結束這一小節:“不要通過共享內存來通信,而應該通過通信來共享內存。”不過想要了解golang關於鎖的通信機制的小伙伴們,我也將筆記早就總結出來了(使勁戳我就成)。
四.channel
channel是Go語言在語言級別提供的goroutine間的通信方式。我們可以使用channel在兩個或多個goroutine之間傳遞消息。channel是進程內的通信方式,因此通過channel傳遞對象的過程和調用函數時的參數傳遞行為比較一致,比如也可以傳遞指針等。如果需要跨進程通信,我們建議用分布式系統的方法來解決,比如使用Socket或者HTTP等通信協議。Go語言對於網絡方面也有非常完善的支持。
channel是類型相關的。也就是說,一個channel只能傳遞一種類型的值,這個類型需要在聲明channel時指定。如果對Unix管道有所了解的話,就不難理解channel,可以將其認為是一種類型安全的管道。
在了解channel的語法前,我們先看下用channel的方式重寫上面的例子是什么樣子的,以此對channel先有一個直感的認識。
需要重新上面案例。暫時空出來,等我把channel講解完畢在寫
1.基本語法(channels)
1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:y1053419035@qq.com 6 */ 7 8 package main 9 10 import "fmt" 11 12 func main() { 13 var yzj_string chan string //般channel的聲明形式為:var chanName chan ElementType.與一般的變量聲明不同的地方僅僅是在類型之前加了 chan 關鍵字。 ElementType 指定這個 channel所能傳遞的元素類型。 14 15 var yzj_map map[string]chan bool //這是我們聲明一個的map ,元素是 bool 型的channel。 16 17 yzj_channel := make(chan []map[string]int)//定義一個channel也很簡單,直接使用內置的函數 make() 即可。 18 19 fmt.Println(yzj_string) 20 fmt.Println(yzj_map) 21 fmt.Println(yzj_channel) 22 /* 23 writ_channel := "yinzhengjie" 24 25 yzj_string <- writ_channel //在channel的用法中,最常見的包括寫入和讀出。將一個數據寫入(發送)至channel的語法很直觀。向channel寫入數據通常會導致程序阻塞,直到有其他goroutine從這個channel中讀取數據。 26 27 read_channel := yzj_string //如果channel之前沒有寫入數據,那么從channel中讀取數據也會導致程序阻塞,直到channel中被寫入數據為止。我們之后還會提到如何控制channel只接受寫或者只允許讀取,即單向channel。 28 */ 29 } 30 31 32 33 #以上代碼執行結果如下: 34 <nil> 35 map[] 36 0xc04203a060
知道如何定義一個channel之后,我們也可以做一下簡單的應用,代碼如下:
1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:y1053419035@qq.com 6 */ 7 8 package main 9 10 import "fmt" 11 12 func MySum(a []int, sum chan int) { //該函數是對切片數組求和,需要傳入一個切片數組和一個channel。 13 value := 0 14 for _, v := range a { 15 value += v 16 } 17 sum <- value //將數據發送到channel中去。 18 } 19 func main() { 20 yzj := []int{1,2,3,4,5,-10} 21 sum := make(chan int) //用chan定義一個channel對象名稱為“sum”,其類型是“int”。 22 go MySum(yzj[:len(yzj)/2], sum) //將切片的前一半發送給channel對象“sum” 23 go MySum(yzj[len(yzj)/2:], sum) //將切片的后一半發送給channel對象“sum” 24 x, y := <-sum, <-sum //從我們定義中的channel中獲取數據,並將讀取到的value賦值給x,y 25 fmt.Println("X =" ,x) 26 fmt.Println("Y =",y) 27 fmt.Println("X+Y =" ,x+y) 28 } 29 30 31 32 #以上代碼執行結果如下: 33 X = 6 34 Y = -1 35 X+Y = 5
2.緩沖機制(Buffered Channels)
之前我們示范創建的都是不帶緩沖的channel,這種做法對於傳遞單個數據的場景可以接受,但對於需要持續傳輸大量數據的場景就有些不合適了。接下來我們介紹如何給channel帶上緩沖,從而達到消息隊列的效果。要創建一個帶緩沖的channel,其實也非常容易,比如“yzj := make(chan int ,4096)”在調用 make() 時將緩沖區大小作為第二個參數傳入即可,創建了一個大小為4096的 int 類型 channel ,即使沒有讀取方,寫入方也可以一直往channel里寫入,在緩沖區被填完之前都不會阻塞。
1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:y1053419035@qq.com 6 */ 7 8 package main 9 import ( 10 "fmt" 11 ) 12 13 func FibonacciSequence(num int, Producer chan int) { 14 x, y := 1, 1 15 for i := 0; i < num; i++ { 16 Producer <- x 17 x, y = y, x + y 18 } 19 close(Producer) //可以顯式的關閉channel,生產者通過關鍵字 close 函數關閉 channel。關閉channel 之后就無法再接受或發送任何數據了。 記住應該在生產者的地方關閉channel, 20 // 而不是消費的地方去關閉它,這樣容易引起 panic 另外記住一點的就是channel 不像文件之類的,不需要經常去關閉,只有當你確實沒有任何發送數據了,或者你想顯式的結束 range 循環之類的。 21 22 } 23 24 func main() { 25 yzj := make(chan int, 5) 26 go FibonacciSequence(cap(yzj), yzj) 27 28 value, status := <-yzj //注意,這里的“value”相當對“yzj”這個channel進行讀取一次數據喲。“status”的值如何為“true”則表明channel還沒有被關閉喲。 29 fmt.Println(value,status) 30 31 for i := range yzj { //我們使用range語法能夠不斷的讀取channel 里面的數據,直到該 channel 被顯式的關閉。 32 fmt.Println(i) 33 } 34 35 value, status = <-yzj //注意,“status”的值如何為“false”,那么說明 channel 已經沒有任何數據並且已經被關閉。 36 fmt.Println(value,status) 37 } 38 39 40 41 42 #以上代碼執行結果如下: 43 1 true 44 1 45 2 46 3 47 5 48 0 false
3.channel的選擇語句selecte語法
早在Unix時代, select 機制就已經被引入。通過調用 select() 函數來監控一系列的文件句柄,一旦其中一個文件句柄發生了IO動作,該 select() 調用就會被返回。后來該機制也被用於實現高並發的Socket服務器程序。Go語言直接在語言級別支持 select 關鍵字,用於處理異步IO問題。select 的用法與 switch 語言非常類似,由 select 開始一個新的選擇塊,每個選擇條件由case 語句來描述。與 switch 語句可以選擇任何可使用相等比較的條件相比, select 有比較多的限制,其中最大的一條限制就是每個 case 語句里必須是一個IO操作。
我們上面介紹的都是只有一個channel 的情況,那么如果存在多個 channel 的時候,我們該如何操作呢,Go里面提供了一個關鍵字 select ,通過 select 可以監聽channel 上的數據流動。select 默認是阻塞的,只有當監聽的channel 中有發送或接收可以進行時才會運行,當多個channel 都准備好的時候,select 是隨機的選擇一個執行的。
1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:y1053419035@qq.com 6 */ 7 8 package main 9 10 import "fmt" 11 12 func fibonacci(channel_name,quit chan int) { //定義兩個channle對象channel_name和quit。 13 x,y := 0,1 14 for{ 15 select { 16 case channel_name <- x: //用channel_name接受數據。 17 x,y = y,x+y 18 19 case <-quit: //表示當接收到quit的channel時,就執行以下代碼。其實就是實現關閉channel的功能。但是它並沒有權限主動關閉channel,而是負責監聽channel 上的數據流動。 20 fmt.Println("EXIT") 21 return //函數一退出協程也就跟着退出了 22 } 23 } 24 } 25 26 func main() { 27 channel_name := make(chan int) 28 quit := make(chan int) 29 30 go func() { //運行一個匿名函數。 31 for i := 0; i < 11; i++ { 32 fmt.Println(<-channel_name) //"<-channel_name"表示讀取channel_name中的參數。 33 } 34 quit<- 100 //當for循環結束后,我們隨便給quit的channel傳一個值就可以實現退出函數的功能,我們之前需要用close(c)來退出發信號的功能,主動權在"fibonacci",而我們現在我們用quit來主動退出協程。 35 }() 36 37 fibonacci(channel_name,quit) //將channel_name和quit傳遞給fibonacci函數 38 } 39 40 41 42 43 #以上代碼執行結果如下: 44 0 45 1 46 1 47 2 48 3 49 5 50 8 51 13 52 21 53 34 54 55 55 EXIT
4.channel的默認語句default語法
1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:y1053419035@qq.com 6 */ 7 8 package main 9 10 import ( 11 "time" 12 "fmt" 13 ) 14 15 func main() { 16 tick := time.Tick(1000*time.Millisecond) //也可以這樣寫:“tick := time.NewTicker(1000*time.Millisecond).C”其中這個點C就是一個channel。 17 boom := time.After(5000*time.Millisecond) 18 for { 19 select { 20 case <-tick: 21 fmt.Println("滴答。。。") 22 case <-boom: 23 fmt.Println("砰~") 24 return 25 default: 26 fmt.Println("吃一口涼皮") 27 time.Sleep(500*time.Millisecond) 28 } 29 } 30 } 31 32 33 34 35 #以上代碼執行結果如下: 36 吃一口涼皮 37 吃一口涼皮 38 滴答。。。 39 吃一口涼皮 40 吃一口涼皮 41 滴答。。。 42 吃一口涼皮 43 吃一口涼皮 44 滴答。。。 45 吃一口涼皮 46 吃一口涼皮 47 滴答。。。 48 吃一口涼皮 49 吃一口涼皮 50 滴答。。。 51 砰~
5.超時機制(timeout)
Go語言沒有提供直接的超時處理機制,但我們可以利用 select 機制。雖然 select 機制不是專為超時而設計的,卻能很方便地解決超時問題。因為 select 的特點是只要其中一個 case 已經完成,程序就會繼續往下執行,而不會考慮其他 case 的情況。有時候會出現goroutine 阻塞的情況,那么我們如何避免整個的程序進入阻塞的情況呢?我們可以利用select 來設置超時,通過如下的方式實現:
1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:y1053419035@qq.com 6 */ 7 8 package main 9 10 import ( 11 "time" 12 "fmt" 13 ) 14 15 func main() { 16 TimeOut := time. After(5 * time.Second) //定義超時時間。 17 NerverRings := make(chan int) 18 RingsOccasionally := make(chan bool) 19 go func() { 20 for { 21 select { 22 case value := <- NerverRings: 23 fmt.Println(value) 24 case <- TimeOut: 25 println("對不起,到目前為止,NerverRings並沒有接收到任何數據!程序已經終止。") 26 RingsOccasionally <- true 27 break 28 } 29 } 30 }() 31 32 <- RingsOccasionally //從RingsOccasionally這個channel中獲取數據,所以在獲取數據之前,成功程序是出於阻塞狀態的喲。 33 34 } 35 36 37 38 #以上代碼執行結果如下: 39 對不起,到目前為止,NerverRings並沒有接收到任何數據!程序已經終止。
6.channel的傳遞
需要注意的是,在Go語言中channel本身也是一個原生類型,與 map 之類的類型地位一樣,因此channel本身在定義后也可以通過channel來傳遞。具體案例如下:
1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:y1053419035@qq.com 6 */ 7 8 package main 9 10 import ( 11 "fmt" 12 "time" 13 ) 14 15 type PipeData struct { //定義結構體 PipeData 16 value int 17 handler func(int) int 18 next chan int 19 } 20 21 func Handle(queue chan *PipeData) { 22 for data := range queue{ 23 fmt.Println("value=",data.value) 24 fmt.Println("handler=",data.handler) 25 fmt.Println("next=",data.next) 26 data.next <- data.handler(data.value) 27 } 28 } 29 30 func main() { 31 yzj := make(chan *PipeData) //由於Handle支持傳入指針類型的*PipeData,因此我們初始化的時候要個其類型保持一致。 32 33 go func() { //我們開啟一個goroutine,讓其不斷的發送數據。 34 data := &PipeData{value:100,handler: func(i int) int { 35 return i 36 }} //我們需要將數據定義好,這個data就是我們需要發送的數據。 37 yzj <- data //將數據發送給名為yzj的channel。 38 }() 39 40 go Handle(yzj) //當我們把數據傳給channel變量yzj之后,就可以把這個channel繼續傳給Handle這個函數啦。 41 // 42 //data := <- yzj //接下來我們開始從channel讀取數據。 43 //fmt.Println(data.value) 44 //fmt.Println(data.handler) 45 //fmt.Println(data.next) 46 time.Sleep(time.Second * 3) //為了避免主進程提前結束,我們需要讓主進程拖長一點時間,以后我會介紹更簡單的方法來控制這個時間。 47 } 48 49 50 51 52 #以上代碼執行結果如下: 53 value= 100 54 handler= 0x489250 55 next= <nil>
7.單向channel
顧名思義,單向channel只能用於發送或者接收數據。channel本身必然是同時支持讀寫的,否則根本沒法用。假如一個channel真的只能讀,那么肯定只會是空的,因為你沒機會往里面寫數據。同理,如果一個channel只允許寫,即使寫進去了,也沒有絲毫意義,因為沒有機會讀取里面的數據。所謂的單向channel概念,其實只是對channel的一種使用限制。
定義一個單向channel很簡單:
1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:y1053419035@qq.com 6 */ 7 8 package main 9 10 import "fmt" 11 12 func main() { 13 data := make(chan int) //默認該channel就是可讀可寫的喲。 14 15 go func() { 16 write := chan<- int(data) //此處的write 是一個單向的寫入channel。 17 write <- 100 18 fmt.Println(write) 19 }() 20 21 read := <-chan int(data) //此處的read就是一個單向的讀取channel。 22 fmt.Println(read) 23 } 24 25 26 27 #以上代碼執行結果如下: 28 0xc04203a060 29 100
當然,我們可以在函數中對channel進行只讀或是只寫的操作,如下:
1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:y1053419035@qq.com 6 */ 7 8 package main 9 10 import ( 11 "fmt" 12 "time" 13 ) 14 15 func Parse(ch <-chan int) { //Parse函數的功能是只讀的channel。注意的是,channel本身就是可讀可寫的,所謂的只讀channel和只寫channel只是使用者在用法上的限制而已。 16 for value := range ch { 17 fmt.Println("Parsing value", value) 18 } 19 } 20 21 func main() { 22 data := make(chan int) 23 go Parse(data) //注意,這行代碼是阻塞代碼。我們知道這行代碼使從channel中讀取數據,但是目前還沒往channel發送數據。我們用go關鍵字開啟一個協程,可以讓代碼繼續往下執行。 24 data <- 10000 //往channel發送的數據。 25 time.Sleep(time.Second * 1) //讓主程序運行一秒鍾,避免主進程提前比goroutine結束。 26 27 } 28 29 30 31 #以上代碼執行結果如下: 32 Parsing value 10000