[A Tour of Go 學習筆記] 09 併發(concurrency)
使用官方教學瞭解 Go 的併發
初入 Golang 的世界,首先我們使用官方教學:A Tour of Go
來認識基本的 Golang 使用
這篇介紹 go 強大的併發(concurrency)
Goroutine
透過 Go runtime 運作的輕量級的執行緒管理
會啟動並執行一個新的Goroutine,可以理解為建立了一個新的 Thread
go f(x, y, z)
以go
開頭來調用函式,可以使指定的函式 f 跑在另一個 Goroutine 上
f
,x
,y
,z
取自目前的 Goroutine,而main
也是執行在同一個 Thread 裡
下面範例中say("world")
會開啟另一個執行序,並行於原本的執行序
而原本的執行序會執行say("hello")
一般情況下會先執行say("world")
,待執行完才執行say("hello")
如果加上go
關鍵字調用say("world")
,變成go say("world")
會開啟一個新的 Goroutine,而原本的執行序會繼續執行say("hello")
兩個執行緒會同時運行,當 Main Goroutine 執行結束後,其他的 Goroutine 會被強制關閉
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
/*
>>> world
>>> hello
>>> world
>>> hello
>>> hello
>>> world
>>> world
>>> hello
>>> hello
*/
Channels
Channel 是帶有類型的通訊,可以透過 <-
來發送或是接收值
具有阻塞
的特性,可以讓執行緒進行等待
和 maps 跟 slices 一樣,在使用前必須先建立 channels
ch := make(chan int)
因為 Channel 就像水管一樣,發送和接收會暫停直到另一邊準備完成
直到另一端完成推入或是拉出的動作後才會繼續往下處理,可以被用在 Goroutines 間同步資料內容
ch <- v // 將 v 發送到 channel ch
v := <-ch // 從 ch 接收值,並且指派給 v
以下範例示範了對切片中的數求和,再將任務分配給兩個 Goroutines
要等到兩個 Goroutines 內的計算都被完成,才會計算出最終結果
package main
import "fmt"
func sum(s []int, ch chan int) {
sum := 0
for _, v := range s {
sum += v
}
ch <- sum // send sum to ch
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
ch := make(chan int)
go sum(s[:len(s)/2], ch)
go sum(s[len(s)/2:], ch)
x, y := <-ch, <-ch // receive from ch
fmt.Println(x, y, x+y)
}
/*
>>> -5 17 12
*/
Buffered Channels
Channels 可以帶有緩衝(buffered),將緩衝長度作為 channel 初始化時候的第二個參數傳入
前面提到的一般 Channel 具有以下特色
- 推入一個資料卻還未拉取資料時,會造成推入方的等待其他 Goroutine 拉出
- Goroutine 拉出時 Channel 中沒有資料,會造成拉出方的等待其他 Goroutine 推入
而若是帶上了緩衝,則會變成只會在 Buffered 中資料填滿以後才會阻塞造成等待
如以下範例中,只有到第101
個資料被推入後,推入方的 Goroutine 才會開始進行等待
ch := make(chan int, 100)
緩衝長度代表這個 channel 可以緩衝儲存的資料數量,如果傳入大於緩衝長度的元素數量則會發生deadlock
package main
import "fmt"
func main() {
ch := make(chan int, 1)
ch <- 1
ch <- 2
fmt.Println(<-ch)
fmt.Println(<-ch)
}
/*
>>> fatal error: all goroutines are asleep - deadlock!
*/
如果有取出就不會發生錯誤
package main
import "fmt"
func main() {
ch := make(chan int, 1)
ch <- 1
fmt.Println(<-ch)
ch <- 2
fmt.Println(<-ch)
}
/*
>>> 1
>>> 2
*/
Range 和 Close
發送者可以透過close
關閉一個 Channel 來表示沒有需要發送的值了
接收者可以通過第二個參數來測試 Channel 是不是被關閉
若沒有值可以接收,且 channel 被關閉的話ok
會被設為 false
v, ok := <-ch
如果對一個 channel 做 for 迴圈for i := range c
則會不斷從 Channel 接收值,直到他被關閉
package main
import (
"fmt"
)
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}
/*
>>> 0
>>> 1
>>> 1
>>> 2
>>> 3
>>> 5
>>> 8
>>> 13
>>> 21
>>> 34
*/
若是向已經關閉的 channel 發送資料會引發panic
,所以應該由推入的 Goroutine 執行關閉 Channel
還有就是 Channel 和檔案不同,通常情況下不需要手動關閉
只有在必須告訴接收者不再有新的值的時候,例如一個range
迴圈,才需要關閉
Select
可以透過select
處理 Channel 的多種情況,其中包括阻塞時的處理
select {
case i := <-c:
// use i
default:
// receiving from c would block
}
當所有 case 都沒符合的時候會執行default
package main
import (
"fmt"
"time"
)
func main() {
tick := time.Tick(100 * time.Millisecond)
boom := time.After(500 * time.Millisecond)
for {
select {
case <-tick:
fmt.Println("tick.")
case <-boom:
fmt.Println("BOOM!")
return
default:
fmt.Println(" .")
time.Sleep(50 * time.Millisecond)
}
}
}
/*
>>> .
>>> .
>>> tick.
>>> .
>>> .
>>> tick.
>>> .
>>> .
>>> tick.
>>> .
>>> .
>>> tick.
>>> .
>>> .
>>> tick.
>>> BOOM!
*/
sync.Mutex
前面我們使用 Channel 讓 Goroutine 之前可以進行溝通傳遞資料
因為阻塞的特點,當拉取時若沒資料,便會等待其他的 Goroutine 將資料推入 Channel
那如果我們不使用 Channel,而是直接在多個 Goroutine 之間共用變數的話呢?
當多個 Goroutine 之間共用變數,同時對一個變數做操作
便會發生 Race Condition,這時候會沒辦法保證運算結果的正確性
這牽涉到 互斥(mutual exclusion) 的概念,可以使用 互斥鎖(Mutex) 來避免衝突
Go 提供了一個 struct:sync.Mutex
提供兩個方法
- Lock
- Unlock
在Lock
和Unlock
之間的操作,會使其他的 Goroutine 進入等待
package main
import (
"fmt"
"sync"
"time"
)
// SafeCounter is safe to use concurrently.
type SafeCounter struct {
mu sync.Mutex
v map[string]int
}
// Inc increments the counter for the given key.
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
// Lock so only one goroutine at a time can access the map c.v.
c.v[key]++
c.mu.Unlock()
}
// Value returns the current value of the counter for the given key.
func (c *SafeCounter) Value(key string) int {
c.mu.Lock()
// Lock so only one goroutine at a time can access the map c.v.
defer c.mu.Unlock()
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Inc("somekey")
}
time.Sleep(time.Second)
fmt.Println(c.Value("somekey"))
}
/*
>>> 1000
*/