(九)goroutine協程和channel管道

1 基本概念

1.1 進程和線程說明

  • 1 進程就是程序在操作系統中的一次執行過程,是系統進行資源分配和調度的基本單位
  • 2 線程是進程的一個執行實例,是程序執行的最小單元,它是比進程更小的能獨立運行的基本單位
  • 3 一個進程可以創建和銷燬多個線程,同一個進程中的多個線程可以併發執行
  • 4 一個程序至少有一個進程,一個進程至少有一個線程

1.2 併發和並行

併發和並行(併發包含並行)

  • 1)多線程程序在單核上運行,就是併發
    因爲在一個CPU上,比如有10個線程,每個線程執行10ms(進行輪詢操作),從人爲角度看,好像是這10個線程都在運行,但是從微觀上看,在某個時間點看,其實只有一個線程在執行,這就是併發。
  • 2)多線程程序在多核上運行,就是並行
    因爲是在多個CPU上(比如有10個CPU),比如有10個線程,每個線程執行10ms(各自在不同cpu上執行),從人爲角度看,這10個線程都在運行,但是從微觀上看,在某個時間點看,也同時有10個線程在執行,這就是並行

2 goroutine協程

在一個go主線程(也可以理解爲一個進程)之上可以起多個協程,可以理解爲協程是輕量級的線程(編譯器優化)。

Go協程的特點:

  • 有獨立的棧空間
  • 共享程序的堆空間
  • 調度由用戶控制
  • 協程是輕量級的線程

MPG

Golang中如何實現高併發的
Go併發原理

總結:

  • 主線程是一個物理線程,其是直接作用在cpu上的,是重量級的,非常消耗cpu資源。
  • 協程從主線程開啓的,是輕量級的線程,是邏輯太。對資源消耗相對小。
  • Golang的協程機制是重要的特點,可以輕鬆地開啓上萬個協程。
  • 如果主線程執行結束退出了,即使協程還沒有執行完畢,也會退出,當然協程也可以在主線程沒有執行結束前就已經結束了,如完成了自己的任務。

2.1 exa1

// goroutine是Go並行設計的核心。
// goroutine說到底其實就是協程,但是它比線程更小,十幾個goroutine可能體現在底層就是五六個線程,Go語言內部幫你實現了這些goroutine之間的內存共享。
// 執行goroutine只需極少的棧內存(大概是4~5KB),當然會根據相應的數據伸縮。
// 也正因爲如此,可同時運行成千上萬個併發任務。goroutine比thread更易用、更高效、更輕便。

// goroutine是通過Go的runtime管理的一個線程管理器。
// goroutine通過go關鍵字實現了,其實就是一個普通的函數。

package main

import (
	"fmt"
	"runtime"
)

func say(s string) {
	for i := 0; i < 5; i++ {
		runtime.Gosched()
		fmt.Println(s)
	}
}

func main() {
	go say("world") //開一個新的Goroutines執行
	go say("world") //開一個新的Goroutines執行
	say("hello") //當前Goroutines執行
}

2.2 exa2

package main

import (
	"fmt"
	"strconv"
	"time"
)

/*
	要求:
	1)在主線程(可以理解成進程)中,開啓一個goroutine,該協程每隔1秒輸出“hello, world”
	2)在主線程中也每隔一秒輸出“hello, golang”,輸出10此後,退出程序
	3)要求主線程和goroutine同時執行
 */

func say()  {
	for i := 0; i < 10; i++ {
		fmt.Println("say() hello, world" + strconv.Itoa(i))
		time.Sleep(time.Second)
	}
}

func main() {

	go say()  // 開啓一個協程

	for i := 0; i < 10; i++ {
		fmt.Println("main() hello, world" + strconv.Itoa(i))
		time.Sleep(time.Second)
	}

}

2.3 exa3

package main

import (
	"fmt"
	"runtime"
)



func main() {

	num := runtime.NumCPU()

	fmt.Printf("當前系統CPU數量 = %v", num)  // 6

	// 設置Golang中使用最多CPU數量
	runtime.GOMAXPROCS(num)

}

3 channel管道

channel管道隊列是先進先出的機制,當取完管道的數據後需要有一定機制來判斷此管道爲空,否則會報錯;channel是線程安全的,多個協程操作同一個管道時,不需要加鎖,不會發生資源競爭問題;channel是有類型的,一個string的channel只能存放string類型的數據

channel使用的注意事項

  1. channel中只能存放指定的數據類型
  2. channel的數據放滿後,就不能再放入了
  3. 如果從channel取出數據後,可以繼續放入
  4. 在沒有使用協程的情況下,如果channel數據取完了,再去取數據,就會報錯 deadlock

PS: 單線程是算好一個寫一個,協程是同步算,一個個寫

3.1 example

3.1.1 exa1

package main

import (
	"fmt"
	"sync"
	"time"
)

// 需求:現在計算1-200的各個數的階乘,並且把各個數的階乘放到map中
// 最後顯示出來。要求使用goroutine完成

var (
	myMap = make(map[int]int, 10)

	// 聲明一個全局的互斥鎖 lock 是一個全局的互斥鎖(寫數據鎖)
	// sync是同步的意思,Mutex是互斥
	lock sync.Mutex
)

// 編寫一個函數,來計算各個數的階乘,並放入到一個map中
func cal(n int)  {

	res := 1
	for i := 1; i <= n; i++ {
		res *= i
	}

	// 我們將每循環一次的結果放入到myMap中
	// 加鎖
	lock.Lock()
	myMap[n] = res  // 如果沒有保護機制會報Fatal錯誤 fatal error: concurrent map writes

	// 解鎖
	lock.Unlock()

}

func main() {

	for i := 1; i <= 20; i++ {
		go cal(i)
		// 使用goroutine來完成,效率高,但是會出現併發/並行安全的問題
		// 這裏就產生了如何讓多個goroutine之間通信的問題?
	}

	// 解決方法
	// 1)全局變量加互斥鎖
	// 2)使用channel管道解決
	time.Sleep(time.Second * 3)

	lock.Lock()
	// 遍歷這個map結果
	for i, v := range myMap {
		fmt.Printf("map[%d] = %d\n", i, v)
	}
	lock.Unlock()

}

3.1.2 exa2

package main

import (
	"fmt"
)

func main() {

	// 1.創建一個可以存放3個int類型的管道
	var intChan chan int
	intChan = make(chan int, 3)

	fmt.Printf("inChan 的值=%v intChan本身的地址= %p\n", intChan, &intChan)
	// inChan 的值=0xc000088080 intChan本身的地址= 0xc000082018

	// 2.向管道寫入數據
	// 當我們給管道寫入數據時候,不能超過make的容量
	intChan<- 10

	num := 211
	intChan<- num
	intChan<- 59

	// 3.看看管道的長度和cap容量 管道不能自動增長
	fmt.Printf("Channel的len = %v, cap =%v\n", len(intChan), cap(intChan))
	// Channel的len = 2, cap =3

	// 4.從管道中讀取數據
	var num2 int
	num2 = <-intChan

	// <-intChan 也可以這樣寫,這樣的話就會把數據丟掉
	fmt.Println("num2=", num2)
	fmt.Printf("Channel的len = %v, cap =%v\n", len(intChan), cap(intChan))

	// 5.在沒有使用協程的情況下,如果我們的管道數據已經全部取出,若再取數據,則會報錯 deadlock

}

3.1.3 exa3(channel不同數據類型的聲明)

package main

import (
	"fmt"
)

type Cat struct {
	Name string
	Age int
}

func main() {

	// 1.channel的map存放和讀取
	var mapChan chan map[string]string

	mapChan = make(chan map[string]string, 10)

	m1 := make(map[string]string, 20)
	m1["cities1"] = "beijing"
	m1["cities2"] = "shanghai"

	m2 := make(map[string]string, 20)
	m2["hero1"] = "宋江"
	m2["hero2"] = "吳淞"

	mapChan <- m1
	mapChan <- m2

	// 2.channel的struct類型
	var catChan chan Cat
	catChan = make(chan Cat, 10)
	cat1 := Cat{
		Name: "tom",
		Age: 10,
	}
	cat2 := Cat{
		Name: "merry",
		Age: 12,
	}
	catChan <- cat1
	catChan <- cat2

	//取數據
	//cat11 := <-catChan
	//cat22 := <-catChan

	// 3.channel的指針類型
	var cat2Chan chan *Cat
	cat2Chan = make(chan *Cat, 10)
	cat3 := Cat{
		Name: "tom",
		Age: 10,
	}
	cat4 := Cat{
		Name: "merry",
		Age: 12,
	}
	cat2Chan <- &cat3
	cat2Chan <- &cat4

	//取數據
	//cat33 := <-cat2Chan
	//cat44 := <-cat2Chan

	// 4.給 allChan 存放任何類型的數據 定義空接口即可  因任何數據類型都實現了空接口
	allChan := make(chan interface{}, 10)

	cat5 := Cat{
		Name: "jack",
		Age: 20,
	}
	cat6 := Cat{
		Name: "jack~~~",
		Age: 18,
	}
	allChan <- 10
	allChan <- "rsq"
	allChan <- cat5
	allChan <- cat6

	// 若我們需要獲取到管道中的第三個元素,則先將前兩個數據推出
	<-allChan
	<-allChan

	cat55 := <- allChan
	fmt.Printf("cat55的類型=%T cat55=%v\n", cat55, cat55)
	// 下邊的寫法是錯誤的,編譯不通過
	// fmt.Println("cat222.Name=", cat55.Name)

	// 用類型斷言可以取出具體字段
	a := cat55.(Cat)
	fmt.Println("cat222.Name=", a.Name)


}

3.1.4 exa4

實例:
1)創建一個Person結構體[Name, Age, Address]
2)使用rand方法配合隨機創建10個Person實例,並放入到channel中
3)遍歷channel,將各個Person實例的信息顯示在終端

package main

import (
	"fmt"
	"math/rand"
	"time"
)

type Person struct {
	Name string
	Age int
	Address string
}

func (p Person) Rand() {

	rand.Seed(time.Now().UnixNano())

	// 定義personChan channel
	var personChan chan Person
	personChan = make(chan Person, 10)
	for i := 0; i < 10; i++ {
		p.Name = string(rand.Intn(100))
		p.Age = rand.Intn(100)
		p.Address = string(rand.Intn(100))
		personChan <- p  // 寫入數據到管道中
	}
	
	// 關閉管道
	close(personChan)

	for v := range personChan {
		fmt.Println(v)
	}

}

func main() {

	var person Person
	person.Rand()

}

3.2 channel的遍歷

使用內置函數 close 可以關閉channel,當channel關閉後,就不能再向channel寫數據了,但是仍然可以從該channel讀取數據。
example.go

package main

import (
	"fmt"
)

func main() {

	intChan := make(chan int, 3)
	intChan <- 100
	intChan <- 200
	close(intChan)  // 關閉管道

	// 此時不能夠再往intChan中寫入數據
	//intChan <- 300  // 這樣就會報錯:panic: send on closed channel
	//fmt.Println("okook")

	// 當管道關閉後,讀取數據是可以的
	n1 := <-intChan
	fmt.Println(n1) // 100
	
}

channel的遍歷支持for-range,需要注意的是:
1)在遍歷時,如果channel沒有關閉,則會出現deadlock的錯誤
2)在遍歷時,如果channel已經關閉,則會正常遍歷數據,遍歷完後就會退出遍歷

package main

import (
	"fmt"
)

func main() {

	intChan := make(chan int, 100)

	for i := 0; i < 100; i++ {
		intChan <- i * 2  // 往管道寫入100個數據
	}
	close(intChan)  // 關閉管道

	// 遍歷管道
	for v := range intChan {
		fmt.Printf("v=%v\n", v)
	}

}

4 goroutine和channel結合實例

4.1 exa1

(1) 開啓一個 writeData 協程,向管道 intChan 中寫入50個數據
(2) 開啓一個 readData 協程,從管道 intChan 中讀取 writeData 寫入的數據
(3) 注意: writeDatareadData 操作的是同一個管道
(4) 主線程需要等待 writeDatareadData 協程都完成工作才能退出

package main

import (
	"fmt"
)

// writeData函數
func writeData(intChan chan int) {
	for i := 1; i <= 50; i++ {
		intChan <- i
		fmt.Println("writeData ", i)
	}
	close(intChan)
}

// readData函數
func readData(intChan chan int, exitChan chan bool)  {
	//for {
	//	v, ok := <-intChan
	//	if !ok {
	//		break
	//	}
	//	fmt.Println("readData ", v)
	//}
	//exitChan <- true
	//close(exitChan)

	// 從intChan中讀取數據
	for num := range intChan {
		fmt.Println("readData ", num)
	}
	exitChan <- true
	close(exitChan)
}


func main() {

	intChan := make(chan int, 50)
	exitChan := make(chan bool, 1)

	go writeData(intChan)
	go readData(intChan, exitChan)

	// 判斷當exitChan管道不爲true,則證明已經取完數據,然後退出主線程即可
	// 也可以直接用 <-exitChan 來阻塞管道實現以下的功能
	for {
		_, ok := <-exitChan
		if !ok {
			break
		}
	}
}

4.2 exa2

3.1.1中的實例用channel來實現

package main

import (
	"fmt"
)

// 需求:現在計算1-200的各個數的階乘,並且把各個數的階乘放到map中
// 最後顯示出來。要求使用goroutine完成

var (
	myMap = make(map[int]int, 10)
)

// 編寫一個函數,來計算各個數的階乘,並放入到一個map中
func cal(resChan chan int, exitChan chan bool)  {

	res := 1
	for i := 1; i <= 20; i++ {
		res *= i
		resChan <- res
		myMap[i] = <- resChan
	}
	close(resChan)

	// 我們將每循環一次的結果放入到myMap中
	for i, v := range myMap {
		fmt.Printf("map[%d] = %d\n", i, v)
	}

	exitChan <- true
	close(exitChan)
}

func main() {

	resChan := make(chan int, 1)
	exitChan := make(chan bool, 1)

	go cal(resChan, exitChan)

	<- exitChan

}

4.3 exa3

要求
(1)啓動一個協程,將1-2000的數放入到一個channel中,如numChan
(2)啓動8個協程,從numChan取出數(n來定義),計算1+...+n的值,並存放到resChan中
(3)最後8個協程同時完成工作後,再遍歷resChan,顯示結果如[res[1] = 1 ... res[10] = 55 ...]

package main

import (
	"fmt"
)

func writeData(numChan chan int) {
	for i := 1; i <= 2000; i++ {
		numChan <- i
	}
	close(numChan)
}

func readData(numChan chan int, resChan chan int, exitChan chan bool)  {
	for num := range numChan {
		res := 0
		for i := 1; i <= num; i++ {
			res += i
		}
		resChan <- res
		fmt.Printf("res[%d] = %v\n", num, res)
	}
	exitChan <- true
	close(exitChan)
}

func main() {

	numChan := make(chan int, 2000)
	resChan := make(chan int, 2000)
	exitChan := make(chan bool, 1)

	go writeData(numChan)

	for i := 1; i <= 8; i++ {
		go readData(numChan, resChan, exitChan)
	}

	<- exitChan
	close(resChan)

}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章