進程、線程以及并行、并發
關于進程和線程
一個進程至少有 5 種基本狀態,它們是:初始態,執行態,等待狀態,就緒狀態,終止狀態。
關于并行和并發
多線程程序在單核 CPU 上面運行就是并發多線程程序在多核 CUP 上運行就是并行。

?Golang 中的協程(goroutine)以及主線程
Golang 中多協程可以實現并行或者并發。

Goroutine 的使用
package mainimport("fmt""time"
)// 在主線程中也每隔10毫輸出"衛宮士郎", 輸出2次后,退出程序
// 要求主線程和goroutine同時執行
func test() {for i := 0; i < 10; i++ {fmt.Println("test() 測試專用..........")time.Sleep(time.Millisecond * 100)}
}func main(){go test()for i := 1; i <=2; i++ {fmt.Println("main () 衛宮士郎")time.Sleep(time.Millisecond*10)}}
暴露出一個問題:主線程執行完畢后即使協程沒有執行完畢
所以我們對代碼進行改造,可以讓主線程和協程并行的同時,主線程執行完畢還不會同時帶領協程退出運行。
?注意:
1、主線程執行完畢后即使協程沒有執行完畢程序也會退出
2、協程可以在主線程沒有執行完畢前提前退出協程是否執行完畢不會影響主線程的執行為了保證我們的程序可以順利執行我們想讓協程執行完畢后在執行主進程退出。
這個時候我們可以使用sync.WaitGroup 等待協程執行完畢
?sync.WaitGroup
package mainimport("fmt""time""sync"
)// 在主線程中也每隔10毫輸出"衛宮士郎", 輸出2次后,退出程序
// 要求主線程和goroutine同時執行
//主線程退出后所有的協程無論有沒有執行完畢都會退出,所以我們在主進程中可以通過WaitGroup等待協程執行完畢
var sw sync.WaitGroupfunc test() {for i := 0; i < 10; i++ {fmt.Println("test() 測試專用..........")time.Sleep(time.Millisecond * 100)}sw.Done() //協程計數器-1
}func main(){sw.Add(1) //協程計數器+1go test()//表示開啟一個協程for i := 1; i <=2; i++ {fmt.Println("main () 衛宮士郎")time.Sleep(time.Millisecond*10)}sw.Wait() //等待協程執行完畢...fmt.Println("主線程執行完畢、、、、、、")
}
啟動多個 Goroutine?
package mainimport("fmt""time""sync"
)// 多個協程Goroutine啟動var sw sync.WaitGroupfunc test0() {for i := 0; i < 5; i++ {fmt.Println("test0() 測試專用..........")time.Sleep(time.Millisecond * 100)}sw.Done() //協程計數器-1
}func test1() {for i := 0; i < 5; i++ {fmt.Println("test1() 測試專用..........")time.Sleep(time.Millisecond * 100)}sw.Done() //協程計數器-1
}func main(){sw.Add(1) //協程計數器+1go test0()//表示開啟一個協程sw.Add(1)//協程計數器+1go test1()//表示開啟一個協程for i := 1; i <=2; i++ {fmt.Println("main () 衛宮士郎")time.Sleep(time.Millisecond*10)}sw.Wait() //等待協程執行完畢...fmt.Println("主線程執行完畢、、、、、、")
}
設置 Golang 并行運行的時候占用的 cup 數量
package mainimport ("fmt""runtime"
)func main() {//獲取當前計算機上面的Cup個數cpuNum := runtime.NumCPU()fmt.Println("cpuNum=", cpuNum)//可以自己設置使用多個cpuruntime.GOMAXPROCS(cpuNum - 1)fmt.Println("設置完成")
}//cpuNum= 8
//設置完成
來求一個素數的操作如下:
package mainimport ("fmt""time"
)func main() {start := time.Now().Unix()fmt.Println(start)for num := 2; num < 10; num++ {var flag = truefor i := 2; i < num; i++ {if num%i == 0 {flag = falsebreak}}if flag {fmt.Println(num, "是素數")}}end := time.Now().Unix()fmt.Println(end)fmt.Println(end-start) }
goroutine ?for循環實現
package mainimport ("fmt""sync""time"
)//需求:要統計1-120000的數字中那些是素數?goroutine for循環實現/*
1 協程 統計 1-300002 協程 統計 30001-600003 協程 統計 60001-900004 協程 統計 90001-120000// start:(n-1)*30000+1 end:n*30000
*/
var wg sync.WaitGroupfunc test(n int) {for num := (n-1)*30000 + 1; num < n*30000; num++ {if num > 1 {var flag = truefor i := 2; i < num; i++ {if num%i == 0 {flag = falsebreak}}if flag {// fmt.Println(num, "是素數")}}}wg.Done()
}func main() {for i := 1; i <= 4; i++ {wg.Add(1)go test(i)}wg.Wait()fmt.Println("執行完畢")}
Channel 管道
channel
單純地將函數并發執行是沒有意義的。
函數與函數間需要交換數據才能體現并發執行函數的意義。
雖然可以使用共享內存進行數據交換,但是共享內存在不同的goroutine中容易發生競態問題。為了保證數據交換的正確性,必須使用互斥量對內存進行加鎖,這種做法勢必造成性能問題。
Go語言的并發模型是CSP(Communicating Sequential Processes),提倡通過通信共享內存而不是通過共享內存而實現通信。
如果說goroutine是Go程序并發的執行體,channel就是它們之間的連接。
channel是可以讓一個goroutine發送特定值到另一個goroutine的通信機制。
Go 語言中的通道(channel)是一種特殊的類型。通道像一個傳送帶或者隊列,總是遵循先入先出(First In First Out)的規則,保證收發數據的順序。每一個通道都是一個具體類型的導管,也就是聲明channel的時候需要為其指定元素類型。
channel類型
channel是一種類型,一種引用類型。聲明通道類型的格式如下:
var 變量 chan 元素類型
舉幾個例子:
var ch1 chan int // 聲明一個傳遞整型的通道var ch2 chan bool // 聲明一個傳遞布爾型的通道var ch3 chan []int // 聲明一個傳遞int切片的通道
創建channel
通道是引用類型,通道類型的空值是nil。
var ch chan int
fmt.Println(ch) // <nil>
package mainimport "fmt"func main() {ch1 := make(chan int ,4)ch1<- 1ch1<- 2ch1<- 3ch2 := ch1ch2<-4<-ch1<-ch1<-ch1d:= <-ch1fmt.Println(d)
}//4
副本ch2的值添加后,取出ch1的值改變了
聲明的通道后需要使用make函數初始化之后才能使用。
創建channel的格式如下:
make(chan 元素類型, [緩沖大小])
channel的緩沖大小是可選的。
舉幾個例子:
//創建一個能存儲 10 個 int 類型數據的管道
ch1 := make(chan int, 10)
//創建一個能存儲 4 個 bool 類型數據的管道
ch2 := make(chan bool, 4)
//創建一個能存儲 3 個[]int 切片類型數據的管道
ch3 := make(chan []int, 3)
package mainimport "fmt"func main() {//創建channelch := make(chan int, 3)//2、給管道里面存儲數據ch <- 12ch <- 33ch <- 3234//獲取管道里面的內容a := <-chfmt.Println(a) //12<-ch //從管道里面取值 //33c := <-chfmt.Println(c) //3234ch <- 1ch <- 22//打印管道的長度和容量fmt.Printf("值:%v 容量:%v 長度%v\n", ch, cap(ch), len(ch))
}
已經消費了的,就相當于沒有,再添加的從新算?
channel操作
通道有發送(send)、接收(receive)和關閉(close)三種操作。
發送和接收都使用<-符號。
現在我們先使用以下語句定義一個通道:
ch := make(chan int)
發送
將一個值發送到通道中。
ch <- 10 // 把10發送到ch中
接收
從一個通道中接收值。
x := <- ch // 從ch中接收值并賦值給變量x
<-ch // 從ch中接收值,忽略結果
關閉
我們通過調用內置的close函數來關閉通道。
close(ch)
?關于關閉通道需要注意的事情是,只有在通知接收方goroutine所有的數據都發送完畢的時候才需要關閉通道。通道是可以被垃圾回收機制回收的,它和關閉文件是不一樣的,在結束操作之后關閉文件是必須要做的,但關閉通道不是必須的。
關閉后的通道有以下特點:
1.對一個關閉的通道再發送值就會導致panic。2.對一個關閉的通道進行接收會一直獲取值直到通道為空。3.對一個關閉的并且沒有值的通道執行接收操作會得到對應類型的零值。4.關閉一個已經關閉的通道會導致panic。
管道阻塞
無緩沖的通道
package main
import ("fmt"
)func main() {ch := make(chan int)ch <- 123fmt.Println("傳遞成功......")
}
上面這段代碼能夠通過編譯,但是執行的時候會出現以下錯誤:
fatal error: all goroutines are asleep - deadlock!goroutine 1 [chan send]:
main.main()E:/goroutine_channel_demo/route_demo/main.go:8 +0x31
exit status 2
為什么會出現死鎖
因為我們使用ch := make(chan int)創建的是無緩沖的通道,無緩沖的通道只有在有接收值的時候才能發送值。(小區沒代收快遞點,需要快遞小哥直接送到手上)
上面的代碼會阻塞在ch <- i這一行代碼形成死鎖
因為我們使用ch := make(chan int)創建的是無緩沖的通道,無緩沖的通道只有在有人接收值的時候才能發送值。
上面的代碼會阻塞在ch <- 123這一行代碼形成死鎖,那如何解決這個問題呢?
一種方法是啟用一個goroutine去接收值,例如:
func recv(c chan int) {ret := <-cfmt.Println("接收成功", ret)
}
func main() {ch := make(chan int)go recv(ch) // 啟用goroutine從通道接收值ch <- 10fmt.Println("發送成功")
}
無緩沖通道上的發送操作會阻塞,直到另一個goroutine在該通道上執行接收操作,這時值才能發送成功,兩個goroutine將繼續執行。
有緩沖的通道
解決上面問題的方法還有一種就是使用有緩沖區的通道。
package main
import ("fmt"
)// func recover(ch chan int){
// rec := <- ch
// fmt.Println("接收成功",rec)
// }func main() {ch := make(chan int,1)// go recover(ch)ch <- 123fmt.Println("傳遞成功......")
}
只要通道的容量大于零,那么該通道就是有緩沖的通道,通道的容量表示通道中能存放元素的數量。(小區快遞格子就一個,你取走了,別人能再放)
?循環遍歷管道數據
循環的話,我們就會提到for,但是for有兩種循環形式
for range 和 for 用兩種方式來操作
for range循環遍歷管道的值 ?,注意:管道沒有key
package mainimport "fmt"func main() {ch1 := make(chan int,5)for i := 1; i <= 5; i++ {ch1 <- i}for v := range ch1 {fmt.Println(v)}}
我們發現雖然可以正常編譯,運行,但是會出現如下情況:
1
2
3
4
5
fatal error: all goroutines are asleep - deadlock!goroutine 1 [chan receive]:
main.main()E:/goroutine_channel_demo/route_demo/main.go:14 +0xb4
exit status 2
這樣也會產生死鎖,使用for range遍歷通道,當通道被關閉的時候就會退出for range,如果沒有關閉管道就會報錯fatal error: all goroutines are asleep - deadlock!
如果通過for range循環的方式來從管道取數據,在插入數據的時候一定要close()
package main
import ("fmt"
)func main() {var ch1 = make(chan int, 5)for i := 1; i <= 5; i++ {ch1 <- i}close(ch1) //關閉管道//for range循環遍歷管道的值 ,注意:管道沒有keyfor v := range ch1 {fmt.Println(v)}
}
通過內置的close()函數關閉channel(如果你的管道不往里存值或者取值的時候一定記得關閉管道)
?第二種方法
package main
import ("fmt"
)func main() {//通過for循環遍歷管道的時候管道可以不關閉var ch2 = make(chan int, 5)for i := 1; i <= 5; i++ {ch2 <- i}for j := 0; j < 5; j++ {fmt.Println(<-ch2)}
}
并發安全和鎖
有時候在Go代碼中可能會存在多個goroutine同時操作一個資源(臨界區),這種情況會發生競態問題(數據競態)。
互斥鎖
package mainimport ("fmt""sync""time"
)var count = 0
var sw sync.WaitGroupvar mutex sync.Mutexfunc test() {mutex.Lock()count++fmt.Println("the count is : ", count)time.Sleep(time.Millisecond)mutex.Unlock()sw.Done()
}func main() {for r := 0; r < 20; r++ { //開啟20個協程來進行這個操作wg.Add(1)go test()}sw.Wait()}
使用互斥鎖能夠保證同一時間有且只有一個 goroutine 進入臨界區,其他的 goroutine 則在等待鎖;當互斥鎖釋放后,等待的 goroutine 才可以獲取鎖進入臨界區,多個 goroutine 同時等待一個鎖時,喚醒的策略是隨機的。雖然使用互斥鎖能解決資源爭奪問題,但是并不完美,通過全局變量加鎖同步來實現通訊,并不利于多個協程對全局變量的讀寫操作。這個時候我們也可以通過另一種方式來實現上面的功能管道(Channel)
讀寫互斥鎖
互斥鎖是完全互斥的,但是有很多實際的場景下是讀多寫少的,當我們并發的去讀取一個資源不涉及資源修改的時候是沒有必要加鎖的,這種場景下使用讀寫鎖是更好的一種選擇。
讀寫鎖在Go語言中使用sync包中的RWMutex類型。
讀寫鎖分為兩種:讀鎖和寫鎖。當一個goroutine獲取讀鎖之后,其他的goroutine如果是獲取讀鎖會繼續獲得鎖,如果是獲取寫鎖就會等待;
當一個goroutine獲取寫鎖之后,其他的goroutine無論是獲取讀鎖還是寫鎖都會等待。
package mainimport("fmt""sync""time"
)var (x int64wg sync.WaitGrouplock sync.Mutexrwlock sync.RWMutex
)func write() {// lock.Lock() // 加互斥鎖rwlock.Lock() // 加寫鎖x = x + 1time.Sleep(10 * time.Millisecond) // 假設讀操作耗時10毫秒fmt.Println("=========進行寫操作")rwlock.Unlock() // 解寫鎖// lock.Unlock() // 解互斥鎖wg.Done()
}func read() {// lock.Lock() // 加互斥鎖rwlock.RLock() // 加讀鎖time.Sleep(time.Millisecond) // 假設讀操作耗時1毫秒fmt.Println("=========進行讀操作")rwlock.RUnlock() // 解讀鎖// lock.Unlock() // 解互斥鎖wg.Done()
}func main() {for i := 0; i < 3; i++ {wg.Add(1)go write()}for i := 0; i < 10; i++ {wg.Add(1)go read()}wg.Wait()}/**/
(也就是說,當一個 goroutine 進行寫操作的時候,其他 goroutine 既不能進行讀操作,也不能進行寫操作)
Goroutine 結合 Channel 管道
需求 1:
1 、開啟一個 fn1 的的協程給向管道 inChan 中寫入 100 條數據2 、開啟一個 fn2 的協程讀取 inChan 中寫入的數據3 、注意: fn1 和 fn2 同時操作一個管道4 、主線程必須等待操作完成后才可以退出
package mainimport ("fmt""sync""time"
)
//這是一個無緩存通道案例
//定義sync等待協程完畢
var wg sync.WaitGroupfunc fn1(intChan chan int) {for i := 0; i < 10; i++ {intChan <- i + 1fmt.Println("寫入數據=", i+1)time.Sleep(time.Millisecond * 100)}close(intChan) //寫入操作完畢,關閉寫入的協程wg.Done()
}
func fn2(intChan chan int) {for v := range intChan { //通道回顯只有一個值fmt.Printf("讀到數據=%v\n", v)time.Sleep(time.Millisecond * 50)}wg.Done()
}
func main() {allChan := make(chan int, 100)wg.Add(1)go fn1(allChan)wg.Add(1)go fn2(allChan)wg.Wait()fmt.Println("讀取完畢...")
}
?需求 2:
goroutine 結合 channel 實現統計 1-120000 的數字中那些是素數?
package mainimport("fmt""sync"
)var sw sync.WaitGroup
//向 intChan放入 1-120個數,創建協程
func putNum(intChan chan int ){for i := 0; i < 120; i++ {intChan <- i}close(intChan)sw.Done()
}// 從 intChan取出數據,并判斷是否為素數,如果是,就把得到的素數放在primeChanfunc primeNum(intChan chan int,primeChan chan int, exitChan chan bool ){for num := range intChan {var flag = truefor i := 2; i < num; i++ {if num%i == 0 {flag = falsebreak}}if flag {primeChan <- num //num是素數}
}//要關閉 primeChan// close(primeChan) //如果一個channel關閉了就沒法給這個channel發送數據了//什么時候關閉primeChan?//給exitChan里面放入一條數據exitChan <- true sw.Done()}//printPrime打印素數的方法
func printPrime(primeChan chan int) {for v := range primeChan {fmt.Println(v)}sw.Done()
}func main(){intChan := make(chan int,1000) //在intchan中放入數字primeChan := make(chan int,1000) //從 intChan取出數據,判斷是否是素數exitChan := make(chan bool ,20) //標識primeChan close,內部數據滿足設定的緩存數量就關閉//存放數字的協程sw.Add(1)go putNum(intChan)//統計素數的協程for i := 0; i < 20; i++ { //你要開啟幾個primechan的協程就寫幾個,對應的exitchan要一致sw.Add(1)go primeNum(intChan ,primeChan , exitChan )}//打印素數的協程sw.Add(1)go printPrime(primeChan)//判斷exitChan是否存滿值sw.Add(1)go func() {for i := 0; i < 20; i++ {<-exitChan}close(primeChan) //關閉primeChansw.Done()}()sw.Wait()fmt.Println("執行完畢....")}
單向管道
package mainimport "fmt"//單向管道
func main() {// 1、在默認情況下下,管道是雙向ch := make(chan int, 2)ch <- 1ch <- 2a := <-chb := <-chfmt.Println(a, b) //1,2// 2、管道聲明為只寫ch1 := make(chan<- int, 2)ch1 <- 10ch1 <- 12// <-ch1 //receive from send-only type chan<- int// 3、管道聲明為只讀ch2 := make(<-chan int, 2)ch2 <- 3c := <-ch2fmt.Println(c) //.\main.go:25:2: invalid operation: cannot send to receive-only channel ch2 (variable of type <-chan int)}
修改之前的案例如下:
package mainimport ("fmt""sync""time"
)
//這是一個無緩存通道案例
//定義sync等待協程完畢
var wg sync.WaitGroupfunc fn1(intChan chan<- int) {for i := 0; i < 10; i++ {intChan <- i + 1fmt.Println("寫入數據=", i+1)time.Sleep(time.Millisecond * 100)}close(intChan) //寫入操作完畢,關閉寫入的協程wg.Done()
}
func fn2(intChan <-chan int) {for v := range intChan { //通道回顯只有一個值fmt.Printf("讀到數據=%v\n", v)time.Sleep(time.Millisecond * 50)}wg.Done()
}
func main() {allChan := make(chan int, 100)wg.Add(1)go fn1(allChan)wg.Add(1)go fn2(allChan)wg.Wait()fmt.Println("讀取完畢...")
}/*
寫入數據= 1
讀到數據=1
寫入數據= 2
讀到數據=2
寫入數據= 3
讀到數據=3
寫入數據= 4
讀到數據=4
寫入數據= 5
讀到數據=5
寫入數據= 6
讀到數據=6
寫入數據= 7
讀到數據=7
寫入數據= 8
讀到數據=8
寫入數據= 9
讀到數據=9
寫入數據= 10
讀到數據=10
讀取完畢...
*/
?select 多路復用
在某些場景下我們需要同時從多個通道接收數據,這個時候就可以用到golang中給我們提供的select多路復用
如果只想在main方法內進行,就可以用這個方法,其他的就是定義協程了
使用select來獲取channel里面的數據的時候不需要關閉channel
package mainimport("fmt""time"
)func main(){
// 在某些場景下我們需要同時從多個通道接收數據,這個時候就可以用到golang中給我們提供的select多路復用//如果只想在main方法內進行,就可以用這個方法,其他的就是定義協程了//1.定義一個管道 10個數據int
intoChan := make(chan int ,10)
for i := 0; i < 10; i++ {intoChan <- i
}
//2.定義一個管道 5個數據string
stringChan := make(chan string,5)
for i := 0; i < 5; i++ {stringChan <- "衛宮士郎"
}
//定義一個for的無限循環
for{select{case value := <- intoChan:fmt.Printf("從 intChan 讀取的數據%d\n", value)case value := <-stringChan:fmt.Printf("從 stringChan 讀取的數據%v\n", value)time.Sleep(time.Millisecond * 50)default:fmt.Printf("數據獲取完畢")return //注意退出...}
}}/*
從 stringChan 讀取的數據衛宮士郎
從 stringChan 讀取的數據衛宮士郎
從 intChan 讀取的數據0
從 intChan 讀取的數據1
從 stringChan 讀取的數據衛宮士郎
從 intChan 讀取的數據2
從 intChan 讀取的數據3
從 stringChan 讀取的數據衛宮士郎
從 intChan 讀取的數據4
從 stringChan 讀取的數據衛宮士郎
從 intChan 讀取的數據5
從 intChan 讀取的數據6
從 intChan 讀取的數據7
從 intChan 讀取的數據8
從 intChan 讀取的數據9
數據獲取完畢*/
Goroutine Recover 解決協程中出現的 Panic
defer + recover
延遲執行(定義的func自執行函數出現問題就交給defer)
其他的協程還可以繼續進行
package mainimport ("fmt""time"
)//函數
func test0() {for i := 0; i < 10; i++ {time.Sleep(time.Millisecond * 50)fmt.Println("遠坂凜")}
}//函數
func test1() {//這里我們可以使用defer + recover //延遲執行(定義的func自執行函數出現問題就交給defer)//其他的協程還可以繼續進行defer func() {//捕獲test拋出的panicif err := recover(); err != nil {fmt.Println("test1() 發生錯誤", err)}}()//定義了一個mapvar myMap map[int]stringmyMap[0] = "golang" //error}func main() {go test0()go test1()//防止主進程退出這里使用time.Sleep演示,搭建也可以用sync.WaitGrouptime.Sleep(time.Second)
}
注意,調用recover()
來捕獲 goroutine 恐慌只在一個defer
函數內部有用;否則,該函數將返回nil
并且沒有其他作用。這是因為defer
函數也是在周圍函數恐慌時執行的。
在 Go 中,panic
是一個停止普通流程的內置函數:
func main() {fmt.Println("a")panic("foo")fmt.Println("b")
}
該代碼打印a
,然后在打印b
之前停止:
a
panic: foogoroutine 1 [running]:
main.main()main.go:7 +0xb3
一旦恐慌被觸發,它將繼續在調用棧中向上運行,直到當前的 goroutine 返回或者panic
被recover
捕獲:
func main() {defer func() { // ?if r := recover(); r != nil {fmt.Println("recover", r)}}()f() // ?
}func f() {fmt.Println("a")panic("foo")fmt.Println("b")
}
? 延遲閉包內調用recover
? 調用f
,f
恐慌。這種恐慌被前面的recover
所抓住。
在f
函數中,一旦panic
被調用,就停止當前函數的執行,并向上調用棧:main
。在main
中,因為恐慌是由recover
引起的,所以并不停止 goroutine:
a
recover foo