目錄
基礎
- 並發:電腦同時聽歌,看小說,打游戲。cpu根據時間片進行划分,交替執行這三個程序。我們可以感覺是同時產生的。
- 並行:多個cpu(多核)上述動作同時執行
- C語言:,實現並發過程使用的是多線程(C++的最小資源單元)
- Golang:Golang中不是線程,而是
Go程
(goroutine),Go程是Golang原生支持的,每一個Go程占用的系統資源,遠遠小於線程,一個Go程大約需要4k到5k的內存資源,一個程序可以啟動大量的Go程序。相同的資源下,線程啟動幾十個,那么Go程是可以啟動成百上千個。Go程對於高並發,性能非常好 - Golang啟動Go程,只需要在函數前面加上關鍵字
go
即可 - 啟動多個子Go程它們會競爭cpu資源
package main
import (
"fmt"
"time"
)
func main() {
go func() {
count := 1
for {
fmt.Println("======>我是子go程:", count)
count++
time.Sleep(time.Second)
}
}()
count := 1
for {
fmt.Println("我是主go程:", count)
count++
time.Sleep(time.Second)
}
}
return、exit、goexit區別
- return:返回當前函數
- exit:退出當前進程
- goexit:提前退出當前go程
return
package main
import (
"fmt"
"time"
)
func main() {
go func() {
func(){
fmt.Println("這是子go程的內部函數")
return // 只是返回當前函數,對於上一層的函數會繼續執行
}()
fmt.Println("子go程結束")
}()
fmt.Println("這是主go程")
time.Sleep(5 * time.Second)
fmt.Println("over")
}
exit
package main
import (
"fmt"
"os"
"time"
)
func main() {
go func() {
func(){
fmt.Println("這是子go程的內部函數")
os.Exit(-1) // 退出進程
}()
fmt.Println("子go程結束")
}()
fmt.Println("這是主go程")
time.Sleep(5 * time.Second)
fmt.Println("over")
}
goexit
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
go func() {
func(){
fmt.Println("這是子go程的內部函數")
runtime.Goexit() // 退出當前go程
}()
fmt.Println("子go程結束")
}()
fmt.Println("這是主go程")
time.Sleep(5 * time.Second)
fmt.Println("over")
}
多go程通信(channel)
- 但涉及到多go程時,c語言使用互斥量,上鎖來保持資源同步,避免資源競爭問題
- Golang也支持這種方式,但是go語言更好的解決方案是使用管道、通道
- 使用通道不需要我們去進行加解鎖
- A往通道里面寫數據,B從管道里面讀數據,Golang自動幫我們做好了數據同步
package main
import "fmt"
func main() {
// 使用一個管道,一定要make,同map一樣,否則為nil
// 此時是無緩沖的管道,會寫一個讀一個,也稱無緩存管道
numChan := make(chan int)
// 如果創建的管道給入容量,那么將會批量寫入,也稱有緩沖通道
// numChan := make(chan int, 10)
// 創建兩個go程,父母寫數據,兒子讀數據。
// 這是go程“兒子”,從管道中讀取數據
go func() {
for i := 0; i < 50; i++ {
data := <-numChan
fmt.Println("<----這是go程“兒子”,讀取數據:", data)
}
}()
go func() {
for i := 0; i < 20; i++ {
// 這是go程“媽媽”,寫入20個數據
numChan <- i
fmt.Println("---->這是go“媽媽”,寫入數據:", i)
}
}()
for i := 20; i < 50; i++ {
// 這是go程“爸爸”,寫入30個數據
numChan <- i
fmt.Println("---->這是go程”爸爸“,寫入數據:", i)
}
}
管道的注意點
管道nil
如果管道沒有使用make分配空間,那么管道默認為nil,讀取寫入都會阻塞
package main
import "fmt"
func main() {
var numChan chan int
numChan <- 1
fmt.Println("numChan", <- numChan)
}
管道死鎖
當管道讀寫次數不一致的時候,如果阻塞在主go程,那么程序會崩潰,如果阻塞在子go程,那么會出現內存泄露
package main
import "fmt"
func main() {
numChan := make(chan int, 10)
// 寫入數據到管道
go func() {
for i := 0; i < 50; i++ {
numChan <- i
fmt.Println("寫入數據:", i)
}
}()
// 讀,當主程序被管道阻塞時,那么程序將鎖死崩潰
for i := 0; i < 60; i++ {
fmt.Println("numChan:", <-numChan)
}
}
for range遍歷管道
for range是不知道管道是否寫完,所以會一直等待,一直等待就會導致死鎖
package main
import "fmt"
func main() {
numChan := make(chan int, 10)
go func() {
for i := 0; i < 50; i++ {
numChan <- i
fmt.Println("寫入數據:", i)
}
}()
// 遍歷管道時,只返回值,不返回坐標
for val := range numChan{
fmt.Println("讀取數據:",val)
}
}
在寫入端,將管道關閉,for range遍歷關閉管道(nil)時,會退出就不會導致死鎖
package main
import "fmt"
func main() {
numChan := make(chan int, 10)
go func() {
for i := 0; i < 50; i++ {
numChan <- i
fmt.Println("寫入數據:", i)
}
// 手動關閉管道
close(numChan)
}()
for val := range numChan{
fmt.Println("讀取數據:",val)
}
}
判斷管道是否已經關閉
我們如何知道一個管道的狀態,如果已經關閉了,讀沒事,會返回零值,如果再寫入的話會有崩潰風險
有沒有類似於map讀取的那種方式val, ok := numMap[0]
的這種ok-idiom
方式知道呢
package main
import "fmt"
func main() {
numChan := make(chan int, 10)
go func() {
for i := 0; i < 10; i++ {
numChan <- i
fmt.Println("寫入數據:", i)
}
// 手動關閉管道
close(numChan)
}()
for {
val, ok := <-numChan
if ok {
fmt.Println("讀取數據:", val)
} else {
fmt.Println("管道已經關閉")
break
}
}
}
單向通道
numChan := make(chan int, 10)
雙向通道,既可以讀,也可以寫- 單向通道:這樣的設計是為了明確語義,一般用於函數參數
- 單向讀通道:
var numChanReadOnly <- chan int
- 單向寫通道:
var numChanWriteOnly chan <- int
- 單向讀通道:
- 雙向管道可以賦值給同類型的單向管道,但單向通道不能賦值給同類型的雙向通道
package main
import (
"fmt"
"time"
)
func main() {
// 生產者消費者模型(、producer)
// C語言:數組+鎖,thread1:寫,thread2:讀
// Golang:goroutine + channel
// 在主函數中創建一個雙向通道
numChan := make(chan int, 10)
// 雙向管道可以賦值給同類型的單向管道
// 將numChan,傳遞給producer,負責生產
go producer(numChan)
// 將numChan,傳遞給consumer 負責消費
go consumer(numChan)
time.Sleep(5 * time.Second)
}
// 生產者,提供一個只寫通道
func producer(write chan<- int) {
for i := 0; i < 50; i++ {
write <- i
// 寫管道中不允許讀操作
// data <- write
fmt.Println("向管道中寫入數據:", i)
}
}
// 消費者,提供一個只讀通道
func consumer(read <-chan int) {
// 讀通道不允許寫入操作
// read <- 12
for val := range read {
fmt.Println("向管道中寫入數據:", val)
}
}
管道監聽(select)
當程序中有多個channel
協同工作,chan1
,chan2
,某一時刻,chan1
或chan2
觸發了,程序要做出處理,使用select
來監聽多個通道,當管道被觸發時(寫入數據、讀取數據、關閉管道),select
語法與switch case
很像,但是所有的分支條件必須是管道io
package main
import (
"fmt"
"time"
)
func main() {
// 啟動一個go程,負責監聽兩個channel
chan1 := make(chan int)
chan2 := make(chan int)
go func() {
for {
select {
case val := <-chan1:
fmt.Println("從chan1讀取數據成功:", val)
case val2 := <-chan2:
fmt.Println("從chan2讀取數據成功:", val2)
}
}
}()
go func() {
for i := 0; i < 10; i++ {
chan1 <- i
time.Sleep(time.Second)
}
}()
go func() {
for i := 0; i < 10; i++ {
chan2 <- i
time.Sleep(time.Second)
}
}()
for{
}
}
管道總結
- 當管道寫滿了,寫阻塞
- 當緩沖區讀完了,讀阻塞
- 如果管道沒有使用make分配空間,管道默認nil
- 從nil管道讀取/寫入數據,都會阻塞(不會崩潰)
- 從一個已經close的管道讀取/寫入數據時,會返回零值(不會崩潰)
- 一個管道,如果重復關閉,程序會崩潰
- 關閉管道的動作,一定要在寫管道的操作方執行,不應該放在讀端,否則繼續寫會崩潰
- 讀寫通道次數一定要對等
- 否則在多個go程中,會出現資源泄露
- 在主go程中,會出現程序崩潰(deadlock)