Golang語言的核心特色
Goroutine
基本介紹
進程和線程介紹
-
進程就是程序在操作系統中的一次執行過程,是系統進行資源分配和調度的基本單位
-
線程是進程的一個執行實例,是程序執行的最小單元,它是比進程更小的能獨立運行的基本單位
-
一個進程可以創建和銷毀多個線程,同一個進程中的多個線程可以並發執行
-
一個程序至少有一個進程,一個進程至少有一個線程
程序、進程和線程的關系示意圖
並發和並行
-
多線程程序在單核上運行,就是並發
-
多線程程序在多核上運行,就是並行
並發:因為是在一個cpu上,比如有10個線程,每個線程執行10毫秒(進行輪詢操作),從人的角度看,好像這10個線程都在運行,但是從微觀上看,在某一個時間點看,其實只有一個線程在執行,這就是並發
並行:因為是在多個cpu上(比如有10個cpu),比如有10個線程,每個線程執行10毫秒(各自在不同cpu上執行),從人的角度看,這10個線程都在運行,但是從微觀上看,在某一個時間點看,也同時有10個線程在執行,這就是並行
Go協程和Go主線程
Go主線程(有程序員直接稱為線程/也可以理解成進程):一個Go線程上,可以起多個協程,可以這樣理解:協程是輕量級的線程【編譯器做優化】
Go協程的特點
1) 有獨立的棧空間
2) 共享程序堆空間
3) 調度由用戶控制
4) 協程是輕量級的線程
快速入門
案例說明
編寫一個程序,完成如下功能:
1) 在主線程(可以理解成進程)中,開啟一個goroutine,該協程每隔一秒輸出“hello,world”
2) 在主線程中也每隔一秒輸出“hello,world”,輸出10次后,退出程序
3) 要求主線程和goroutine同時執行
畫出主線程和協程執行流程圖
import (
"fmt"
"strconv"
"time"
)
//編寫一個函數/每隔一秒輸出"hello,world"
func test() {
for i := 1; i <= 10; i++ {
fmt.Println("test() hello,world" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
func main() {
go test() //開啟了一個協程
for i := 1; i <= 10; i++ {
fmt.Println("main() hello,world" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
//main() hello,world1 //main主線程和test協程同時執行
//test() hello,world1
//main() hello,world2
//test() hello,world2
//......
小結
-
主線程是一個物理線程,直接作用在cpu上的。是重量級的,非常耗費cpu資源
-
協程從主線程開啟的,是輕量級的線程,是邏輯態。對資源消耗相對小
-
Go的協程機制是重要的特點,可以輕松的開啟上萬個協程。其它編程語言的並發機制一般是基於線程的,開啟過多的線程,資源耗費大,這里就突顯了Go在並發上的優勢了
goroutine的調度模型
-
M:操作系統的主線程(是物理線程)
-
P:協程執行需要的上下文
-
G:協程
MPG模式運行的狀態 -1
-
當前程序有三個M,如果三個M都在一個cpu上運行,就是並發,如果在不同的cpu上運行就是並行
-
M1,M2,M3正在執行一個G,M1的協程隊列有三個,M2的協程隊列有三個,M3的協程隊列有兩個
-
從上圖可以看到:Go的協程是輕量級的線程,是邏輯態的,Go可以容易的起上萬個協程
-
其它程序c/java的多線程,往往是內核態的,比較重量級,幾千個線程可能耗光cpu
MPG模式運行的狀態 - 2
-
分成兩個部分來看
-
原來的情況是MO主線程正在執行Go協程,另外有三個協程在隊列等待
-
如果Go協程阻塞,比如讀取文件或者數據庫等
-
這時就會創建M1主線程(也可能是從已有的線程池中取出M1),並且將等待的3個協程掛到M1下開始執行,M0的主線程下的Go仍然執行文件io的讀寫
-
這樣的MPG調度模式,可以既讓Go執行,同時也不會讓隊列的其它協程一直阻塞,仍然可以並發/並行執行
-
等到Go不阻塞了,M0會被放到空閑的主線程繼續執行(從已有的線程池中取),同時Go又會被喚醒
設置Go運行的CPU數
為了充分利用多cpu的優勢,在Go程序中,設置運行的cpu數目
import (
"fmt"
"runtime"
)
func main() {
//獲取當前系統cpu的數目
num := runtime.NumCPU()
//這里設置num - 1的cpu運行Go程序
runtime.GOMAXPROCS(num - 1)
fmt.Println("num = ", num)
}
Go1.8后,默認讓程序運行在多核上,可以不用設置
Go1.8前,還是要設置一下,可以更高效的利用cpu
Channel(管道)
看個需求
需求:現在要計算1 - 200 的各個數的階乘,並且把各個數的階乘放入到map中,最后顯示出來
要求:使用goroutine
分析思路
1) 使用goroutine來完成,效率高,但是會出現並發/並行安全問題
2) 這里就提出了不同goroutine如何通信的問題
代碼區
1) 使用goroutine來完成(看看使用goroutine並發完成會出現什么問題?然后再去解決)
2) 在運行某個程序時,如何知道是否存在資源競爭問題。方法很簡單,在編譯該程序時,增加一個參數 - race 即可
示意圖
import (
"fmt"
"time"
)
//思路
//1. 編寫一個函數,計算各個數的階乘,並放入到map中
//2. 啟動的協程多個,統計的結果放入到map中
//3. map應該做出一個全局的
var (
myMap = make(map[int]int,10)
)
//test函數就是計算n!,將這個結果放入到myMap
func test(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
//這里將res 放入到myMap
myMap[n] = res // concurrent map writes?
}
func main() {
//這里開啟多個協程完成這個任務[200個]
for i := 1; i <= 200; i++ {
go test(i)
}
//休眠10秒鍾【第二個問題】
time.Sleep(time.Second * 10)
//這里輸出結果,遍歷這個結果
for i, v := range myMap {
fmt.Printf("map[%d] = %d\n", i, v)
}
}
//fatal error: concurrent map writes
//
//goroutine 55 [running]:
//runtime.throw(0x4d6d6d, 0x15)
// E:/GO/go/src/runtime/panic.go:774 +0x79 fp=0xc0000eff60 sp=0xc0000eff30 pc=0x42d229
//runtime.mapassign_fast64(0x4b6240, 0xc00005c330, 0x31, 0x0)
// E:/GO/go/src/runtime/map_fast64.go:101 +0x357 fp=0xc0000effa0 sp=0xc0000eff60 pc=0x410167
//main.test(0x31)
// E:/gostudent/src/2020-04-06/main.go:21 +0x6b fp=0xc0000effd8 sp=0xc0000effa0 pc=0x49c72b
//runtime.goexit()
// E:/GO/go/src/runtime/asm_amd64.s:1357 +0x1 fp=0xc0000effe0 sp=0xc0000effd8 pc=0x4556a1
//created by main.main
// E:/gostudent/src/2020-04-06/main.go:26 +0x5f
//
//goroutine 1 [runnable]:
//time.Sleep(0x2540be400)
// E:/GO/go/src/runtime/time.go:84 +0x248
//main.main()
// E:/gostudent/src/2020-04-06/main.go:29 +0x82
不同goroutine之間如何通訊
-
全局變量的互斥鎖
-
使用管道channel來解決
使用全局變量加鎖同步改進程序
因為沒有對全局變量m加鎖,因此會出現資源爭奪問題,代碼會出現錯誤,提示concurrent map writes
解決方案:加入互斥鎖
數的階乘很大,結果會越界,可以將求階乘改成sum += uint64(i)
代碼區改進
package main
import (
"fmt"
"sync"
"time"
)
//思路
//1. 編寫一個函數,計算各個數的階乘,並放入到map中
//2. 啟動的協程多個,統計的結果放入到map中
//3. map應該做出一個全局的
var (
myMap = make(map[uint]uint,10)
//聲明一個全局的互斥鎖
//lock 是一個全局的互斥鎖
//sync 是包:synchornized 同步
//Mutex :是互斥
lock sync.Mutex
)
//test函數就是計算n!,將這個結果放入到myMap
func test(n uint) {
var res uint = 1
var i uint = 1
for ; i <= n; i++ {
res *= i
}
//這里將res 放入到myMap
//加鎖
lock.Lock()
myMap[n] = res // concurrent map writes?
//解鎖
lock.Unlock()
}
func main() {
//這里開啟多個協程完成這個任務[200個]
var i uint = 1
for ; i <= 200; i++ {
go test(i)
}
//休眠10秒鍾【第二個問題】
time.Sleep(time.Second * 10)
//這里輸出結果,遍歷這個結果
lock.Lock()
for i, v := range myMap {
fmt.Printf("map[%d] = %d\n", i, v)
}
lock.Unlock()
}
需求注意的是:uint64最大到20的階乘,大整數可以使用math/big 來進行 實例:https://blog.csdn.net/hudmhacker/article/details/90081630
為什么需要channel
-
前面使用全局變量加鎖同步來解決goroutine的通訊,但不完美
-
主線程在等待所有gorountine全部完成的時間很難確定,這里設置了10秒,僅僅是估算
-
如果主線程休眠時間長了,會加長等待時間,如果等待時間短了,可能還有goroutine處於工作狀態,這時也會隨主線程的退出而銷毀
-
通過全局變量加鎖同步來實現通訊,也並不利於多個協程對全局變量的讀寫操作
-
上面種種分析都在呼喚一個新的通訊機制 - channel
channel的基本介紹
-
channel本質就是一個數據結構 - 隊列
-
數據是先進先出【FIFO :first int first out】
-
線程安全,多goroutine訪問時,不需要加鎖,就是說channel本身就是線程安全的
-
channel有類型的,一個string的channel只能存放string類型數據
定義/聲明channel
var 變量名 chan 數據類型
舉例:
var intChan chan int(intChan 用於存放int數據)
var mapChan chan map[int]string (mapChan用於存放map[int]string類型)
var perChan chan Person
var perChan2 chan *Person
....
說明
1) channel是引用類型
2) channel必須初始化才能寫入數據,即make后才能使用
3) 管道是有類型的,intChan只能寫入整數int
管道的初始化、寫入數據到管道、從管道讀取數據
package main
import "fmt"
func main() {
//演示一下管道的使用
//1. 創建一個可以存放3個int類型的管道
var intChan chan int
intChan = make(chan int, 3)
//2. 看看intChan是什么
fmt.Printf("intChan 的值 = %v intChan本身的地址 = %p\n", intChan, &intChan)
//3. 向管道寫入數據
intChan <- 10
num := 211
intChan <- num
intChan <- 50
//intChan <- 99 //當給管道寫入數據時,不能超過其容量
//4. 看看管道的長度和cap(容量)
fmt.Printf("channel len = %v cap = %v \n", len(intChan), cap(intChan))
//5. 從管道中讀取數據
var num2 int
num2 = <- intChan
fmt.Println("num2 = ", num2)
fmt.Printf("channel len = %v cap = %v \n", len(intChan), cap(intChan))
//6. 在沒有使用協程的情況下,如果管道數據已經全部取出,再取就會報告deadlock
num3 := <- intChan
num4 := <- intChan
num5 := <- intChan
fmt.Printf("num3 = %v num4 = %v num5 = %v ", num3, num4, num5)
}
//fatal error: all goroutines are asleep - deadlock!
//intChan 的值 = 0xc000090000 intChan本身的地址 = 0xc00008a018
//channel len = 3 cap = 3
//num2 = 10
//channel len = 2 cap = 3
//
//goroutine 1 [chan receive]:
//main.main()
// E:/gostudent/src/2020-04-06/main.go:28 +0x4d4
channel使用的注意事項
-
channel 中只能存放指定的數據類型
-
channel 的數據放滿后,就不能再放入了
-
如果從channel取出數據后,可以繼續放入
-
在沒有使用協程的情況下,如果channel數據取完了,再取,就會報dead lock
讀寫channel案例演示
- 創建一個intChan,最多可以存放3個int,演示存3個數據到intChan,然后再取出這三個int
func main() {
var intChan chan int
intChan = make(chan int, 3)
intChan <- 10
intChan <- 20
intChan <- 10
//因為intChan 的容量為3,再存放會報告deadlock
//intChan <- 50
num1 := <- intChan
num2 := <- intChan
num3 := <- intChan
//因為intChan 這時已經沒有數據了,再取會報告deadlock
//num4 := <- intChan
fmt.Printf("num1 = %v num2 = %v num3 = %v", num1, num2, num3)
}
//num1 = 10 num2 = 20 num3 = 10
- 創建一個mapChan,最多可以存放10個map[string]string的key-val,演示寫入和讀取
func main() {
var mapChan chan map[string]string
mapChan = make(chan map[string]string, 2)
m1 := make(map[string]string, 2)
m1["city1"] = "北京"
m1["city2"] = "天津"
m2 := make(map[string]string, 2)
m2["hero1"] = "宋江"
m2["hero2"] = "林沖"
mapChan <- m1
mapChan <- m2
num1 := <- mapChan
num2 := <- mapChan
fmt.Printf("num1 = %v num2 = %v", num1, num2)
}
//num1 = map[city1:北京 city2:天津] num2 = map[hero1:宋江 hero2:林沖]
- 創建一個catChan,最多可以存放10個Cat結構體變量,演示寫入和讀取的用法
type Cat struct{
Name string
Age int
}
func main() {
var catChan chan Cat
catChan = make(chan Cat, 10)
cat1 := Cat{Name: "tom", Age: 18,}
cat2 := Cat{Name: "zise", Age: 18,}
catChan <- cat1
catChan <- cat2
//取出
cat11 := <- catChan
cat22 := <- catChan
fmt.Println(cat11, cat22)
}
//{tom 18} {zise 18}
- 創建一個catChan2,最多可以存放10個*Cat變量,演示寫入和讀取的用法
type Cat struct{
Name string
Age int
}
func main() {
var catChan chan *Cat
catChan = make(chan *Cat, 10)
cat1 := Cat{Name: "tom", Age: 18,}
cat2 := Cat{Name: "zise", Age: 18,}
catChan <- &cat1
catChan <- &cat2
//取出
cat11 := <- catChan
cat22 := <- catChan
fmt.Println(*cat11, *cat22)
}
//{tom 18} {zise 18}
- 創建一個allChan,最多可以存放10個任意數據類型變量,演示寫入和讀取的用法
type Cat struct {
Name string
Age int
}
func main() {
var allChan chan interface{}
allChan = make(chan interface{}, 10)
cat1 := Cat{Name: "tom", Age: 18}
cat2 := Cat{Name: "zise", Age: 18}
allChan <- cat1
allChan <- cat2
allChan <- 10
allChan <- "jack"
//取出
cat11 := <- allChan
cat22 := <- allChan
v1 := <- allChan
v2 := <- allChan
fmt.Println(cat11, cat22, v1, v2)
}
//{tom 18} {zise 18} 10 jack
- 看下面的代碼,會輸出什么
type Cat struct {
Name string
Age int
}
func main() {
var allChan chan interface{}
allChan = make(chan interface{}, 10)
cat1 := Cat{Name: "tom", Age: 18}
cat2 := Cat{Name: "zise", Age: 18}
allChan <- cat1
allChan <- cat2
allChan <- 10
allChan <- "jack"
//取出
//cat11 := <- allChan
//fmt.Println(cat11.Name)
// # command-line-arguments
//src\go_code\chapter15\exec03\test03.go:23:19: cat11.Name undefined (type interface {} is interface with no methods)
newCat := <- allChan //從管道中取出的Cat是什么
fmt.Printf("newCat = %T newCat = %v \n", newCat, newCat)
//下面寫法是錯誤的,編譯不通過
//fmt.Printf("newCat.Name = %v", newCat.Name)
//使用類型斷言
a := newCat.(Cat)
fmt.Printf("newCat.Name = %v", a.Name)
}
//newCat = main.Cat newCat = {tom 18}
//newCat.Name = tom
channel的遍歷和關閉
channel的關閉
使用內置函數close可以關閉channel,當channel關閉后,就不能再向channel寫數據了,但是仍然可以從該channel讀取數據
func main() {
intChan := make(chan int, 3)
intChan <- 100
intChan <- 200
close(intChan) //close
//這時不能夠再寫入數到channel
//intChan <- 300
fmt.Println("oko")
//當管道關閉后,讀取數據是可以的
n1 := <- intChan
fmt.Println("n1 = ", n1)
}
//oko
//n1 = 100
channel的遍歷
channel支持 for - range 的方式進行遍歷,注意兩個細節
-
在遍歷時,如果channel沒有關閉,則會出現deadlock的錯誤
-
在遍歷時,如果channel已經關閉,則會正常遍歷數據,遍歷完后,就會退出遍歷
channel遍歷和關閉的案例演示
func main() {
//遍歷管道
intChan2 := make(chan int, 100)
for i := 0; i < 100; i++ {
intChan2 <- i *2 //放入100個數據到管道
}
//遍歷管道不能使用普通的for循環
//for i := 0; i < len(intChan2); i++ {
//
//}
//1)在遍歷時,如果channel沒有關閉,則會出現deadlock的錯誤
//2)在遍歷時,如果channel已經關閉,則會正常遍歷數據,遍歷完后,就會退出遍歷
close(intChan2)
for v := range intChan2 {
fmt.Println("v = ", v)
}
}
應用案例
應用案例-利於管道實現邊寫邊讀
請完成goroutine和channel協同工作的案例,具體要求:
-
開啟一個writeData協程,向管道intChan中寫入50個整數
-
開啟一個readData協程,從管道intChan中讀取writeData寫入的數據
-
注意:writeData和readData操作的是同一個管道
-
主線程需要等待writeData和readData協程都完成工作才能退出【管道】
思路分析
代碼區
import (
"fmt"
"time"
)
//writeData
func writeData(intChan chan int) {
for i := 1; i <= 50; i++ {
//放入數據
intChan <- i
fmt.Println("writeData", i)
time.Sleep(time.Second)
}
close(intChan) //關閉
}
//readData
func readData(intChan chan int, exitChan chan bool) {
for {
v, ok := <- intChan
if !ok {
break
}
time.Sleep(time.Second)
fmt.Printf("readData 讀到數據 = %v\n", v)
}
//readData 讀取完數據后,即任務完成
exitChan <- true
close(exitChan)
}
func main() {
//創建兩個管道
intChan := make(chan int, 50)
exitChan := make(chan bool, 1)
go writeData(intChan)
go readData(intChan, exitChan)
time.Sleep(time.Second * 10)
for {
_,ok := <- exitChan
if !ok {
break
}
}
}
var (
myMap = make(map[int]int, 10)
)
func cal(n int) map[int]int {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
myMap[n] = res
return myMap
}
func write(myChan chan map[int]int) {
for i := 0; i <= 15; i++ {
myChan <- cal(i)
fmt.Println("writer data:", cal(i))
}
close(myChan)
}
func read(myChan chan map[int]int, exitChan chan bool) {
for {
v, ok := <-myChan
if !ok {
break
}
fmt.Println("read data:", v)
}
exitChan <- true
close(exitChan)
}
func main() {
var myChan chan map[int]int
myChan = make(chan map[int]int, 20)
var exitChan chan bool
exitChan = make(chan bool, 1)
go write(myChan)
go read(myChan, exitChan)
for {
_, ok := <-exitChan
if !ok {
break
}
}
}
應用案例 - 阻塞
思考:假設我們注銷掉go read(myChan,exitChan)會發生什么呢?
也就是說,只有寫入myChan而沒有讀取myChan,當存入myChan里面的數據達到了myChan的容量,再繼續存入就會報deadlock錯誤。同時,由於exitChan需要寫入一個true,而exitChan需要讀取完myChan中的數據后才寫入一個true,但是現在不能進行讀取,也就是說,true不會寫入exitChan,就形成了阻塞。假設我們打開go read(myChan,exitChan),我們設置其每隔1秒才讀取一條數據,而寫入則讓其正常運行,也就是說,寫入很快,讀取很慢,這樣會導致deadlock嗎?答案是不會,只要有讀取,golang會有個機制,不會讓myChan存儲的值超過myChan的容量。
應用案例-求素數
需求
要求統計 1 - 8000的數字中,哪些是素數?
現在具備了goroutine和channel的知識后,就可以完成了
分析思路
傳統的方法:使用一個循環,循環的判斷各個數是不是素數
使用並發/並行的方式:將統計素數的任務分配給多個(4個)goroutine去完成,完成任務時間短
畫出分析思路
說明:有五個協程,三個管道。其中一個協程用於寫入數字到intChan管道中,另外四個用於取出intChan管道中的數字並判斷是否是素數,然后將素數寫入到primeChan管道中,最后如果后面四個協程哪一個工作完了,就寫入一個true到exit管道中,最后利用循環判斷這四個協程是否都完成任務,並退出
package main
import (
"fmt"
"time"
)
//向intChan放入1 - 8000個數
func putNum(intChan chan int) {
for i:= 1; i <= 8000; i++ {
intChan <- i
}
//關閉intChan
close(intChan)
}
//從intChan取出數據,並判斷是否為素數,如果是,就放入到primeChan
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
//使用for循環
//var num int
var flag bool
for {
time.Sleep(time.Millisecond * 10)
num, ok := <- intChan
if !ok { //intChan 娶不到..
break
}
flag = true //假設是素數
//判斷num是不是素數
for i := 2; i < num; i++ {
if num % i == 0 { //說明該num 不是素數
flag = false
break
}
}
if flag {
//將這個數就放入到primeChan
primeChan <- num
}
}
fmt.Println("有一個primeNum協程因為取不到數據,退出")
//這里還不能關閉primeChan
//向exitChan 寫入true
exitChan <- true
}
func main() {
intChan := make(chan int, 200000)
primeChan := make(chan int, 200000) //放入結果
//標識退出的管道
exitChan := make(chan bool, 4) // 4個
//開啟一個協程,向intChan放入1 - 200000個數
go putNum(intChan)
//開啟四個協程,從intChan取出數據,
//並判斷是否為素數,如果是,就放入到primeChan
for i := 0; i < 4; i++ {
go primeNum(intChan, primeChan, exitChan)
}
//這里對主線程,進行處理
go func() {
for i := 0; i < 4; i++ {
<- exitChan
}
//當從exitChan 取出4個結果
//就可以關閉prprimeChan
close(primeChan)
}()
//遍歷primeChan,把結果取出
for {
res, ok := <- primeChan
if !ok {
break
}
//將結果輸出
fmt.Printf("素數 = %d\n", res)
}
fmt.Println("main線程退出")
}
升級
package main
import (
"fmt"
"time"
)
func isPrime(n int) bool {
for i := 2; i <= n; i++ {
if n%i == 0 {
return false
}
}
return true
}
//傳統方法耗時
func Test() {
start := time.Now()
for i := 1; i < 80000; i++ {
isPrime(i)
}
cost := time.Since(start)
fmt.Printf("傳統方法消耗時間為:%s", cost)
}
//向intChan放入1 - 80000個數
func putNum(intChan chan int) {
for i:= 1; i <= 80000; i++ {
intChan <- i
}
//關閉intChan
close(intChan)
}
//從intChan取出數據,並判斷是否為素數,如果是,就放入到primeChan
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
//使用for循環
//var num int
//var flag bool
for {
//time.Sleep(time.Millisecond * 10)
num, ok := <- intChan
if !ok { //intChan 娶不到..
break
}
//flag = true //假設是素數
//判斷num是不是素數
// for i := 2; i < num; i++ {
// if num % i == 0 { //說明該num 不是素數
// flag = false
// break
// }
// }
// if flag {
// //將這個數就放入到primeChan
// primeChan <- num
// }
//}
isp := isPrime(num)
if !isp {
continue
} else {
primeChan <- num
}
}
fmt.Println("有一個primeNum協程因為取不到數據,退出")
//這里還不能關閉primeChan
//向exitChan 寫入true
exitChan <- true
}
func main() {
intChan := make(chan int, 200000)
primeChan := make(chan int, 200000) //放入結果
//標識退出的管道
exitChan := make(chan bool, 4) // 4個
//記錄當前時間
start := time.Now()
//開啟一個協程,向intChan放入1 - 200000個數
go putNum(intChan)
//開啟四個協程,從intChan取出數據,
//並判斷是否為素數,如果是,就放入到primeChan
for i := 0; i < 4; i++ {
go primeNum(intChan, primeChan, exitChan)
}
//這里對主線程,進行處理
go func() {
for i := 0; i < 4; i++ {
<- exitChan
}
//當從exitChan 取出4個結果
//就可以關閉prprimeChan
//計算耗時時間
cost := time.Since(start)
fmt.Printf("使用協程耗費時間:%s\n", cost)
close(primeChan)
}()
//遍歷primeChan,把結果取出
for {
_, ok := <- primeChan
if !ok {
break
}
//將結果輸出
//fmt.Printf("素數 = %d\n", res)
}
fmt.Println("main線程退出")
Test()
}
//有一個primeNum協程因為取不到數據,退出
//有一個primeNum協程因為取不到數據,退出
//有一個primeNum協程因為取不到數據,退出
//有一個primeNum協程因為取不到數據,退出
//使用協程耗費時間:876.6558ms
//main線程退出
//傳統方法消耗時間為:3.3300976s
channel使用細節和注意事項
channel可以聲明為只讀,或者只寫性質
func main() {
//管道可以聲明為只讀或者只寫
//1. 在默認情況下,管道是雙向
//var chan1 chan int //可讀可寫
//2. 聲明為只寫
var chan2 chan <- int
chan2 = make(chan int, 3)
chan2 <- 20
//num := <- chan2 //error
fmt.Println("chan2 = ", chan2)
//3. 聲明為只讀
var chan3 <- chan int
num2 := <- chan3
//chan3 <- 30 //err
fmt.Println("num2", num2)
}
channel只讀和只寫的最佳實踐案例
//ch chan <- int 這樣ch就只能寫操作了
func send(ch chan <- int, exitChan chan struct{}) {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
var a struct{}
exitChan <- a
}
//ch <- chan int ,這樣ch 就只能讀操作了
func recv(ch <- chan int, exitChan chan struct{}) {
for {
v, ok := <- ch
if !ok {
break
}
fmt.Println(v)
}
var a struct{}
exitChan <- a
}
func main() {
var ch chan int
ch = make(chan int, 10)
exitChan := make(chan struct{}, 2)
go send(ch, exitChan)
go recv(ch, exitChan)
var total = 0
for _ = range exitChan {
total ++
if total == 2 {
break
}
}
fmt.Println("結束...")
}
使用select可以解決從管道取數據的阻塞問題
import (
"fmt"
"time"
)
func main() {
//使用select可以解決從管道取數據的阻塞問題
//1. 定義一個管道10個數據int
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
}
//2. 定義一個管道5個數據string
stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan <- "hello" + fmt.Sprintf("%d", i)
}
//傳統的方法在遍歷管道時,如果不關閉會阻塞而導致deadlock
//問題:在實際開發中,可能不好確定什么時間關閉該管道
//可以使用select方式解決
//label:
for {
select {
//注意:這里intChan一直沒有關閉,不會一直阻塞而deadlock
//會自動到下一個case匹配
case v := <-intChan:
fmt.Printf("從intChan讀取的數據%d\n", v)
time.Sleep(time.Second)
case v := <-stringChan:
fmt.Printf("從stringChan讀取的數據%s\n", v)
time.Sleep(time.Second)
default:
fmt.Printf("都取不到了,不玩了,程序員可以加入邏輯\n")
time.Sleep(time.Second)
return
//break label
}
}
}
goroutine中使用recover,解決協程中出現panic,導致程序崩潰問題
說明:如果起了一個協程,但是這個協程出現了panic,如果我們沒有捕獲這個panic,就會造成整個程序崩潰,這時我們可以在goroutine中使用recover來捕獲panic,進行處理,這樣即使這個協程發生了問題,但是主線程仍然不受影響,可以繼續執行。
import (
"fmt"
"time"
)
//函數
func sayHello() {
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
fmt.Println("hello,world")
}
}
//函數
func test() {
//這里可以使用defer + recover
defer func() {
//捕獲test拋出的panic
if err := recover(); err != nil {
fmt.Println("test() 發生錯誤", err)
}
}()
//定義了一個map
var myMap map[int]string
myMap[0] = "golang" // error
}
func main() {
go sayHello()
go test()
for i := 0; i < 10; i++ {
fmt.Println("main() ok=", i)
time.Sleep(time.Second)
}
}
//main() ok= 0
//test() 發生錯誤 assignment to entry in nil map
//hello,world
//main() ok= 1
//hello,world
//main() ok= 2
//hello,world
//main() ok= 3
//hello,world
//main() ok= 4
//hello,world
//main() ok= 5
//hello,world
//main() ok= 6
//hello,world
//main() ok= 7
//hello,world
//main() ok= 8
//hello,world
//main() ok= 9
//hello,world
管道的練習題
說明:
-
創建一個Person結構體[Name,Age,Address]
-
使用rand方法配合隨機創建10個Person實例,並放入到channel中
-
遍歷channel,將各個Person實例的信息顯示在終端...