goroutine
在Go里,每一個並發執行的活動稱為goroutine
。 如果你是一名Java程序員,可以把goroutine
比作為線程,但是goroutine
和線程在數量上有很大的差別,原因在於Go語言引入了協程的概念,協程相比於線程是一種用戶態的線程,協程更加輕量,實用更加經濟,因此同樣的服務器可以開銷的協程數量要比線程多很多。
goroutine和協程的區別:
- goroutine是協程的go語言實現,相當於把別的語言的類庫的功能內置到語言里。從調度上看,goroutine的調度開銷遠遠小於線程調度開銷。
- 不同的是:Golang在runtime,系統調用等多方面對goroutine調度進行了封裝和處理,即goroutine不完全是用戶控制,一定程度上由go運行時(runtime)管理,好處:當某goroutine阻塞時,會讓出CPU給其他goroutine。
線程和goroutine的區別:
- OS的線程由OS內核調度,每隔幾毫秒,一個硬件時鍾中斷發到CPU,CPU調用一個調度器內核函數。這個函數暫停當前正在運行的線程,把他的寄存器信息保存到內存中,查看線程列表並決定接下來運行哪一個線程,再從內存中恢復線程的注冊表信息,最后繼續執行選中的線程。這種線程切換需要一個完整的上下文切換:即保存一個線程的狀態到內存,再恢復另外一個線程的狀態,最后更新調度器的數據結構。某種意義上,這種操作還是很慢的。
- 從調度上講,線程的調度由 OS 的內核完成;線程的切換需要CPU寄存器和內存的數據交換,在線程切換的過程中需要保存/恢復所有的寄存器信息,比如16個通用寄存器,PC(Program Counter),SP(Stack Pointer),段寄存器等等,從而切換不同的線程上下文。 其觸發方式為 CPU時鍾。而goroutine 的調度 則比較輕量級,由go自身的調度器完成;Go運行的時候包涵一個自己的調度器,這個調度器使用一個稱為一個M:N調度技術,m個goroutine到n個os線程(可以用GOMAXPROCS來控制n的數量),Go的調度器不是由硬件時鍾來定期觸發的,而是由特定的go語言結構來觸發的,他不需要切換到內核語境,所以調度一個goroutine比調度一個線程的成本低很多。其只關心當前go程序內協程的調度;觸發方式為 go內部的事件,如文件和網絡操作垃圾回收,time.sleep,通道阻塞,互斥量操作等。在同一個原生線程里,若當前goroutine不發生阻塞,那么不會主動讓出CPU給其他同一線程的goroutine的。在go程序啟動時,會首先創建一個特殊的內核線程sysmom,負責監控和調度。
- 從棧空間上,goroutine的棧空間更加動態靈活。每個OS的線程都有一個固定大小的棧內存,通常是2MB,棧內存用於保存在其他函數調用期間哪些正在執行或者臨時暫停的函數的局部變量。這個固定的棧大小,如果對於goroutine來說,可能是一種巨大的浪費。作為對比goroutine在生命周期開始只有一個很小的棧,典型情況是2KB, 在go程序中,一次創建十萬左右的goroutine也不罕見(2KB*100,000=200MB)。而且goroutine的棧不是固定大小,它可以按需增大和縮小,最大限制可以到1GB。
- goroutine沒有一個特定的標識。在大部分支持多線程的操作系統和編程語言中,線程有一個獨特的標識,通常是一個整數或者指針,這個特性可以讓我們構建一個線程的局部存儲,本質是一個全局的map,以線程的標識作為鍵,這樣每個線程可以獨立使用這個map存儲和獲取值,不受其他線程干擾。goroutine中沒有可供程序員訪問的標識,原因是一種純函數的理念,不希望濫用線程局部存儲導致一個不健康的超距作用,即函數的行為不僅取決於它的參數,還取決於運行它的線程標識。
簡單的示例代碼如下:在Go里,每一個並發執行的活動稱為goroutine
。
簡單的示例代碼如下:
f() //調用f();等它返回
go f() // 新建一個調用分()的goroutine,不用等待
在下面的例子中,主goroutine
計算第45個斐波那契數。因為它使用非常低效的遞歸算法,因此需要大量的時間來執行,在此期間我們提供一個可見的提示,顯示一個字符串”spinner“來指示程序依然在運行。
package main
import (
"fmt"
"time"
)
func spinner(delay time.Duration) {
for {
for _, r := range `-\|/` {
fmt.Printf("\r%c", r)
time.Sleep(delay)
}
}
}
func fib(x int) int {
if x < 2 {
return x
}
return fib(x-1) + fib(x-2)
}
func main() {
go spinner(100 * time.Microsecond)
const n = 45
fibN := fib(n) // slow
fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN)
}
若干秒后,fib(45)返回結果,如下圖所示:
然后main
函數返回,所有的goroutine
都暴力地直接終結,然后程序退出。
通道
如果說goroutine
是Go程序並發的執行體,通道就是它們之間的連接。通道是可以讓一個goroutine
發送特定值到另一個goroutine
的通信機制。每一個通道是一個具體類型的導管,叫做通道的元素類型。一個有int
類型元素的通道寫為chan int
。
使用內置的make
函數來創建一個通道:
ch := make(chan int) // ch的類型是 chan int
像map
一樣,通道是一個使用make
創建的數據結構的引用。當復制或者作為參數傳遞到一個函數時,復制的是引用,這樣調用者和被調用者都引用同一份數據結構。和其他引用類型一樣,通道的零值是nil
。
通道有兩個主要的操作:發送和接收,這兩者統稱為通信。send
語句從一個goroutine
傳輸一個值到另一個在執行接收表達式的goroutine
。兩個操作都使用<-
操作符書寫。發送語句中,通道和值分別在<-
的左右兩邊。在接收表達式中,<-
放在通道操作數的前面。
具體書寫格式如下:
ch <- x //發送語句
x = <- ch // 賦值語句中的接收表達式
<- ch // 接收語句,丟棄結果
通道支持第三個操作:關閉,它設置一個標志位來指示值當前已經發送完畢,這個通道后面沒有值了,關閉后的發送操作將導致宕機。在一個已經關閉的通道上進行接收操作,將獲取所有已經發送的值,直到通道為空,這是任何接收操作會立即完成,同時獲取到一個通道元素類型對應的零值。
調用內置的close
函數來關閉通道:
close(ch)
無緩沖通道
使用簡單的make
調用創建的通道叫做無緩沖通道,但make
還可以接受第二個可選參數,一個表示通道容量的整數。如果容量是0,make
創建一個無緩沖的通道。
ch = make(chan int) // 無緩沖通道
ch = make(chan int, 0) // 無緩沖通道
ch = make(chan int, 3) // 容量為3的緩沖通道
無緩沖通道上的發送操作將會阻塞,直到另一個goroutine
在對應的通道上執行接收操作,這時值傳送完成,兩個goroutine
都可以繼續執行。相反,如果接收操作先執行,接收方goroutine
將阻塞,直到另一個goroutine
在同一個通道發送一個值。
使用無緩沖通道進行通信導致發送和接收goroutine
同步化。因此,無緩沖通道也稱為同步通道。當一個值在無緩沖通道上傳遞時,接收值后發送方goroutine
才被再次喚醒。
package main
import "fmt"
func main(){
ch:=make(chan int) //這里就是創建了一個channel,這是無緩沖管道注意
go func(){ //創建子go程
for i:=0;i<=6;i++{
ch<-i //循環寫入管道
fmt.Println("寫入",i)
}
}()
for i:=0;i<6;i++{ //主go程
num:=<-ch //循環讀出管道
fmt.Println("讀出",num)
}
}
緩沖通道
緩沖通道有一個元素隊列,隊列的最大長度在創建的時候通過make
的容量參數來設置。如下代碼創建了一個帶有10個字符串的緩沖通道:
ch = make(chan string,10)
緩沖通道上的發送操作在對列的尾部插入一個元素,接收操作從隊列的頭部移除一個元素。如果通道滿了,發送操作會阻塞所在的goroutine
直到另一個goroutine
對它進行接收操作來騰出可用的空間。反過來,如果通道是空的,執行接收操作的goroutine
阻塞,直到另一個goroutine
在通道上發送數據。
現在,我們可以在通道上無阻塞的發送三個值,但是在發送第四個值的時候就會阻塞。
package main
func main() {
ch := make(chan string, 3)
ch <- "A"
ch <- "B"
ch <- "C"
ch <- "D"
}
在我們向管道塞入第四個值的時候,程序爆出了死鎖的異常,如下圖:
但是當我們在執行第四次向通道塞值的時候,從通道取出一個值,就可以安全的進行第四次塞值了,並且成功的打印出了隊列的第一個元素A,如下圖:
管道
通道可以用來連接goroutine
,這樣一個具體的輸出是另一個的輸入。管道一般是由三個goroutine
組成,使用兩個通道連接起來。
如下代碼所示:
package main
import "fmt"
func main() {
naturals := make(chan int)
squares := make(chan int)
// counter
go func() {
for x := 0; x< 100; x++ {
naturals <- x
}
close(naturals)
}()
// squares
go func() {
for {
x,ok := <-naturals
if !ok {
break // 通道關閉並且讀完
}
squares <- x * x
}
close(squares)
}()
// printer(在主goroutine中)
for x := range squares{
fmt.Println(x)
}
}
結束時,關閉每一個通道不是必需的,只有在通知接收方goroutine
所有數據都發送完畢的時候才需要關閉通道。通道也是可以通過垃圾回收器根據它是否可以訪問來決定是否回收它,而不是根據它是否關閉。
不要將這個close
操作和對於文件的close
操作混淆。當結束的時候對每一個文件調用Close
方法是非常重要的。
單向通道
Go也提供了單向通道類型,僅僅導出發送或者接收操作。類型chan <- int
是一個只能發送的通道,允許接收但是不能發送。(<-
操作符相對於chan
關鍵字的位置是一個幫助記憶的點)。
package main
import "fmt"
// 單向輸出通道 chan<-
func counter(out chan<- int) {
for x := 0; x < 100; x++ {
out <- x
}
}
// 單向輸出通道 chan<-
func squarer(out chan<- int, in <-chan int) {
for v := range in {
out <- v * v
}
}
// 單向輸入通道 <-chan
func printer(in <-chan int) {
for v := range in {
fmt.Println(v)
}
}
func main() {
naturals := make(chan int)
squares := make(chan int)
go counter(naturals)
go squarer(squares, naturals)
printer(squares)
}
並行循環
為了演示並行循環,我們考慮生成一批全尺寸圖像的縮略圖,代碼如下:
package main
import (
"gopl.io/ch8/thumbnail"
"io/ioutil"
"log"
)
func makeThumbnails(filenames []string) {
for _, f := range filenames {
if _, err := thumbnail.ImageFile(f); err != nil {
log.Println(err)
}
}
}
func main() {
rootPath := "W:\\Google_Download\\壁紙\\動漫壁紙\\"
//var rootPath string = "W:\\Google_Download\\壁紙\\動漫壁紙\\"
files, _ := ioutil.ReadDir(rootPath)
var fileNames []string
/*
第一種讀取文件列表的方法
*/
for _, f := range files {
//fmt.Println(f.Name())
fileNames = append(fileNames, rootPath+f.Name())
}
makeThumbnails(fileNames)
}
其中需要導入的gopl.io/ch8/thumbnail
包,從下面的網站下載:
GitHub - adonovan/gopl.io: Example programs from "The Go Programming Language"](https://github.com/adonovan/gopl.io/)
生成后的結果如圖所示,都是我喜歡的動漫圖片,如果也喜歡,私信我給你發哈~
以上就是Go語言關於goroutine
和通道的內容,關於goroutine
和通道其實還有很多可以深挖的東西,我們后面會繼續學習。希望這篇文章可以幫助到你~