首頁 > 軟體

Golang中goroutine和channel使用介紹深入分析

2023-01-14 14:01:29

1.goroutine-看一個需求

需求:要求統計1-900000000的數位中,那些是素數?

分析:

  • 傳統方法,就是使用一個迴圈,迴圈的判斷各個數是不是素數。
  • 使用並行或並行的方式,將統計素數的任務分配給多個goroutine去完成,這時就會使用到goroutine。

2.程序和執行緒介紹

  • 程序就是程式在作業系統中的一次執行過程,是系統進行資源分配和排程的基本單位
  • 執行緒是程序的一個執行範例,是程式執行的最小單位,它是比程序更小的能獨立執行的基本單位。
  • 一個程序可以建立和銷燬多個執行緒,同一個程序中的多個執行緒可以並行執行
  • 一個程式至少有一個程序,一個程序至少有一個執行緒

3.並行和並行

  • 多執行緒程式在單核上執行,就是並行
  • 多個程程式在多核上執行,就是並行

並行:因為是在一個CPU上,比如有10個執行緒,每個執行緒執行10毫秒(進行輪詢操作),從人的角度看,好像這10個執行緒都在執行,但是從微觀上看,在某一個時間點看,其實只有一個執行緒在執行,這就是並行。

並行:因為是在多個CPU上(比如有10個CPU),比如有10個執行緒,每個執行緒執行10毫秒(各自在不同CPU上執行),從人的角度看,這10個執行緒都在執行,但是從微觀上看,在某一個時間點看,也同時有10個執行緒在執行,這就是並行

4.Go協程和Go主執行緒

Go主執行緒(有程式設計師直接稱為執行緒/也可以理解成程序):一個Go執行緒上,可以起多個攜程,你可以這樣理解,攜程是輕量的執行緒

Go協程的特點

有獨立的棧空間

共用程式堆空間

排程由使用者控制

攜程是輕量級的執行緒

案例說明

請編寫一個程式,完成如下功能:

1.在主執行緒(可以理解成程序)中,開啟一個goroutine,該攜程每隔1秒輸出“hello,world”

2.在主執行緒中也每隔一秒輸出“hello,golang”,輸出10次後,退出程式

3.要求主執行緒和goroutine同時執行

4.畫出主執行緒和協程執行流程圖

程式碼實現

// 在主執行緒(可以理解成程序)中,開啟一個goroutine,該協程每秒輸出 「hello,world」
// 在主執行緒中也每隔一秒輸出「hello,golang」,輸出10次後,退出程式
// 要求主執行緒和goroutine同時執行
//編寫一個函數,每隔1秒輸出 「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,golang"+strconv.Itoa(i))
		time.Sleep(time.Second)
	}
}

總結

  • 主執行緒是一個物理執行緒,直接作用在CPU上的,是重量級的,非常耗費CPU資源。
  • 協程從主執行緒開啟的,是輕量級的執行緒,是邏輯態。對資源消耗相對少。
  • Golang的協程機制是重要的特點,可以輕鬆的開啟上萬個協程。其他程式語言的並行機制是一般基於執行緒的,開啟過多的執行緒,資源耗費大,這裡就突顯Golang在並行上的優勢了

MPG模式基本介紹

M:作業系統的主執行緒(是物理執行緒)

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

G:協程

5.設定Golang執行的CPU數

介紹:為了充分利用多CPU的優勢,在Golang程式中設定執行的CPU數目

 package main
 import "fmt"
 import "runtime"
func main(){
	// 獲取當前系統CPU的數量
	num := runtime.NumCPU()
	// 這裡設定num-1的CPU執行go程式
	runtime.GOMAXPROCS(num)
	fmt.Println("num=",num)
}
  • go1.8後,預設讓程式執行在多個核上,可以不用設定了
  • go1.8前,還是要設定一下,可以更高效的利用CPU

6.channel(管道)看需求

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

分析思路:

使用goroutine來完成,效率高,但是會出現並行/並行安全問題

這裡就提出了不同goroutine如何通訊的問題

程式碼實現

使用goroutine來完成(看看使用gorotine並行完成會出現什麼問題?然後我們會去解決)

在執行某個程式時,如何知道是否存在資源競爭問題,方法很簡單,在編譯該程式時,增加一個引數 -race即可

不同goroutine之間如何通訊

1.全域性變數的互斥鎖

2.使用管道channel來解決

使用全域性變數加鎖同步改程序式

  • 英文沒有對全域性變數m加鎖,因此會出現資源爭奪問題,程式碼會出現錯誤,提示concurrent map writes
  • 解決方案:加入互斥鎖
  • 我們的數的階乘很大,結果會越界,可以將求階乘改成sum += uint64(i)

原始碼

package main
import (
	"fmt"
	"time"
	"sync"
)
// 需求:現在要計算 1-200的各個數的階乘,並且把各個數的階乘放入到map中
// 最後顯示出來。要求使用goroutine完成
// 思路
// 1. 編寫一個函數,來計算各個數的階乘,並放入到map中
// 2. 我們啟動的協程多個,統計的將結果放入到map中
// 3. map應該做出一個全域性的
var (
  myMap = make(map[int]int,10)
  // 宣告一個全域性的互斥鎖
  // lock 是一個全域性的互斥鎖
  //sync 是包:synchornized 同步
  // Mutex: 是互斥
  lock sync.Mutex
)
// test函數就是計算n!,讓將這個結果放入到myMap
func test(n int){
	res := 1
	for i := 1;i<=n;i++{
		res *= i
	}
	// 這裡我們將res放入到myMap
	// 加鎖
	lock.Lock()
	myMap[n] = res  // concurrent map writes?
	// 解鎖
	lock.Unlock()
}
func main(){
	// 我們這裡開啟多個協程完成這個任務[200個]
	for i := 1;i<=20;i++{
		go test(i)
	}
	// 休眠10秒鐘【第二個問題】
	time.Sleep(time.Second * 10)
	lock.Lock()
	// 這裡我們輸出結果 變數這個結果
	for i,v := range myMap{
		fmt.Printf("map[%d]=%dn",i,v)
	} 
	lock.Unlock()
}

channel(管道)-基本使用

channel初始化

說明:使用make進行初始化

var intChan chan int

intChan = make(chan int,10)

向channel中寫入(存放)資料

var intChan chan int

intChan = make(chan int,10)

num := 999

intChan <-10

intChan <-num

管道的初始化,寫入資料到管道,從管道讀取資料及基本的注意事項

package main
import (
	"fmt"
)
func main(){
	// 演示一下管道的使用
	// 1.建立一個可以存放3個int型別的管道
	var intChan chan int
	intChan = make(chan int,3)
	// 2.看看intChannel是什麼
	fmt.Printf("intChan 的值=%v intChan本身的地址=%pn",intChan,&intChan)
	// 3.向管道寫入資料
	intChan<- 10
	num := 211
	intChan<- num
	// 注意點,當我們給管寫入資料時,不能超過其容量
	intChan<- 50
	// intChan<- 98
	//4. 看看管道的長度和cap(容量)
	fmt.Printf("channel len=%v cap=%v n",len(intChan),cap(intChan)) // 2,3
	// 5.從管道中讀取資料
	var num2 int 
	num2 = <-intChan
	fmt.Println("num2=",num2)
	fmt.Printf("channel len=%v cap=%v n",len(intChan),cap(intChan)) // 2,3
	// 6.在沒有使用協程的情況下,如果我們的管道資料已經全部取出,再取就會報告 deadlock
	num3 := <-intChan
	num4 := <-intChan
	// num5 := <-intChan
	fmt.Println("num3=",num3,"num4=",num4)//,"num5=",num5)
}

channel使用的注意事項

1.channel中只能存放指定的資料型別

2.channel的資料放滿後,就不能再放入了

3.如果從channel取出資料後,可以繼續放入

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

範例程式碼

package main
import (
	"fmt"
)
type Cat struct{
	Name string
	Age int
}
func main(){
	// 定義一個存放任意資料型別的管道  3個資料
	// var callChan chan interface{}
	allChan := make(chan interface{},3)
	allChan<- 10
	allChan<- "tom jack"
	cat := Cat{"小花貓",4}
	allChan<- cat
	// 我們希望獲得到管道中的第三個元素,則先將前2個推出
	<-allChan
	<-allChan
	newCat := <-allChan // 從管道中取出的Cat是什麼?
	fmt.Printf("newCat=%T,newCat=%vn",newCat,newCat)
	// 下面的寫法是錯誤的!編譯不通過
	// fmt.Printf("newCat.Name=%v",newCat.Name)
	// 使用型別斷言
	a := newCat.(Cat)
	fmt.Printf("newCat.Name=%v",a.Name)
}

channel的關閉

使用內建函數close可以關閉channel,當channel關閉後,就不能再向channel寫資料了,但是仍然可以從該channel讀取資料

channel的遍歷

channel支援for-range的方式進行遍歷,請注意兩個細節

  • 在遍歷時,如果channel沒有關閉,則會出現deadlock的錯誤
  • 在遍歷時,如果channel已經關閉,則會正常遍歷資料,遍歷完後,就會退出遍歷。

程式碼演示:

package main
import (
	"fmt"
)
func main(){
	intChan := make(chan int,3)
	intChan<- 100
	intChan<- 200
	close(intChan) // close
	// 這是不能夠再寫入到channel
	// intChan<-300
	fmt.Println("okook~")
	// 當管道關閉後,讀取資料是可以的
	n1 := <-intChan
	fmt.Println("n1=",n1)
	// 遍歷管道
	intChan2 := make(chan int,100)
	for i := 0; i< 100;i++{
		intChan2<-i*2 // 放入100個資料到管道
	}
	// 遍歷管道不能使用普通的for迴圈
	// 在遍歷時,如果channel沒有關閉,則會出現deadlock的錯誤
	// 在遍歷時,如果channel已經關閉,則會正常遍歷資料,遍歷完後,就會退出遍歷
	close(intChan2)
	for  v := range intChan2{
		fmt.Println("v=",v)
	}
}

到此這篇關於Golang中goroutine和channel使用介紹深入分析的文章就介紹到這了,更多相關Go goroutine與channel內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com