主頁 > 後端開發 > go語言系列-從Goroutine到Channel

go語言系列-從Goroutine到Channel

2020-09-13 20:42:34 後端開發

Golang語言的核心特色
目錄

  • Goroutine
    • 基本介紹
      • 行程和執行緒介紹
      • 程式、行程和執行緒的關系示意圖
      • 并發和并行
      • Go協程和Go主執行緒
    • 快速入門
      • 案例說明
      • 小結
    • goroutine的調度模型
      • MPG模式運行的狀態 -1
      • MPG模式運行的狀態 - 2
    • 設定Go運行的CPU數
  • Channel(管道)
    • 看個需求
    • 不同goroutine之間如何通訊
      • 使用全域變數加鎖同步改行程式
      • 為什么需要channel
      • channel的基本介紹
      • 定義/宣告channel
      • 管道的初始化、寫入資料到管道、從管道讀取資料
      • channel使用的注意事項
      • 讀寫channel案例演示
    • channel的遍歷和關閉
      • channel的關閉
      • channel的遍歷
      • channel遍歷和關閉的案例演示
  • 應用案例
    • 應用案例-利于管道實作邊寫邊讀
    • 應用案例 - 阻塞
    • 應用案例-求素數
    • channel使用細節和注意事項
      • channel可以宣告為只讀,或者只寫性質
      • channel只讀和只寫的最佳實踐案例
      • 使用select可以解決從管道取資料的阻塞問題
      • goroutine中使用recover,解決協程中出現panic,導致程式崩潰問題
    • 管道的練習題

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實體的資訊顯示在終端...

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/27358.html

標籤:Go

上一篇:Gin框架系列01:極速上手

下一篇:go語言系列-TCP編程

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more