1. 程式人生 > >golang高併發的實現與注意事項

golang高併發的實現與注意事項

一、併發的意義

     併發的意義就是讓一個程式同時做多件事情,其目的只是為了能讓程式同時做另一件事情而已,而不是為了讓程式執行的更快(如果是多核處理器,而且任務可以分成相互獨立的部分,那麼併發確實可以讓事情解決的更快)。

    golang從語言級別上對併發提供了支援,而且在啟動併發的方式上直接添加了語言級的關鍵字,不必非要按照固定的格式來定義執行緒函式,也不必因為啟動執行緒的時候只能給執行緒函式傳遞一個引數而煩惱。

二、併發的啟動

    go的併發啟動非常簡單,幾乎沒有什麼額外的準備工作,要併發的函式和一般的函式沒有什麼區別,引數隨意,啟動的時候只需要加一個go關鍵之即可,其最精髓的部分在於這些協程(協程類似於執行緒,但是是更輕量的執行緒)的排程。

package main

import (
	"fmt"
	"time"
)

func comFunc() {
	fmt.Println("This is a common function.")
}

func main() {
	go comFunc()
	time.Sleep(time.Second * 3)
}

三、協程間的同步與通訊
1、sync.WaitGroup

    sync包中的WaitGroup實現了一個類似任務佇列的結構,你可以向佇列中加入任務,任務完成後就把任務從佇列中移除,如果佇列中的任務沒有全部完成,佇列就會觸發阻塞以阻止程式繼續執行,具體用法參考如下程式碼:

package main

import (
	"fmt"
	"sync"
)

var waitGroup sync.WaitGroup

func Afunction(index int) {
	fmt.Println(index)
	waitGroup.Done() //任務完成,將任務佇列中的任務數量-1,其實.Done就是.Add(-1)
}

func main() {
	for i := 0; i < 10; i++ {
		waitGroup.Add(1) //每建立一個goroutine,就把任務佇列中任務的數量+1
		go Afunction(i)
	}
	waitGroup.Wait() //.Wait()這裡會發生阻塞,直到佇列中所有的任務結束就會解除阻塞
}
2、channel
channel是一種golang內建的型別,英語的直譯為"通道",其實,它真的就是一根管道,而且是一個先進先出的資料結構。

我們能對channel進行的操作只有4種:
(1) 建立chennel (通過make()函式)
(2) 放入資料 (通過 channel <- data 操作) 
(3) 取出資料 (通過 <-channel 操作)
(4) 關閉channel (通過close()函式)


channel的3種性質入如下:

(1) channel是一種自動阻塞的管道。如果管道滿了,一個對channel放入資料的操作就會阻塞,直到有某個routine從channel中取出資料,這個放入資料的操作才會執行。相反同理,如果管道是空的,一個從channel取出資料的操作就會阻塞,直到某個routine向這個channel中放入資料,這個取出資料的操作才會執行。這是channel最重要的一個性質!!!

package main

func main() {
	ch := make(chan int, 3)
	ch <- 1
	ch <- 1
	ch <- 1
	ch <- 1 //這一行操作就會發生阻塞,因為前三行的放入資料的操作已經把channel填滿了
}
package main

func main() {
	ch := make(chan int, 3)
	<-ch //這一行會發生阻塞,因為channel才剛建立,是空的,沒有東西可以取出
}

(2)channel分為有緩衝的channel和無緩衝的channel。兩種channel的建立方法如下:

ch := make(chan int) 	//無緩衝的channel,同等於make(chan int, 0)
ch := make(chan int, 5) //一個緩衝區大小為5的channel

    無緩衝通道與有緩衝通道的主要區別為:無緩衝通道存取資料是同步的,即如果通道中無資料,則通道一直處於阻塞狀態;有緩衝通道存取資料是非同步的,即存取資料互不干擾,只有當通道中已滿時,存資料操作,通道阻塞;當通道中為空時,取資料操作,通道阻塞。

   因此,使用無緩衝的channel時,放入操作和取出操作不能在同一個routine中,而且應該是先確保有某個routine對它執行取出操作,然後才能在另一個routine中執行放入操作,否則會發生死鎖現象,示例如下:

package main

import (
	"fmt"
	"sync"
)

var waitGroup sync.WaitGroup //使用wg等待所有routine執行完畢,並輸出相應的提示資訊

func AFunc(ch chan int) {
	waitGroup.Add(1)
FLAG:
	for {
		select {
		case val := <-ch:
			fmt.Println(val)
			break FLAG
		}
	}
	waitGroup.Done()
	fmt.Println("WaitGroup Done")
}

func main() {

	ch := make(chan int) //無緩衝通道
	execMode := 0        //執行模式 0:先啟動併發,正常輸出100 1:後啟動併發,發生死鎖
	switch execMode {
	case 0:
		go AFunc(ch)
		ch <- 100
	case 1:
		ch <- 100
		go AFunc(ch)
	}
	waitGroup.Wait()
	close(ch)
}

    使用帶緩衝的channel時,因為有緩衝空間,所以只要緩衝區不滿,放入操作就不會阻塞,同樣,只要緩衝區不空,取出操作就不會阻塞。而且,帶有緩衝的channel的放入和取出操作可以用在同一個routine中。但是,一定要注意放入和取出的速率問題,否則也會發生死鎖現象,示例如下:

package main

import (
	"fmt"
	"sync"
)

var waitGroup sync.WaitGroup

func AFunc(ch chan int, putMode int) {
	val := <-ch
	switch putMode {
	case 0:
		fmt.Printf("Vaule=%d\n", val)
	case 1:
		fmt.Printf("Vaule=%d\n", val)
		for i := 1; i <= 5; i++ {
			ch <- i * val
		}
	case 2:
		fmt.Printf("Vaule=%d\n", val)
		for i := 1; i <= 5; i++ {
			<-ch
		}
	}

	waitGroup.Done()
	fmt.Println("WaitGroup Done", val)
}

func main() {
	ch := make(chan int, 10)
	putMode := 0 //該模式下,能夠正常輸出所有資料
	//putMode := 1//當放入速度遠大於取數速度時,程式阻塞
	//putMode := 2//當取數速度遠大於放數速度時,程式阻塞
	for i := 0; i < 1000; i++ {
		ch <- i
		waitGroup.Add(1)
		go AFunc(ch, putMode)
	}

	waitGroup.Wait()
	close(ch)
}

(3)關閉後的channel可以取資料,但是不能放資料。而且,channel在執行了close()後並沒有真的關閉,channel中的資料全部取走之後才會真正關閉。

package main

func main() {
	ch := make(chan int, 5)
	ch <- 1
	ch <- 1
	close(ch)
	ch <- 1 //不能對關閉的channel執行放入操作
        
        // 會觸發panic
}
package main

func main() {
	ch := make(chan int, 5)
	ch <- 1
	ch <- 1
	close(ch)
	<-ch //只要channel還有資料,就可能執行取出操作

        //正常結束
}
package main

import "fmt"

func main() {
	ch := make(chan int, 5)
	ch <- 1
	ch <- 1
	ch <- 1
	ch <- 1
	close(ch)  //如果執行了close()就立即關閉channel的話,下面的迴圈就不會有任何輸出了
	for {
		data, ok := <-ch
		if !ok {
			break
		}
		fmt.Println(data)
	}
	
	// 輸出:
	// 1
	// 1
	// 1
	// 1
	// 
	// 呼叫了close()後,只有channel為空時,channel才會真的關閉
}
參考連結:https://studygolang.com/articles/1615