一隻箱子裡的貓,看著電腦螢幕

Schrödinger's Programmer

奔跑吧工程師,趁年輕跑得越遠越好

[A Tour of Go 學習筆記] 09 併發(concurrency)

使用官方教學瞭解 Go 的併發

Ray

a photo

初入 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

LockUnlock之間的操作,會使其他的 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
*/

最新文章

Category

Tag