1.并發編程基礎概念
進程和線程
A. 進程是程序在操作系統中的一次執行過程,系統進行資源分配和調度的一個獨立單位。B. 線程是進程的一個執行實體,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位。C.一個進程可以創建和撤銷多個線程;同一個進程中的多個線程之間可以并發執行。
并發和并行
A. 多線程程序在一個核的cpu上運行,就是并發。B. 多線程程序在多個核的cpu上運行,就是并行。
并發
并行
goroutine 只是由官方實現的超級"線程池"。
每個實力4~5KB
的棧內存占用和由于實現機制而大幅減少的創建和銷毀開銷是go高并發的根本原因。
進程:程序執行所用的內存空間。
線程:程序執行者。
協程:就是比線程更小的線程。(開銷小、體量小)
并發:多線程程序執行, 但是吧cpu只有一個核,一段時間內,只能執行一個線程。
并行:多線程程序執行,cpu有多個核,每個核對不同程序的調用是可以同時進行的。
為什么說go支持并發?
1.因為 go 中的 goroutine(由go在語言層面實現的線程池 ):
我們在類似于java語言層面要實現線程,
1.1首先創建一個線程來執行任務;
1.2 線程池的存在,就是里面有一組可復用線程,我們直接把任務交給線程池,它幫我們選出空閑線程來執行。在這個層面上,go已經語言層面已經遙遙領先,如次,簡化多線程程序開發。
2.以及協程: 一種比線程更小,可以被用戶調度的線程。看協程的定義我們知道,創建一個協程所用的開銷(創建、管理啥的)所用的內存是比線程更小。
3.還有就是其余編程語言并發模型是共享內存模型,比如java,線程通信通過共享內存實現的,我們無法保證操作是原子性的,因此需要設計同步機制,但是go通過channel來實現協程通信,就解決了如上問題。
因此,綜上所述,說go默認支持高并發。
?2.Goroutine
嗯,我們要在go語言啟用協程是十分簡單的。
原先。
func main() {hello()fmt.Println("main goroutine done")
}func hello() {fmt.Println("Hello Goroutine!")
}
把函數的執行交給協程來管理。
func main() {go hello()fmt.Println("main goroutine done")
}func hello() {fmt.Println("Hello Goroutine!")
}
PS : go 關鍵字,必須跟著函數調用。
但是,我們發現實際調用時,go 關鍵字后面的hello程序并未執行,這是因為main函數的執行,也是由一個協程來執行的。當main函數結束時,main 協程也會結束,在里面執行的協程也會強制終結。可能hello 協程,剛創建還沒執行就寄了。
因此,我們可以如下修改。延長main()函數的執行時間。
func main() {go hello()fmt.Println("main goroutine done")time.Sleep(time.Second)
}func hello() {fmt.Println("Hello Goroutine!")
}
按照官方的說法,協程(goroutine)的調度是由GPM控制的。GPM是go語言自己實現的一套調度系統。區別于操作系統調度OS線程。
- 1.G很好理解,就是個goroutine的,里面除了存放本goroutine信息外 還有與所在P的綁定等信息。
- 2.P管理著一組goroutine隊列,P里面會存儲當前goroutine運行的上下文環境(函數指針,堆棧地址及地址邊界),P會對自己管理的goroutine隊列做一些調度(比如把占用CPU時間較長的goroutine暫停、運行后續的goroutine等等,當自己的隊列消費完了就去全局隊列里取,如果全局隊列里也消費完了會去其他P的隊列里搶任務。
- 3.M(machine)是Go運行時(runtime)對操作系統內核線程的虛擬, M與內核線程一般是一一映射的關系, 一個groutine最終是要放到M上執行的;
P與M一般也是一一對應的。他們關系是: P管理著一組G掛載在M上運行。當一個G長久阻塞在一個M上時,runtime會新建一個M,阻塞G所在的P會把其他的G 掛載在新建的M上。當舊的G阻塞完成或者認為其已經死掉時 回收舊的M。
因此,但從線程調度來講,相比其他語言(如:Java),go可以使用戶自己調度線程。
3.runtime包
runtime包提供和go運行時環境的互操作,如制go協程的函數。它也包括用于reflect包的低層次類型信息;參見reflect報的文檔獲取運行時類型系統的可編程接口。
runtime.Gosched()
Gosched使當前go協程放棄處理器,以讓其它go程運行。暫時停止該go協程的處理。它不會終結當前go協程,因此當前go協程未來會恢復執行。
func main() {go func(s string) {for i := 0; i < 2; i++ {fmt.Println(s)}}("world")// 主協程for i := 0; i < 2; i++ {fmt.Println("hello")runtime.Gosched()}
}
當我們運行此代碼時,執行一次 "hello"。之后中斷主協程,執行其他協程。之后,再次執行該go協程。
runtime.Goexit()
Goexit終止調用它的go協程。其它go協程不會受影響。Goexit會在終止該go協程前執行所有defer的函數。在程序的main go協程調用本函數,會終結 main函數的go協程,而不會讓main返回。因為main函數沒有返回,程序會繼續執行其它的go程。如果所有其它go協程都退出了,程序就會崩潰。
區別于,main函數執行完畢,main 協程結束。會自動終結函數內開啟的所有協程。
func main() {go func() {defer fmt.Println("A.defer")func() {defer fmt.Println("B.defer")// 結束協程runtime.Goexit()defer fmt.Println("C.defer")fmt.Println("B")}()fmt.Println("A")}()for {}
}
runtime.GOMAXPROCS()
GOMAXPROCS設置可同時執行的最大CPU數,并返回先前的設置。?若?n < 1,它就不會更改當前設置。本地機器的邏輯CPU數可通過?NumCPU?查詢。本函數在調度程序優化后會去掉。
我們看下面代碼: 用來驗證并行。
當我們設置go程序并發時占用的cpu核數為1時。我們得出的時間納秒差。
PS: 下面出現的差是不定的。
var atime int64
var btime int64func a() {for i := 1; i < 1000; i++ {fmt.Println("A:", i)}
}
func b() {for i := 1; i < 1000; i++ {fmt.Println("B:", i)}}func main() {runtime.GOMAXPROCS(1)atime = time.Now().UnixMicro() // 獲取當前時間納秒值go a()go b()runtime.Gosched()btime = time.Now().UnixMicro()fmt.Println(btime - atime)
}
當我們,設置執行該程序的cpu核數為2時。執行時間,大概率是0或者是接近于0的數。
var atime int64
var btime int64func a() {for i := 1; i < 1000; i++ {fmt.Println("A:", i)}
}
func b() {for i := 1; i < 1000; i++ {fmt.Println("B:", i)}}func main() {runtime.GOMAXPROCS(2)atime = time.Now().UnixMicro()go a()go b()runtime.Gosched()btime = time.Now().UnixMicro()fmt.Println("執行完其余協程:", btime-atime)
}
4. channel
單純地將函數并發執行是沒有意義的。函數與函數間需要交換數據才能體現并發執行函數的意義。
雖然可以使用共享內存進行數據交換,但是共享內存在不同的goroutine中容易發生競態問題。為了保證數據交換的正確性,必須使用互斥量對內存進行加鎖,這種做法勢必造成性能問題。
Go語言的并發模型是CSP(Communicating Sequential Processes),提倡通過通信共享內存而不是通過共享內存而實現通信。
如果說goroutine是Go程序并發的執行體,channel就是它們之間的連接。channel是可以讓一個goroutine發送特定值到另一個goroutine的通信機制。
Go 語言中的通道(channel)是一種特殊的類型。通道像一個傳送帶或者隊列,總是遵循先入先出(First In First Out)的規則,保證收發數據的順序。每一個通道都是一個具體類型的導管,也就是聲明channel的時候需要為其指定元素類型。
- channel: 在go中,用于協程之間的通信。
- 共享內存模型: 在共享內存模型中,多個線程可以訪問和修改同一個變量或數據結構。由于線程是并發執行的,它們之間的執行順序是不確定的。如果沒有適當的同步機制來保證原子性和可見性,就有可能導致數據的不一致性和意料之外的結果。
- CSP模型: CSP 模型通過強調通過通信來共享數據,而不是直接共享內存,可以避免一些線程安全問題。
channel操作?
?通道創建、發送、接受、關閉通道。
// 聲明通道,這時候,不能用的,需要make創建
var ch chan int// 創建指定大小的通道
ints := make(chan int,10)// 發送: 向ch通道發送10
ch <- 10 // 接受:接受來自ch通道的值
x := <-ch// 關閉通道
close(ch)
關于關閉通道需要注意的事情是,只有在通知接收方goroutine所有的數據都發送完畢的時候才需要關閉通道。通道是可以被垃圾回收機制回收的,它和關閉文件是不一樣的,在結束操作之后關閉文件是必須要做的,但關閉通道不是必須的。
但是協程之間通道遍歷取值,如果不關閉通道,遍歷的時候會報異常而寄。
1.對一個關閉的通道再發送值就會導致panic。2.對一個關閉的通道進行接收會一直獲取值直到通道為空。3.對一個關閉的并且沒有值的通道執行接收操作會得到對應類型的零值。4.關閉一個已經關閉的通道會導致panic。
?無緩沖的通道
?無緩沖通道又稱為被阻塞的通道。
func main() {ch := make(chan int)ch <- 10fmt.Println("發送成功")
}
至于為什么執行上面代碼,會報這個錯誤,原因在于,ch := make(chan int),創建是個無緩沖通道。而,
?無緩沖通道的發送操作會被阻塞,直到另一個協程在給通道上執行接受操作,這時值才能發送成功。
使用無緩沖通道操作進行通信時將導致發送和接受的協程同步化。(無緩沖通道使用時,有發送,就必須有接受)。因此,無緩沖通道也被稱為同步通道。
這個,無緩沖通道說白了,沒有實際存儲容量,就像快遞員一樣,接受了快遞(數據),必須要送到接受者手里。到最后,不能自己持有。
func main() {ch := make(chan int)go recv(ch)ch <- 10fmt.Println("發送成功")
}func recv(c chan int) {recvData := <-cfmt.Println("接受成功", recvData)
}
有緩沖通道
顧名思義,對比無緩沖通道創建時,是有實際存儲值的。就像快遞柜一樣。快遞員把快遞放到快遞柜里,但是接受者,還沒拿到(但這個過程沒有阻塞)。假如,當有下個快遞來臨時(假如,快遞柜只有1個位置)?。那么,沒有人接受,快遞柜沒位置會再次阻塞。
我們可以通過使用make創建通道時,指定容量,就是有緩沖的通道。
func main() {ch := make(chan int, 1) // 創建一個容量為1的有緩沖區通道ch <- 10fmt.Println("發送成功")
}
func main() {ints := make(chan int, 5)ints <- 1fmt.Println(len(ints), cap(ints)) // 1 5
}
只要通道的容量大于零,那么該通道就是有緩沖的通道,通道的容量表示通道中能存放元素的數量。就像你小區的快遞柜只有那么個多格子,格子滿了就裝不下了,就阻塞了,等到別人取走一個快遞員就能往里面放一個。
我們可以使用內置的len函數獲取通道內元素的數量,使用cap函數獲取通道的容量,雖然我們很少會這么做。
看到這,嗯,channel 專門用于協程之間通信,就是一個臨時的容器,當容器,為0時,你可以往里面輸入元素,但是要接著有輸出;當容量大于0時,你可以往里面存儲東西,這時候不用輸出,但是達到channel 容量頂峰時,你在想存入,必須把之前存入的輸出。
close()
close關閉通道,下次取值變為0,false
func main() {ch := make(chan int)go recv(ch)ch <- 10fmt.Println("發送成功")
}func recv(c chan int) {recvData := <-cfmt.Println("接受成功", recvData)close(c)data, ok := <-cfmt.Println(data, ok)
}
從通道遍歷取值
func main() {var ch1 = make(chan int)go func() {for i := 0; i < 10000; i++ {ch1 <- ifmt.Println("B send", i)}close(ch1)}()for i := range ch1 {fmt.Println("B recv", i)}}
單向通道
func main() {ch1 := make(chan int)go counter(ch1)printer(ch1)
}func counter(out chan<- int) { // 只能發送for i := 0; i < 100; i++ {out <- i * i}close(out)
}func printer(in <-chan int) { // 只能接受for i := range in {fmt.Println(i)}
}