天天看點

Go--關于 goroutine、channel

Go--關于 goroutine、channel

goroutine

協程是一種輕量化的線程,由

Go

編譯器進行優化。

Go

協程具有以下特點:

  • 有獨立的棧空間
  • 共享程式堆中的空間
  • 排程由使用者控制

如果主線程

main

函數(主

goroutine

或者

main goroutine

)傳回或者退出時,即使所有協程(

goroutine

)還沒執行完畢,也會退出。當然,協程可以在主線程未退出之前自己執行完畢,并退出。

  • 主線程是一個實體線程,直接作用在

    cpu

    上。是重量級的,非常耗費

    cpu

    資源。
  • 協程從主線程開啟的,是輕量級的線程,是邏輯态的。對資源要求相對較小。
  • Golang

    可以開啟成千上萬個協程。這是

    Golang

    的并發優勢。

MPG模式

Go--關于 goroutine、channel
Go--關于 goroutine、channel
Go--關于 goroutine、channel
  • Go

    1.8後,預設讓程式運作在多個核上,可以不用設定了
  • Go

    1.8前,還是要設定一下,可以更高效的利益

    cpu

numsCPU :=runtime.NumCPU()  //擷取系統CPU數
	runtime.GOMAXPROCS(numsCPU)  //設定運作的CPU數目
           

channel

在此之前,先說明一種實作同步的方式:加鎖(注意這裡說的指互斥鎖)

需求:計算

n!

var lock sync.Mutex //使用全局變量加鎖

func testInput(n int)  {
	res := 1
	for i := 1; i <= n; i++ {
		res *= i
	}
	lock.Lock()
	myMap[n] = uint64(res)
	lock.Unlock()
}

func main() {
	for i := 1; i <= 50; i++ {
		go testInput(i)  
	}

	time.Sleep(time.Second *5)  //不等待會提前結束計算,未計算的線程将被退出
    
	lock.Lock()
	for i,v := range myMap {
		fmt.Println("map[",i,"]=",v)
	}
	lock.Unlock()
}
           

通過加互斥鎖(同步鎖)的方式,并發進行運算、添加,但是這種方式也有缺點:

  • 前面使用全局變量加鎖同步來解決

    goroutine

    的通訊,但不完美
  • 主線程在等待所有

    goroutine

    全部完成的時間很難确定,我們這裡設定 5 秒,僅僅是估算。
  • 如果主線程休眠時間長了,會加長等待時間,如果等待時間短了,可能還有

    goroutine

    處于工作狀态,這時也會随主線程的退出而銷毀。
  • 通過全局變量加鎖同步來實作通訊,也并不利用多個協程對全局變量的讀寫操作。

還可以用

channel

來解決:

func add(s []int , c chan int)  {
	sum := 0
	for _, v := range s {
		sum += v
		fmt.Println(v)
	}
	c <- sum
}

func main() {
	c := make(chan int)
	s :=[]int{2,5,9,23,7,3,4}
	go add(s[:len(s)/2  ] ,c)  //寫channel操作會阻塞,直到讀channel操作執行

	go add( s[len(s)/2:] ,c)

	x ,y:= <-c ,<-c   //随機并發
	fmt.Println(x ,y ,x+y)
}
           

信道是帶有類型的管道,你可以通過它用信道操作符 <- 來發送或者接收值。

ch <- v    // 将 v 發送至信道 ch。
v := <-ch  // 從 ch 接收值并賦予 v。
           

(“箭頭”就是資料流的方向。)

和映射與切片一樣,信道在使用前必須建立:

ch := make(chan int)
           

預設情況下,發送和接收操作在另一端準備好之前都會阻塞。這使得

Go

程可以在沒有顯式的鎖或競态變量的情況下進行同步。

  • channel

    是線程安全的;
  • channel

    本質是隊列,遵循先進先出;
  • channel

    中隻能存放指定的資料類型;
  • channel

    的資料放滿後,就不能再放入;
  • 如果從

    channel

    取出資料後,可以繼續放入;
  • 在沒有使用協程的情況下,如果

    channel

    資料取完了,再取,就會報

    deadlock

還可以放進任意類型(

interface{}

)的資料:

package main

import "fmt"

type Student struct {
	Name string	`json:"name"`  // 是 ` ` (tab鍵上的~按鍵) ,不是 ' '
	Sex string `json:"sex"`
}

func main() {
	allChan := make(chan interface{},5)

	stu1 := Student{Name: "lili",Sex: "f"}
	stu2 := Student{Name: "chang",Sex: "m"}
	stu3 := Student{Name: "ling",Sex: "m"}

	allChan <- stu1
	allChan <- 10
	allChan <- stu2
	allChan <- 99.5
	allChan <- stu3

	<- allChan
	<- allChan
	stuRes := <- allChan
	fmt.Println(stuRes)

	//讀取結構體類型資料字段,需要先進行類型斷言
	stu := stuRes.(Student)
	fmt.Println(stu.Name)
	fmt.Println(stu.Sex)
}
           

由于

channel

interface{}

類型,是以使用的時候,都需要先進行類型斷言。

allChan <- myMap
	<- allChan
	<- allChan
	stuMap := <- allChan
	stus := stuMap.(map[int]Student)
	fmt.Println(stus)
	fmt.Println(stus[0])
	fmt.Println(stus[1])
           
Go--關于 goroutine、channel
allChan <- stu1
	allChan <- 10
	allChan <- stu2
	allChan <- 99.5
	allChan <- stu3

	<- allChan
	n := <- allChan
	n += 1  //報錯
           
Go--關于 goroutine、channel

不使用類型斷言,直接使用将會報錯。因為編譯器并不認識此類型,需要經過類型斷言進行确認。

channel的關閉

channel

關閉使用

close(chan)

,關閉

channel

關閉後不能再向

channel

發送資料,隻能從

channel

讀取資料。

在上面的例子中的

<-allChan

前加入以下代碼:

close(allChan)
           
Go--關于 goroutine、channel

channel的周遊

周遊

channel

之前需要關閉

channel

,否則會報錯(

deadlock

)。

Go--關于 goroutine、channel

關閉

channel

後,即可正常進行周遊

channel

,知道周遊完成,退出周遊。

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

隻讀、隻寫 channel

  • 隻讀

    channel

    :(例如)
var chan1 <-chan int
           
  • 隻寫

    channel

    :(例如)
var chan2 chan<- int
           

案例:

func send(ch chan<- int,exit chan  struct{})  {
	for i := 0; i < 10; i++ {
		ch <- i
		fmt.Println("輸入",i)
	}
	close(ch)
	var  a struct{}
	exit <- a
}

func get(ch <-chan int,exit chan struct{})  {
	for i := 0; i < 10; i++ {
		v,ok := <-ch
		if !ok {
			break
		}
		fmt.Println("輸出",v)
	}
	var  a struct{}
	exit <- a
}

func main() {
	ch := make(chan int ,10)  //雙向通道
	exitChan := make(chan struct{} ,2)
	go send(ch,exitChan)
	go get(ch,exitChan)
	for {
		if len(exitChan) == 2 {
			break
		}
	}
}
           
Go--關于 goroutine、channel

可以使用

for

+

select

語句防止阻塞:

func main() {
	ch := make(chan int ,10)
	for i := 0; i < 5; i++ {
		ch <- i
	}

	ch2 := make(chan float64 ,10)
	for i := 0; i < 5; i++ {
		ch2 <- rand.Float64()
	}
	label:
	for  {
		select {
		case n:= <-ch:
			fmt.Println(n)
			time.Sleep(time.Second)
		case m:=<-ch2:
			fmt.Println(m)
			time.Sleep(time.Second)
		default:
			fmt.Println("沒了")
			time.Sleep(time.Second)
			//return  直接結束退出程式運作
			break label   //中斷指定的 for 循環
		}
	}
}
           
Go--關于 goroutine、channel

使用

defer

+

recover

解決運作時協程中抛出的

panic

,保證程式繼續運作:

func wrong()  {
	defer func(){
		if e := recover() ; e != nil {
			fmt.Print("func wrong()計算錯誤,")
			fmt.Println("異常",e)
		}
	}()

	num := 0
	num1 := 100
	num = num1 /num
	fmt.Println("func wrong()計算正确",num)
}

func right()  {
	defer func(){
		if e := recover() ; e != nil {
			fmt.Println(e)
		}
	}()

	num := 10
	num1 := 100
	num = num1 /num
	fmt.Println("func right() 計算正确",num)
}

func main() {
	go wrong()
	go right()
	for i := 0; i < 5; i++ {
		time.Sleep(time.Second)
		i++
	}
}
           
Go--關于 goroutine、channel

關于

recover

:

内建函數recover允許程式管理恐慌過程中的Go程。在defer的函數中,執行recover調用會取回傳至panic調用的錯誤值,恢複正常執行,停止恐慌過程。若recover在defer的函數之外被調用,它将不會停止恐慌過程式列。在此情況下,或當該Go程不在恐慌過程中時,或提供給panic的實參為nil時,recover就會傳回nil。