go語言系列-從Goroutine到Channel


Golang語言的核心特色

Goroutine

基本介紹

進程和線程介紹

  1. 進程就是程序在操作系統中的一次執行過程,是系統進行資源分配和調度的基本單位

  2. 線程是進程的一個執行實例,是程序執行的最小單元,它是比進程更小的能獨立運行的基本單位

  3. 一個進程可以創建和銷毀多個線程,同一個進程中的多個線程可以並發執行

  4. 一個程序至少有一個進程,一個進程至少有一個線程

程序、進程和線程的關系示意圖

並發和並行

  1. 多線程程序在單核上運行,就是並發

  2. 多線程程序在多核上運行,就是並行

    並發:因為是在一個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
//......

小結

  1. 主線程是一個物理線程,直接作用在cpu上的。是重量級的,非常耗費cpu資源

  2. 協程從主線程開啟的,是輕量級的線程,是邏輯態。對資源消耗相對小

  3. Go的協程機制是重要的特點,可以輕松的開啟上萬個協程。其它編程語言的並發機制一般是基於線程的,開啟過多的線程,資源耗費大,這里就突顯了Go在並發上的優勢了

goroutine的調度模型

  1. M:操作系統的主線程(是物理線程)

  2. P:協程執行需要的上下文

  3. G:協程

MPG模式運行的狀態 -1

  1. 當前程序有三個M,如果三個M都在一個cpu上運行,就是並發,如果在不同的cpu上運行就是並行

  2. M1,M2,M3正在執行一個G,M1的協程隊列有三個,M2的協程隊列有三個,M3的協程隊列有兩個

  3. 從上圖可以看到:Go的協程是輕量級的線程,是邏輯態的,Go可以容易的起上萬個協程

  4. 其它程序c/java的多線程,往往是內核態的,比較重量級,幾千個線程可能耗光cpu

MPG模式運行的狀態 - 2

  1. 分成兩個部分來看

  2. 原來的情況是MO主線程正在執行Go協程,另外有三個協程在隊列等待

  3. 如果Go協程阻塞,比如讀取文件或者數據庫等

  4. 這時就會創建M1主線程(也可能是從已有的線程池中取出M1),並且將等待的3個協程掛到M1下開始執行,M0的主線程下的Go仍然執行文件io的讀寫

  5. 這樣的MPG調度模式,可以既讓Go執行,同時也不會讓隊列的其它協程一直阻塞,仍然可以並發/並行執行

  6. 等到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之間如何通訊

  1. 全局變量的互斥鎖

  2. 使用管道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

  1. 前面使用全局變量加鎖同步來解決goroutine的通訊,但不完美

  2. 主線程在等待所有gorountine全部完成的時間很難確定,這里設置了10秒,僅僅是估算

  3. 如果主線程休眠時間長了,會加長等待時間,如果等待時間短了,可能還有goroutine處於工作狀態,這時也會隨主線程的退出而銷毀

  4. 通過全局變量加鎖同步來實現通訊,也並不利於多個協程對全局變量的讀寫操作

  5. 上面種種分析都在呼喚一個新的通訊機制 - channel

channel的基本介紹

  1. channel本質就是一個數據結構 - 隊列

  2. 數據是先進先出【FIFO :first int first out】

  3. 線程安全,多goroutine訪問時,不需要加鎖,就是說channel本身就是線程安全的

  4. 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使用的注意事項

  1. channel 中只能存放指定的數據類型

  2. channel 的數據放滿后,就不能再放入了

  3. 如果從channel取出數據后,可以繼續放入

  4. 在沒有使用協程的情況下,如果channel數據取完了,再取,就會報dead lock

讀寫channel案例演示

  1. 創建一個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
  1. 創建一個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:林沖]
  1. 創建一個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}
  1. 創建一個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}
  1. 創建一個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
  1. 看下面的代碼,會輸出什么
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 的方式進行遍歷,注意兩個細節

  1. 在遍歷時,如果channel沒有關閉,則會出現deadlock的錯誤

  2. 在遍歷時,如果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協同工作的案例,具體要求:

  1. 開啟一個writeData協程,向管道intChan中寫入50個整數

  2. 開啟一個readData協程,從管道intChan中讀取writeData寫入的數據

  3. 注意:writeData和readData操作的是同一個管道

  4. 主線程需要等待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

管道的練習題

說明:

  1. 創建一個Person結構體[Name,Age,Address]

  2. 使用rand方法配合隨機創建10個Person實例,並放入到channel中

  3. 遍歷channel,將各個Person實例的信息顯示在終端...


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM