《Go語言聖經》學習筆記:8. Goroutine和Channel

8. Goroutine和Channel

這兩個的實現是Go大受歡迎的原因。在其他的一些主流語言,實現線程佔用內存資源比較大還有線程之間的通訊必須通過複雜的加鎖機制來實現。Goroutine和Channel的出現就是爲了解決這一個問題。

  • Goroutine是由官方實現的超級“線程池”,每一個實例4-5kb的棧內存佔用和由於實現機制而大幅度減少和創建和銷燬開銷,是Go號稱高併發的根本原因。
  • 併發不是並行:併發主要由切換時間片來實現“同時”運行,而並行則是直接利用多核實現多線程運行,但Go可以設置使用核數,以發揮多核計算機的能力。
  • Go奉行通過通信來共享內存,而不是共享內存來通信。

Goroutine

Goroutine翻譯過來應該叫做協程,或者叫做微線程或者用戶態輕量級線程。一個協程或者多個協程對應着一個線程,協程的實現實在用戶態中,這就避免了要陷入內核態進行操作,節省了不少時間。具體可以看看這篇《Golang:線程 和 協程 的區別》

在Go中實現一個協程是很簡單的,只需要在函數之前使用‘‘ go “就好了,比如:

func Hello()  {   
    fmt.Println("Hello World in func")
}

func main()  {   
    go Hello()   // 對hello函數創建協程
    fmt.Println("Hello world in main")
}

但是運行以上代碼會發現只輸出了Hello world in main

爲了解釋這個原因,我們先可以這樣main是一個主協程,它可以控制由其產生的協程。另外協程的運作方式是時間片運作方式,也就是一個協程運行一段時間,然後交個其他協程運行(這也是並行的原理)。

而在上面的代碼段中,主協程由於進行的操作很少,一個時間片就能運行完,所以Hello這個協程還沒來得及運行主協程就結束了。而主協程是整個程序的主體,一旦結束其他的所有操作也就會結束,包括Hello這個協程。

那麼,怎麼辦呢?最簡單粗暴的方法就是讓主協程“等”一會,等Hello這個協程運行完再結束。使用time.Sleep讓主協程“睡”一會:

func main()  {
	go Hello()
	time.Sleep(time.Second)   			// 等待一秒
	fmt.Println("Hello world in main")
}

這樣一來,就可以得到我們想要的結果了:

Hello World in func
Hello world in main

但是上面的程序有一個很大的問題,就是主協程等待的時間很有可能太長了或者我們很難控制主協程要等待的時間。比如Hello這個協程僅運行了0.1s(假設而已),而主協程卻等待了1s,那麼就有0.9s的時間浪費了。

那麼有沒有一種機制來告訴主協程,Hello協程運行完了呢?答案就是Channel

Channel

特性:

  • Channel是Goroutine溝通的橋樑,大都是阻塞同步的
  • 通過make創建,close關閉
  • Channel是引用類型
  • 可以使用for range類不斷迭代操作Channel
  • 可以設置單向或者雙向通道
  • 可以設置緩存大小,在未被填滿前不會發生阻塞
  • 永遠不要在關閉的通道寫數據

Channel翻譯過來就叫做通道或者消息通道。我們把協程比做人,那麼Channel就是快遞員(送信員也可以)。

通過make創建,close關閉

c := make(chan, bool) // 創建bool類型的通道,當然也可以是其他類型。
//c := make(chan, bool, 2)		// 創建容量爲2的通道,也叫做異步通道
close(c)

用通道改寫上一小節的代碼,實現協程間的通訊:

func main()  {
	c := make(chan bool)
	// 爲了方便使用c,將Hello函數改寫爲匿名函數
	go func() {
		fmt.Println("Hello World in func")
		c <- true
	}()
	<- c
	fmt.Println("Hello World in main")
}

上面的代碼執行的效果和上一節最後一個代碼是一樣的。

在主協程中<- c表示在通道中取出數據,但是通道如果沒有數組取出來,那麼將會一直阻塞(等待),直到從通道中順利取出一個數據爲止。而匿名函數實現的協程一旦將數據寫入c,主協程就會立刻被通知可以取數據,取出數據之後便可以順利的往下執行其他操作了。這就實現了兩個協程之間的操作。

可以使用for range類不斷迭代操作Channel:

for range操作Channel要注意的是,必須有要手動關閉通道,不然會造成阻塞:

func main()  {
	c := make(chan bool)
	// 爲了方便使用c,將Hello函數改寫爲匿名函數
	go func() {
		fmt.Println("Hello World in func")
		c <- true
	}()
	for v := range c {
		fmt.Println(v)
	}
	fmt.Println("Hello World in main")
}

結果:

Hello World in func
true
fatal error: all goroutines are asleep - deadlock!

可以看到造成死鎖(deadlock)了,也就是for range 一直在等待c傳輸數據過來,但是我們的匿名函數協程已經運行完畢,不會繼續往通道中寫數據了。

爲了解決這個問題,一定要手動關閉通道,告訴for range通道關閉了,停止繼續去參數的操作:

func main()  {
	c := make(chan int)
	go func() {
		fmt.Println("Hello World in func")
		c <- 10
		close(c)
	}()
	for v := range c {
		fmt.Println(v)
	}
	fmt.Println("Hello World in main")
}

結果:

Hello World in func
10
Hello World in main

可以設置單向或者雙向通道

我們在將通道傳遞給一個函數的時候,有時只需要往通道里面寫數據,有時只需要讀數據。那麼我們就可以聲明一個單向通道,只允許讀或寫的操作。當然我們也可以定義一個雙向通道。

首先雙向通道的聲明和我們之前學得是一樣的 var ch chan int

func main()  {
	var ch = make(chan int)
	go Go(ch)
	ch <- 20
	get := <- ch
	fmt.Println(get)
}

func Go(ch chan int)  {
	temp := <- ch
	temp++
	ch <- temp
}

可以用<-->來聲明通道的可讀性或者可寫性。

var wg  = sync.WaitGroup{}
func main()  {
	var ch = make(chan int)
	wg.Add(2)
	go test1(ch)
	go test2(ch)
	wg.Wait()
}

func test1(ch <-chan int)  {
	temp := <- ch
	fmt.Println(temp)
	//ch <- 21      // 錯誤
	wg.Done()
}

func test2(ch chan<- int)  {
	ch <- 20
	//temp := <-ch 	// 錯誤
	wg.Done()
}

上述代碼中加入WaitGroup進行流程控制,避免主協程過快停止運行。

永遠不要在關閉的通道寫數據

在關閉的通道寫數據會引發panic:

func main()  {
	c := make(chan int)
	for i:=0; i<10; i++ {
		go func(a int) {
			c <- a
			if a == 9 {
				close(c)
			}
		}(i)
	}
	for v := range c {
		fmt.Printf("%d ", v)
	}
	fmt.Println("In Main")
}

由於協程是隨機調度的,所以有可能10個協程不是按順序執行的,如果a==9在有的數字還沒執行前執行了,並且關閉了通道,那麼很有可能其他協程在寫數據的時候會報錯。

Selet

  • select 是 Go 中的一個控制結構,類似於用於通信的 switch 語句。每個 case 必須是一個通信操作,要麼是發送要麼是接收。
  • 可以處理一個或者多個Channel的發送和接收
  • 同時有多個可用的Channel時按隨機順序處理
  • 可用空的select來阻塞main函數
  • 可以設置超時

select一般和case配合使用,並且case是隨機選擇執行的。

處理兩個channel:

func main() {
	c1, c2 := make(chan int), make(chan string)
	o := make(chan bool)
	go func() {
		for {
			select {
			case v, ok := <- c1:
				if !ok {
					o <- false
					break
				}
				fmt.Println("c1", v)
			case v, ok := <- c2:
				if !ok {
					o <- false
					break
				}
				fmt.Println("c1", v)
			}
		}
	}()
	c1 <- 10
	c2 <- "hello"
	c1 <- 20
	c2 <- "world"

	close(c1)
	close(c2)

	for i:=0; i<2; i++ {
		<- o
	}
}

多個channel同時準備好讀的情況:

func TestMultiChannel() {
    c1 := make(chan interface{}); close(c1)
    c2 := make(chan interface{}); close(c2)
    c3 := make(chan interface{}); close(c3)

    var c1Count, c2Count, c3Count int
    for i := 1000; i >= 0; i-- {
        select {
        case <-c1:
            c1Count++
        case <-c2:
            c2Count++
        case <-c3:
            c3Count++
        }
    }
    fmt.Printf("c1Count: %d\nc2Count: %d\nc3Count: %d\n", c1Count, c2Count, c3Count)
}

輸出:

c1Count: 337
c2Count: 319
c3Count: 345

多運行幾次,可以看出,幾個數字相差都不是很大。
以上例子,同時有3個channel可讀取,從以上的輸出可以看出,select對多個channel的讀取調度是基本公平的。讓每一個channel的數據都有機會被處理。

沒有任何channel準備好,處理超時:

在很多情況下,當channel沒有準備好時,我們希望能夠設置一個超時時間,並在等待channel超時時進行一些處理。此時就可以按以下方式來進行編碼:

func main() {
	c, o := make(chan int),  make(chan bool)

	go func() {
		for {
			select {
			case v := <- c :
				fmt.Println(v)
				o <- true
				return
			case <- time.After(2*time.Second):  // 等待兩秒沒有消息從通道過來打印一下
				fmt.Println("Wait 2 seconds")
			}
		}
	}()

	time.Sleep(6*time.Second)
	c <- 10
	<- o     		// 阻塞,避免主協程提早停止
}

結果:

After
Push
After
After
After

沒有任何channel準備好,處理默認事件:

func TestDefaultProc() {
    start := time.Now()
    var c1, c2 <-chan int
    select {
    case <-c1:
    case <-c2:

    default:
        fmt.Printf("In default after %v\n\n", time.Since(start))
    }
}

注意:default和處理超時不同,當沒有channel可讀取時,會立即執行default分支。而超時的處理,必須要等到超時,才處理。

通過channel通知,從而退出死循環:

func TestExitLoop() {
    done := make(chan interface{})

    go func() {
        time.Sleep(2*time.Second)
        close(done)
    }()

    workCounter := 0
loop:
    for {
        select {
        case <-done:
            break loop
        default:
        }

        // Simulate work
        workCounter++
        time.Sleep(1*time.Second)
    }

    fmt.Printf("在通知退出循環時,執行了%d次.\n", workCounter)
}

**永久等待:**永遠等待,直到有信號中斷。

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