Goroutines就是一系列的Thread操作,意思即一支程式同時進行好幾個小程式。使用go的時候程式會將go所要執行的項目放到背景執行,繼續進行主程式的程式碼
在完全沒有使用go的情況下進行這一支程式
package main
import "fmt"
func foo() {
for i := 1; i <= 10; i++ {
fmt.Println("Foo: ", i)
}
}
func bar() {
for i := 1; i <= 10; i++ {
fmt.Println("Foo: ", i)
}
}
func main() {
foo()
bar()
}
那如果在執行function前加上go呢?
go foo()
go bar()
完整程式碼:
package main
import "fmt"
func foo() {
for i := 1; i <= 10; i++ {
fmt.Println("Foo: ", i)
}
}
func bar() {
for i := 1; i <= 10; i++ {
fmt.Println("Foo: ", i)
}
}
func main() {
go foo()
go bar()
}
這時候大家可能會發現好像甚麼都沒有執行,原因在於在執行go foo()和go bar()之後,系統將這兩個項目放到背景執行,而接著馬上就是程式main的尾端,代表程式已經結束。所以放到背景執行的foo()和bar()還來不及執行程式就結束了
有鑑於剛剛背景Thread所執行的項目都還沒開始程式就結束了,WaitGroup的功能即為等待這個Group的項目執行完再繼續進行
首先,因為WaitGroup是包含在package sync裏頭的,所以需要引入一下
import "sync"
需要宣告一個WaitGroup變數
var wg sync.WaitGroup
當WaitGroup裏頭的空間等於0的時候就會繼續進行主程式,下面會介紹一些操作方式
增加WaitGroup裡可以容納個Thread
wg.Add(2)
等待WaitGroup裡的Thread執行完畢再繼續進行
wg.Wait()
使WaitGroup的容量-1
wg.Done()
或是
wg.Add(-1)
有了這些基本操作後,我們可以改良剛剛的程式
package main
import (
"fmt"
"sync"
)
func foo() {
for i := 1; i <= 10; i++ {
fmt.Println("Foo: ", i)
}
wg.Done()
}
func bar() {
for i := 1; i <= 10; i++ {
fmt.Println("Foo: ", i)
}
wg.Done()
}
var wg sync.WaitGroup
func main() {
wg.Add(2)
go foo()
go bar()
wg.Wait()
}
這時候Thread裡的內容就有確實執行完畢了
讓程式暫停
這個功能是放在package time裡面的,所以又要引入了
import "time"
使用方法:
time.Sleep(5 * time.Millisecond)
這個範例是暫停五秒鐘
有了go這個好用的東西可以分成好幾個Thread同時進行,那就有可能會有同時搶奪資源的情況。比方說兩個Thread同時都要讀取並修改同一個變數,拿一個情境題來舉例:
如果今天同一個銀行的網路銀行有提款功能,而有人同時在兩台電腦都登入了要提款,兩台電腦都送出了提領1000的請求會怎樣呢?
package main
import (
"fmt"
"sync"
"time"
)
func withdraw() {
balance := money
time.Sleep(3000 * time.Millisecond)
balance -= 1000
money = balance
fmt.Println("After withdrawing $1000, balace: ", money)
wg.Done()
}
var wg sync.WaitGroup
var money int = 1500
func main() {
fmt.Println("We have $1500")
wg.Add(2)
go withdraw() // first withdraw
go withdraw() // second withdraw
wg.Wait()
}
到最後大家會發現1500提領了兩次1000還剩500?為甚麼呢?
因為在第一次提領的時候,系統先讀取餘額為1500元,同時第二台電腦也登入了餘額也是顯示為1500元,這時候就是因為兩邊同時搶著讀取餘額的原因,所以第一次提領1000元時回報給系統"餘額剩500元",第二台領了1000元也回報系統"餘額剩500元"
不過Golang的編譯器可以檢查是不是有race condition,只要在平常執行go run後面加上參數-race即可
$ go run -race main.go
那要如何避免這樣的情況發生呢?Mutex可以解決上面這樣的問題。這個也是在package sync裏頭
import "sync"
先宣告一個Mutex變數
var mu sync.Mutex
當使用mu.Lock()的時候,之後所用到的變數就會上鎖,只有在使用中的Thread可以存取,其他都需要等到釋放後才能存取
mu.Lock()
釋放lock的變數
mu.Unlock()
所以我可以將剛剛withdraw()改良
func withdraw() {
mu.Lock()
balance := money
time.Sleep(3000 * time.Millisecond)
balance -= 1000
money = balance
mu.Unlock()
fmt.Println("After withdrawing $1000, balace: ", money)
wg.Done()
}
因為Lock()和Unlock()通常都會一起出現,所以有些人會這樣寫
func withdraw() {
{
mu.Lock()
balance := money
time.Sleep(3000 * time.Millisecond)
balance -= 1000
money = balance
mu.Unlock()
}
fmt.Println("After withdrawing $1000, balace: ", money)
wg.Done()
}
全部的程式碼
package main
import (
"fmt"
"sync"
"time"
)
func withdraw() {
mu.Lock()
balance := money
time.Sleep(3000 * time.Millisecond)
balance -= 1000
money = balance
mu.Unlock()
fmt.Println("After withdrawing $1000, balace: ", money)
wg.Done()
}
var wg sync.WaitGroup
var money int = 1500
var mu sync.Mutex
func main() {
fmt.Println("We have $1500")
wg.Add(2)
go withdraw() // first withdraw
go withdraw() // second withdraw
wg.Wait()
}
如此一來就解決race condition的問題了