golang學習筆記---Goroutine

什麼是goroutine?golang

Goroutine是創建在線程之上的輕量級的抽象。它容許咱們以很是低的代價在同一個地址空間中並行地執行多個函數或者方法。相比於線程,它的建立和銷燬的代價要小不少,而且它的調度是獨立於線程的。在golang中建立一個goroutine很是簡單,使用「go」關鍵字便可:算法

package main

import (
	"fmt"
	"time"
)

func learning() {
	fmt.Println("My first goroutine")
}
func main() {
	go learning() /* we are using time sleep so that the main program does not terminate before the execution of goroutine.*/
	time.Sleep(1 * time.Second)
	fmt.Println("main function")
}

 輸出:緩存

My first goroutine
main function

  如果將time.sleep拿掉安全

package main

import (
	"fmt"
	//"time"
)

func learning() {
	fmt.Println("My first goroutine")
}
func main() {
	go learning() /* we are using time sleep so that the main program does not terminate before the execution of goroutine.*/
	//time.Sleep(1 * time.Second)
	fmt.Println("main function")
}

  只輸出:網絡

main function

  這是由於,和線程同樣,golang的主函數(其實也跑在一個goroutine中)並不會等待其它goroutine結束。若是主goroutine結束了,全部其它goroutine都將結束。併發

 

 

Goroutine與線程的區別函數

許多人認爲goroutine比線程運行得更快,這是一個誤解。Goroutine並不會更快,它只是增長了更多的併發性。當一個goroutine被阻塞(好比等待IO),golang的scheduler會調度其它能夠執行的goroutine運行。與線程相比,它有如下幾個優勢:高併發

  • 內存消耗更少:

  Goroutine所須要的內存一般只有2kb,而線程則須要1Mb(500倍)。工具

  • 建立與銷燬的開銷更小

  因爲線程建立時須要向操做系統申請資源,而且在銷燬時將資源歸還,所以它的建立和銷燬的開銷比較大。相比之下,goroutine的建立和銷燬是由go語言在運行時本身管理的,所以開銷更低。ui

 

 

  • 切換開銷更小

  這是goroutine於線程的主要區別,也是golang可以實現高併發的主要緣由。線程的調度方式是搶佔式的,若是一個線程的執行時間超過了分配給它的時間片,就會被其它可執行的線程搶佔。在線程切換的過程當中須要保存/恢復全部的寄存器信息,好比16個通用寄存器,PC(Program Counter),SP(Stack Pointer),段寄存器等等。

而goroutine的調度是協同式的,它不會直接地與操做系統內核打交道。當goroutine進行切換的時候,以後不多量的寄存器須要保存和恢復(PC和SP)。所以gouroutine的切換效率更高。

 

Goroutine的調度

真如前面提到的,goroutine的調度方式是協同式的。在協同式調度中,沒有時間片的概念。爲了並行執行goroutine,調度器會在如下幾個時間點對其進行切換:

  • Channel接受或者發送會形成阻塞的消息
  • 當一個新的goroutine被建立時
  • 能夠形成阻塞的系統調用,如文件和網絡操做垃圾回收

下面讓咱們來看一下調度器具體是如何工做的。Golang調度器中有三個概念

  • Processor(P):邏輯處理器,表明着調度的上下文,它使goroutine在一個M上跑
  • OSThread(M):操做系統線程,這是真正的內核OS線程
  • Goroutines(G):擁有本身的棧,指令指針等信息,被P調度

在一個Go程序中,可用的線程數是經過GOMAXPROCS來設置的,默認值是可用的CPU核數。咱們能夠用runtime包動態改變這個值。OSThread調度在processor上,goroutines調度在OSThreads上,

 

 

每一個P會維護一個全局運行隊列(稱爲runqueue),處於ready就緒狀態的goroutine(灰色G)被放在這個隊列中等待被調度。在編寫程序時,每當go func啓動一個goroutine時,runqueue便在尾部加入一個goroutine。在下一個調度點上,P就從runqueue中取出一個goroutine出來執行(藍色G)。

當某個操做系統線程M阻塞的時候(好比goroutine執行了阻塞的系統調用),P能夠綁定到另一個操做系統線程M上,讓運行隊列中的其餘goroutine繼續執行:

 

 

上圖中G0執行了阻塞操做,M0被阻塞,P將在新的系統線程M1上繼續調度G執行。M1有多是被新建立的,或者是從線程緩存中取出。Go調度器保證有足夠的線程來運行全部的P,Go語言運行時默認限制每一個程序最多建立10000個線程,這個如今能夠經過調用runtime/debug包的SetMaxThreads方法來更改。

Go能夠在在一個邏輯處理器P上實現併發,若是須要並行,必須使用多於1個的邏輯處理器。Go調度器會把goroutine平等分配到每一個邏輯處理器上,此時goroutine將在不一樣的線程上運行,不過前提是要求機器擁有多個物理處理器。

package main

import (
	"fmt"
	"runtime"
	"sync"
)

var (
	wg sync.WaitGroup
)

func main() {
	//分配一個邏輯處理器P給調度器使用
	runtime.GOMAXPROCS(1)
	//在這裏,wg用於等待程序完成,計數器加2,表示要等待兩個goroutine
	wg.Add(2)
	//聲明1個匿名函數,並建立一個goroutine
	fmt.Printf("Begin Coroutinesn\n")
	go func() {
		//在函數退出時,wg計數器減1
		defer wg.Done()
		//打印3次小寫字母表
		for count := 0; count < 3; count++ {
			for char := 'a'; char < 'a'+26; char++ {
				fmt.Printf("%c ", char)
			}
			fmt.Println("\n")
		}

	}()
	//聲明1個匿名函數,並建立一個goroutine
	go func() {
		defer wg.Done()
		//打印大寫字母表3次
		for count := 0; count < 3; count++ {
			for char := 'A'; char < 'A'+26; char++ {
				fmt.Printf("%c ", char)
			}
			fmt.Println("\n")
		}
	}()
	fmt.Printf("Waiting To Finish....\n")
	//等待2個goroutine執行完畢
	wg.Wait()
}

  

這個程序使用runtime.GOMAXPROCS(1)來分配一個邏輯處理器給調度器使用,兩個goroutine將被該邏輯處理器調度併發執行。程序輸出:

Begin Coroutinesn
Waiting To Finish....
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 

a b c d e f g h i j k l m n o p q r s t u v w x y z 

a b c d e f g h i j k l m n o p q r s t u v w x y z 

a b c d e f g h i j k l m n o p q r s t u v w x y z 

 從輸出來看,是先執行完一個goroutine,再接着執行第二個goroutine的,大寫字母所有打印完後,再打印所有的小寫字母。那麼,有沒有辦法讓兩個goroutine並行執行呢?爲程序指定兩個邏輯處理器便可: 

 

package main

import (
	"fmt"
	"runtime"
	"sync"
)

var (
	wg sync.WaitGroup
)

func main() {
	//分配一個邏輯處理器P給調度器使用
	runtime.GOMAXPROCS(2)
	//在這裏,wg用於等待程序完成,計數器加2,表示要等待兩個goroutine
	wg.Add(2)
	//聲明1個匿名函數,並建立一個goroutine
	fmt.Printf("Begin Coroutinesn\n")
	go func() {
		//在函數退出時,wg計數器減1
		defer wg.Done()
		//打印3次小寫字母表
		for count := 0; count < 3; count++ {
			for char := 'a'; char < 'a'+26; char++ {
				fmt.Printf("%c ", char)
			}
			fmt.Println("\n")
		}

	}()
	//聲明1個匿名函數,並建立一個goroutine
	go func() {
		defer wg.Done()
		//打印大寫字母表3次
		for count := 0; count < 3; count++ {
			for char := 'A'; char < 'A'+26; char++ {
				fmt.Printf("%c ", char)
			}
			fmt.Println("\n")
		}
	}()
	fmt.Printf("Waiting To Finish....\n")
	//等待2個goroutine執行完畢
	wg.Wait()
}

  結果輸出

Begin Coroutinesn
Waiting To Finish....
A B C D E F a b c d e G H I J K L M N O P Q R S T U V W X Y Z 

A B C D E F G H I J K L M N O P Q R S f g h i j k l m n o p q r s t u v w x y z 

a b c d e f g h i j k l m n o p q r s t u v w x y z 

a b c d e f g h T U V W X Y Z 

A B C D E F G H I J K L M N O P i j k l m n o p q r s t u v w x y z 

Q R S T U V W X Y Z 

 那若是隻有1個邏輯處理器,如何讓兩個goroutine交替被調度?實際上,若是goroutine須要很長的時間才能運行完,調度器的內部算法會將當前運行的goroutine讓出,防止某個goroutine長時間佔用邏輯處理器。因爲示例程序中兩個goroutine的執行時間都很短,在爲引發調度器調度以前已經執行完。不過,程序也可使用runtime.Gosched()來將當前在邏輯處理器上運行的goruntine讓出,讓另外一個goruntine獲得執行:

package main

import (
	"fmt"
	"runtime"
	"sync"
)

var (
	wg sync.WaitGroup
)

func main() {
	//分配一個邏輯處理器P給調度器使用
	runtime.GOMAXPROCS(2)
	//在這裏,wg用於等待程序完成,計數器加2,表示要等待兩個goroutine
	wg.Add(2)
	//聲明1個匿名函數,並建立一個goroutine
	fmt.Printf("Begin Coroutinesn\n")
	go func() {
		//在函數退出時,wg計數器減1
		defer wg.Done()
		//打印3次小寫字母表
		for count := 0; count < 3; count++ {
			for char := 'a'; char < 'a'+26; char++ {
				if char == 'k' {
					runtime.Gosched()
				}
				fmt.Printf("%c ", char)
			}
			fmt.Println("\n")
		}

	}()
	//聲明1個匿名函數,並建立一個goroutine
	go func() {
		defer wg.Done()
		//打印大寫字母表3次
		for count := 0; count < 3; count++ {
			for char := 'A'; char < 'A'+26; char++ {
				if char == 'K' {
					runtime.Gosched()
				}
				fmt.Printf("%c ", char)
			}
			fmt.Println("\n")
		}
	}()
	fmt.Printf("Waiting To Finish....\n")
	//等待2個goroutine執行完畢
	wg.Wait()
}

  

 兩個goroutine在循環的字符爲k/K的時候會讓出邏輯處理器,程序的輸出結果爲:

Begin Coroutinesn
Waiting To Finish....
A B C D E F G H I J a K L b c d e f g h i j M N O P Q R S T U V W X Y Z k l 

A B C D E F G H I J m n o p q r s K L M N O P Q R S T U V W X Y Z 

A B C D E F G H I J t K L M N O P Q R S T U V W X Y Z 

u v w x y z 

a b c d e f g h i j k l m n o p q r s t u v w x y z 

a b c d e f g h i j k l m n o p q r s t u v w x y z 

  這裏大小寫字母果真是交替着輸出了。

處理競爭狀態

 併發程序避免不了的一個問題是對資源的同步訪問。若是多個goroutine在沒有互相同步的狀況下去訪問同一個資源,並進行讀寫操做,這時goroutine就處於競爭狀態下:

package main

import (
	"fmt"
	"runtime"
	"sync"
)

var (
	//counter爲訪問的資源
	counter int64
	wg      sync.WaitGroup
)

func addCount() {
	defer wg.Done()
	for count := 0; count < 2; count++ {
		value := counter
		//當前goroutine從線程退出
		runtime.Gosched()
		value++
		counter = value
	}
}
func main() {
	wg.Add(2)
	go addCount()
	go addCount()
	wg.Wait()
	fmt.Printf("counter: %d\n", counter)
}


//output:counter: 4 或者counter: 2

這段程序中,goroutine對counter的讀寫操做沒有進行同步,goroutine 1對counter的寫結果可能被goroutine 2所覆蓋。Go可經過以下方式來解決這個問題:
  • 使用原子函數操做
  • 使用互斥鎖鎖住臨界區
  • 使用通道chan

檢測競爭狀態

有時候競爭狀態並不能一眼就看出來。Go 提供了一個很是有用的工具,用於檢測競爭狀態。使用方式是:

go build -race example4.go//用競爭檢測器標誌來編譯程序./example4 //運行程序

  

 原子操做實例:

package main

import (
	"fmt"
	"runtime"
	"sync"
	"sync/atomic"
)

var (
	//counter爲訪問的資源
	counter int64
	wg      sync.WaitGroup
)

func addCount() {
	defer wg.Done()
	for count := 0; count < 2; count++ {
		//使用原子操做來進行
		atomic.AddInt64(&counter, 1)
		//當前goroutine從線程退出
		runtime.Gosched()
	}

}
func main() {
	wg.Add(2)
	go addCount()
	go addCount()
	wg.Wait()
	fmt.Printf("counter: %d\n", counter)
}

 這裏使用atomic.AddInt64函數來對一個整形數據進行加操做,另一些有用的原子操做還有:atomic.StoreInt64() //寫 ,   atomic.LoadInt64() //讀 ,更多的原子操做函數請看atomic包中的聲明。

使用互斥鎖

對臨界區的訪問,可使用互斥鎖來進行

package main

import (
	"fmt"
	"runtime"
	"sync"
)

var (
	//counter爲訪問的資源
	counter int64
	wg      sync.WaitGroup
	mutex   sync.Mutex
)

func addCount() {
	defer wg.Done()
	for count := 0; count < 2; count++ {

		//加上鎖,進入臨界區域
		mutex.Lock()
		value := counter
		//當前goroutine從線程退出
		runtime.Gosched()
		value++
		counter = value

		//離開臨界區,釋放互斥鎖
		mutex.Unlock()

	}

}
func main() {
	wg.Add(2)
	go addCount()
	go addCount()
	wg.Wait()
	fmt.Printf("counter: %d\n", counter)
}

  輸出: counter: 4

 

使用Lock()與Unlock()函數調用來定義臨界區,在同一個時刻內,只有一個goroutine可以進入臨界區,直到調用Unlock()函數後,其餘的goroutine纔可以進入臨界區。

在Go中解決共享資源安全訪問,更經常使用的使用通道chan。

 

利用通道共享數據

CSP(Communicating Sequential Process)模型提供一種多個進程公用的「管道(channel)」, 這個channel中存放的是一個個」任務」.

目前正流行的go語言中的goroutine就是參考的CSP模型,原始的CSP中channel裏的任務都是當即執行的,而go語言爲其增長了一個緩存,即任務能夠先暫存起來,等待執行進程準備好了再逐個按順序執行.

 

Go語言採用CSP消息傳遞模型。經過在goroutine之間傳遞數據來傳遞消息,而不是對數據進行加鎖來實現同步訪問。這裏就須要用到通道chan這種特殊的數據類型。當一個資源須要在goroutine中共享時,chan在goroutine中間架起了一個通道。通道使用make來建立:

unbuffered := make(chan int) //建立無緩存通道,用於int類型數據共享

buffered := make(chan string,10)//建立有緩存通道,用於string類型數據共享

buffered<- "hello world" //向通道中寫入數據

value:= <-buffered //從通道buffered中接受數據

通道用於放置某一種類型的數據。建立通道時指定通道的大小,將建立有緩存的通道。無緩存通道是一種同步通訊機制,它要求發送goroutine和接收goroutine都應該準備好,不然會進入阻塞。

 

無緩存的通道

無緩存通道是同步的——一個goroutine向channel寫入消息的操做會一直阻塞,直到另外一個goroutine從通道中讀取消息。反過來也是,一個goroutine從channel讀取消息的操做會一直阻塞,直到另外一個goroutine向通道中寫入消息。《Go in action》中關於無緩存通道的解釋有一個很是棒的例子:網球比賽。在網球比賽中,兩位選手老是處在如下兩種狀態之一:要麼在等待接球,要麼在把球打向對方。球的傳遞可看爲通道中數據傳遞。下面這段代碼使用通道模擬了這個過程:

package main

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

var wg sync.WaitGroup

func player(name string, court chan int) {
	defer wg.Done()
	for {
		//若是通道關閉,那麼選手勝利
		ball, ok := <-court
		if !ok {
			fmt.Printf(" %s Won\n", name)
			return
		}
		n := rand.Intn(100)
		//隨機機率使某個選手Miss
		if n%13 == 0 {
			fmt.Printf(" %s Missed\n", name)
			//關閉通道
			close(court)
			return
		}
		fmt.Printf(" %s Hit %d\n", name, ball)
		ball++
		//不然選手進行擊球
		court <- ball
	}
}
func main() {
	rand.Seed(time.Now().Unix())
	//建立無緩存channel
	court := make(chan int)
	//等待兩個goroutine都執行完
	wg.Add(2)
	//選手1等待接球
	go player("candy", court)
	//選手2等待接球
	go player("luffic", court)
	//球進入球場(能夠開始比賽了)
	court <- 1
	wg.Wait()
}

  

有緩存的通道

有緩存的通道是一種在被接收前能存儲一個或者多個值的通道,它與無緩存通道的區別在於:無緩存的通道保證進行發送和接收的goroutine會在同一時間進行數據交換,有緩存的通道沒有這種保證。有緩存通道讓goroutine阻塞的條件爲:通道中沒有數據可讀的時候,接收動做會被阻塞;通道中沒有區域容納更多數據時,發送動做阻塞。向已經關閉的通道中發送數據,會引起panic,可是goroutine依舊能從通道中接收數據,可是不能再向通道里發送數據。因此,發送端應該負責把通道關閉,而不是由接收端來關閉通道。

小結

  • goroutine被邏輯處理器執行,邏輯處理器擁有獨立的系統線程與運行隊列
  • 多個goroutine在一個邏輯處理器上能夠併發執行,當機器有多個物理核心時,可經過多個邏輯處理器來並行執行。
  • 使用關鍵字 go 來建立goroutine。
  • 在Go中,競爭狀態出如今多個goroutine試圖同時去訪問一個資源時,可使用互斥鎖或者原子函數,去防止競爭狀態的出現。
  • 在go中,更好的解決競爭狀態的方法是使用通道來共享數據。
  • 無緩衝通道是同步的,而有緩衝通道不是。
相關文章
相關標籤/搜索