Channel
單純地將函數併發執行是沒有意義的。函數與函數間需要交換數據才能體現併發執行函數的意義。
雖然可以使用共享內存進行數據交換,但是共享內存在不同的goroutine中容易發生競態問題。爲了保證數據交換的正確性,必須使用互斥量對內存進行加鎖,這種做法勢必造成性能問題。
Go語言的併發模型是CSP(Communicating Sequential Processes),提倡通過通信共享內存而不是通過共享內存而實現通信。
如果說goroutine是Go程序併發的執行體,channel就是它們之間的連接。channel是可以讓一個goroutine發送特定值到另一個goroutine的通信機制。
Go 語言中的通道(channel)是一種特殊的類型。通道像一個傳送帶或者隊列,總是遵循先入先出(First In First Out)的規則,保證收發數據的順序。每一個通道都是一個具體類型的導管,也就是聲明channel的時候需要爲其指定元素類型。
channel類型
channel是一種類型,一種引用類型。聲明通道類型的格式如下:
var 變量 chan 元素類型
舉幾個例子:
var ch1 chan int // 聲明一個傳遞整型的通道
var ch2 chan bool // 聲明一個傳遞布爾型的通道
var ch3 chan []int // 聲明一個傳遞int切片的通道
創建channel
通道是引用類型,通道類型的空值是nil。
var ch chan int
fmt.Println(ch) // <nil>
聲明的通道後需要使用make函數初始化之後才能使用。
創建channel的格式如下:
make(chan 元素類型, [緩衝大小])
- channel的緩衝大小是可選的。
舉幾個例子:
ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)
channel操作
通道有發送(send)、接收(receive)和關閉(close)三種操作。
發送和接收都使用<-符號。
現在我們先使用以下語句定義一個通道:
ch := make(chan int)
發送
將一個值發送到通道中。
ch <- 10 // 把10發送到ch中
接收
從一個通道中接收值。
x := <- ch // 從ch中接收值並賦值給變量x
<-ch // 從ch中接收值,忽略結果
關閉
我們通過調用內置的close函數來關閉通道。
close(ch)
關於關閉通道需要注意的事情是,只有在通知接收方goroutine所有的數據都發送完畢的時候才需要關閉通道。
通道是可以被垃圾回收機制回收的,它和關閉文件是不一樣的,在結束操作之後關閉文件是必須要做的,但關閉通道不是必須的。
關閉後的通道有以下特點:
1.對一個關閉的通道,再發送值就會導致panic。
2.對一個關閉的通道,進行接收會一直獲取值直到通道爲空。
3.對一個關閉的並且沒有值的通道,執行接收操作會得到對應類型的零值。
4.關閉一個已經關閉的通道會導致panic。
無緩衝的通道
無緩衝的通道又稱爲阻塞的通道。我們來看一下下面的代碼:
func main() {
ch := make(chan int)
ch <- 10
fmt.Println("發送成功")
}
上面這段代碼能夠通過編譯,但是執行的時候會出現以下錯誤:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
.../src/github.com/pprof/studygo/day06/channel02/main.go:8 +0x54
爲什麼會出現deadlock錯誤呢?
因爲我們使用ch := make(chan int)創建的是無緩衝的通道,無緩衝的通道只有在有人接收值的時候才能發送值。就像你住的小區沒有快遞櫃和代收點,快遞員給你打電話必須要把這個物品送到你的手中,簡單來說就是無緩衝的通道必須有接收才能發送。
上面的代碼會阻塞在ch <- 10這一行代碼形成死鎖,那如何解決這個問題呢?
一種方法是啓用一個goroutine去接收值,例如:
func recv(c chan int) {
ret := <-c
fmt.Println("接收成功", ret)
}
func main() {
ch := make(chan int)
go recv(ch) // 啓用goroutine從通道接收值
ch <- 10
fmt.Println("發送成功")
}
無緩衝通道上的發送操作會阻塞,直到另一個goroutine在該通道上執行接收操作,這時值才能發送成功,兩個goroutine將繼續執行。相反,如果接收操作先執行,接收方的goroutine將阻塞,直到另一個goroutine在該通道上發送一個值。
使用無緩衝通道進行通信將導致發送和接收的goroutine同步化。因此,無緩衝通道也被稱爲同步通道。
有緩衝的通道
解決上面問題的方法還有一種就是使用有緩衝區的通道。
我們可以在使用make函數初始化通道的時候爲其指定通道的容量,例如:
func main() {
ch := make(chan int, 1) // 創建一個容量爲1的有緩衝區通道
ch <- 10
fmt.Println("發送成功")
}
只要通道的容量大於零,那麼該通道就是有緩衝的通道,通道的容量表示通道中能存放元素的數量。就像你小區的快遞櫃只有那麼個多格子,格子滿了就裝不下了,就阻塞了,等到別人取走一個快遞員就能往裏面放一個。
我們可以使用內置的len函數獲取通道內元素的數量,使用cap函數獲取通道的容量,雖然我們很少會這麼做。
close()
可以通過內置的close()函數關閉channel(如果你的管道不往裏存值或者取值的時候一定記得關閉管道)
package main
import "fmt"
func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
close(c)
}()
for {
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}
fmt.Println("main結束")
}
//0
//1
//2
//3
//4
//main結束
如何優雅的從通道循環取值
當通過通道發送有限的數據時,我們可以通過close函數關閉通道來告知從該通道接收值的goroutine停止等待。當通道被關閉時,往該通道發送值會引發panic,從該通道里接收的值一直都是類型零值。那如何判斷一個通道是否被關閉了呢?
我們來看下面這個例子:
package main
import "fmt"
// channel 練習
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
// 開啓goroutine將0~100的數發送到ch1中
go func() {
for i := 0; i < 100; i++ {
ch1 <- i
}
close(ch1)
}()
// 開啓goroutine從ch1中接收值,並將該值的平方發送到ch2中
go func() {
for {
i, ok := <-ch1 // 通道關閉後再取值ok=false
if !ok {
break
}
ch2 <- i * i
}
close(ch2)
}()
// 在主goroutine中從ch2中接收值打印
for i := range ch2 { // 通道關閉後會退出for range循環
fmt.Println(i)
}
}
從上面的例子中我們看到有兩種方式在接收值的時候判斷通道是否被關閉,我們通常使用的是for range的方式。
單向通道
有的時候我們會將通道作爲參數在多個任務函數間傳遞,很多時候我們在不同的任務函數中使用通道都會對其進行限制,比如限制通道在函數中只能發送或只能接收。
Go語言中提供了單向通道來處理這種情況。例如,我們把上面的例子改造如下:
package main
import "fmt"
func counter(out chan<- int) {
for i := 0; i < 100; i++ {
out <- i
}
close(out)
}
func squarer(out chan<- int, in <-chan int) {
for i := range in {
out <- i * i
}
close(out)
}
func printer(in <-chan int) {
for i := range in {
fmt.Println(i)
}
}
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go counter(ch1)
go squarer(ch2, ch1)
printer(ch2)
}
其中,
1.chan<- int是一個只能發送的通道,可以發送但是不能接收;
2.<-chan int是一個只能接收的通道,可以接收但是不能發送。
在函數傳參及任何賦值操作中將雙向通道轉換爲單向通道是可以的,但反過來是不可以的。
通道總結
channel常見的異常總結,如下圖:
注意:關閉已經關閉的channel也會引發panic。
Goroutine池
- 本質上是生產者消費者模型
- 可以有效控制goroutine數量,防止暴漲
- 需求:
- 計算一個數字的各個位數之和,例如數字123,結果爲1+2+3=6
- 隨機生成數字進行計算
package main
import (
"fmt"
"math/rand"
)
type Job struct {
// id
Id int
// 需要計算的隨機數
RandNum int
}
type Result struct {
// 這裏必須傳對象實例
job *Job
// 求和
sum int
}
func main() {
// 需要2個管道
// 1.job管道
jobChan := make(chan *Job, 128)
// 2.結果管道
resultChan := make(chan *Result, 128)
// 3.創建工作池
createPool(64, jobChan, resultChan)
// 4.開個打印的協程
go func(resultChan chan *Result) {
// 遍歷結果管道打印
for result := range resultChan {
fmt.Printf("job id:%v randnum:%v result:%d\n", result.job.Id,
result.job.RandNum, result.sum)
}
}(resultChan)
var id int
// 循環創建job,輸入到管道
for {
id++
// 生成隨機數
r_num := rand.Int()
job := &Job{
Id: id,
RandNum: r_num,
}
jobChan <- job
}
}
// 創建工作池
// 參數1:開幾個協程
func createPool(num int, jobChan chan *Job, resultChan chan *Result) {
// 根據開協程個數,去跑運行
for i := 0; i < num; i++ {
go func(jobChan chan *Job, resultChan chan *Result) {
// 執行運算
// 遍歷job管道所有數據,進行相加
for job := range jobChan {
// 隨機數接過來
r_num := job.RandNum
// 隨機數每一位相加
// 定義返回值
var sum int
for r_num != 0 {
tmp := r_num % 10
sum += tmp
r_num /= 10
}
// 想要的結果是Result
r := &Result{
job: job,
sum: sum,
}
//運算結果扔到管道
resultChan <- r
}
}(jobChan, resultChan)
}
}
定時器
Timer:時間到了,執行只執行1次
package main
import (
"fmt"
"time"
)
func main() {
// 1.timer基本使用
//timer1 := time.NewTimer(2 * time.Second)
//t1 := time.Now()
//fmt.Printf("t1:%v\n", t1)
//t2 := <-timer1.C
//fmt.Printf("t2:%v\n", t2)
// 2.驗證timer只能響應1次
//timer2 := time.NewTimer(time.Second)
//for {
// <-timer2.C
// fmt.Println("時間到")
//}
// 3.timer實現延時的功能
//(1)
//time.Sleep(time.Second)
//(2)
//timer3 := time.NewTimer(2 * time.Second)
//<-timer3.C
//fmt.Println("2秒到")
//(3)
//<-time.After(2*time.Second)
//fmt.Println("2秒到")
// 4.停止定時器
//timer4 := time.NewTimer(2 * time.Second)
//go func() {
// <-timer4.C
// fmt.Println("定時器執行了")
//}()
//b := timer4.Stop()
//if b {
// fmt.Println("timer4已經關閉")
//}
// 5.重置定時器
timer5 := time.NewTimer(3 * time.Second)
timer5.Reset(1 * time.Second)
fmt.Println(time.Now())
fmt.Println(<-timer5.C)
for {
}
}
Ticker:時間到了,多次執行
package main
import (
"fmt"
"time"
)
func main() {
// 1.獲取ticker對象
ticker := time.NewTicker(1 * time.Second)
i := 0
// 子協程
go func() {
for {
//<-ticker.C
i++
fmt.Println(<-ticker.C)
if i == 5 {
//停止
ticker.Stop()
}
}
}()
for {
}
}
select多路複用
在某些場景下我們需要同時從多個通道接收數據。通道在接收數據時,如果沒有數據可以接收將會發生阻塞。你也許會寫出如下代碼使用遍歷的方式來實現:
for{
// 嘗試從ch1接收值
data, ok := <-ch1
// 嘗試從ch2接收值
data, ok := <-ch2
…
}
這種方式雖然可以實現從多個通道接收值的需求,但是運行性能會差很多。爲了應對這種場景,Go內置了select關鍵字,可以同時響應多個通道的操作。
select的使用類似於switch語句,它有一系列case分支和一個默認的分支。每個case會對應一個通道的通信(接收或發送)過程。select會一直等待,直到某個case的通信操作完成時,就會執行case分支對應的語句。具體格式如下:
select {
case <-chan1:
// 如果chan1成功讀到數據,則進行該case處理語句
case chan2 <- 1:
// 如果成功向chan2寫入數據,則進行該case處理語句
default:
// 如果上面都沒有成功,則進入default處理流程
}
select可以同時監聽一個或多個channel,直到其中一個channel ready
package main
import (
"fmt"
"time"
)
func test1(ch chan string) {
time.Sleep(time.Second * 5)
ch <- "test1"
}
func test2(ch chan string) {
time.Sleep(time.Second * 2)
ch <- "test2"
}
func main() {
// 2個管道
output1 := make(chan string)
output2 := make(chan string)
// 跑2個子協程,寫數據
go test1(output1)
go test2(output2)
// 用select監控
select {
case s1 := <-output1:
fmt.Println("s1=", s1)
case s2 := <-output2:
fmt.Println("s2=", s2)
}
}
如果多個channel同時ready,則隨機選擇一個執行
package main
import (
"fmt"
)
func main() {
// 創建2個管道
int_chan := make(chan int, 1)
string_chan := make(chan string, 1)
go func() {
//time.Sleep(2 * time.Second)
int_chan <- 1
}()
go func() {
string_chan <- "hello"
}()
select {
case value := <-int_chan:
fmt.Println("int:", value)
case value := <-string_chan:
fmt.Println("string:", value)
}
fmt.Println("main結束")
}
Select可以用於判斷管道是否存滿
package main
import (
"fmt"
"time"
)
// 判斷管道有沒有存滿
func main() {
// 創建管道
output1 := make(chan string, 10)
// 子協程寫數據
go write(output1)
// 取數據
for s := range output1 {
fmt.Println("res:", s)
time.Sleep(time.Second)
}
}
func write(ch chan string) {
for {
select {
// 寫數據
case ch <- "hello":
fmt.Println("write hello")
default:
fmt.Println("channel full")
}
time.Sleep(time.Millisecond * 500)
}
}
併發安全和鎖
有時候在Go代碼中可能會存在多個goroutine同時操作一個資源(臨界區),這種情況會發生競態問題(數據競態)。類比現實生活中的例子有十字路口被各個方向的的汽車競爭;還有火車上的衛生間被車廂裏的人競爭。
舉個例子:
package main
import (
"fmt"
"sync"
)
var x int64
var wg sync.WaitGroup
func add() {
for i := 0; i < 5000; i++ {
x = x + 1
}
wg.Done()
}
func main() {
wg.Add(1)
go add()
go add()
wg.Wait()
fmt.Println(x)
}
// 6310 (想要的結果應該是10000)
上面的代碼中我們開啓了兩個goroutine去累加變量x的值,這兩個goroutine在訪問和修改x變量的時候就會存在數據競爭,導致最後的結果與期待的不符。
互斥鎖
互斥鎖是一種常用的控制共享資源訪問的方法,它能夠保證同時只有一個goroutine可以訪問共享資源。Go語言中使用sync包的Mutex類型來實現互斥鎖。
使用互斥鎖來修復上面代碼的問題:
package main
import (
"fmt"
"sync"
)
var x int64
var wg sync.WaitGroup
var lock sync.Mutex
func add() {
for i := 0; i < 5000; i++ {
lock.Lock() // 加鎖
x = x + 1
lock.Unlock() // 解鎖
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}
// 10000
使用互斥鎖能夠保證同一時間有且只有一個goroutine進入臨界區,其他的goroutine則在等待鎖;
當互斥鎖釋放後,等待的goroutine纔可以獲取鎖進入臨界區,多個goroutine同時等待一個鎖時,喚醒的策略是隨機的。
讀寫互斥鎖
互斥鎖是完全互斥的,但是有很多實際的場景下是讀多寫少的,當我們併發的去讀取一個資源不涉及資源修改的時候是沒有必要加鎖的,這種場景下使用讀寫鎖是更好的一種選擇。讀寫鎖在Go語言中使用sync包中的RWMutex類型。
讀寫鎖分爲兩種:讀鎖和寫鎖。
當一個goroutine獲取讀鎖之後,其他的goroutine如果是獲取讀鎖會繼續獲得鎖,如果是獲取寫鎖就會等待;
當一個goroutine獲取寫鎖之後,其他的goroutine無論是獲取讀鎖還是寫鎖都會等待。
讀寫鎖示例:
package main
import (
"fmt"
"sync"
"time"
)
var (
x int64
wg sync.WaitGroup
lock sync.Mutex
rwlock sync.RWMutex
)
func write() {
// lock.Lock() // 加互斥鎖
rwlock.Lock() // 加寫鎖
x = x + 1
time.Sleep(10 * time.Millisecond) // 假設讀操作耗時10毫秒
rwlock.Unlock() // 解寫鎖
// lock.Unlock() // 解互斥鎖
wg.Done()
}
func read() {
// lock.Lock() // 加互斥鎖
rwlock.RLock() // 加讀鎖
time.Sleep(time.Millisecond) // 假設讀操作耗時1毫秒
rwlock.RUnlock() // 解讀鎖
// lock.Unlock() // 解互斥鎖
wg.Done()
}
func main() {
start := time.Now()
for i := 0; i < 10; i++ {
wg.Add(1)
go write()
}
for i := 0; i < 1000; i++ {
wg.Add(1)
go read()
}
wg.Wait()
end := time.Now()
fmt.Println(end.Sub(start))
}
需要注意的是讀寫鎖非常適合讀多寫少的場景,如果讀和寫的操作差別不大,讀寫鎖的優勢就發揮不出來。
Sync
sync.WaitGroup
在代碼中生硬的使用time.Sleep肯定是不合適的,Go語言中可以使用sync.WaitGroup來實現併發任務的同步。 sync.WaitGroup有以下幾個方法:
方法名 | 功能 |
---|---|
(wg * WaitGroup) Add(delta int) | 計數器+delta |
(wg *WaitGroup) Done() | 計數器-1 |
(wg *WaitGroup) Wait() | 阻塞直到計數器變爲0 |
sync.WaitGroup內部維護着一個計數器,計數器的值可以增加和減少。例如當我們啓動了N 個併發任務時,就將計數器值增加N。每個任務完成時通過調用Done()方法將計數器減1。通過調用Wait()來等待併發任務執行完,當計數器值爲0時,表示所有併發任務已經完成。
我們利用sync.WaitGroup將上面的代碼優化一下:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func hello() {
defer wg.Done()
fmt.Println("Hello Goroutine!")
}
func main() {
wg.Add(1)
go hello() // 啓動另外一個goroutine去執行hello函數
fmt.Println("main goroutine done!")
wg.Wait()
}
//main goroutine done!
//Hello Goroutine!
需要注意sync.WaitGroup是一個結構體,傳遞的時候要傳遞指針。
sync.Once
說在前面的話:這是一個進階知識點。
在編程的很多場景下我們需要確保某些操作在高併發的場景下只執行一次,例如只加載一次配置文件、只關閉一次通道等。
Go語言中的sync包中提供了一個針對只執行一次場景的解決方案–sync.Once。
sync.Once只有一個Do方法,其簽名如下:
func (o *Once) Do(f func()) {}
注意:如果要執行的函數f需要傳遞參數就需要搭配閉包來使用。
加載配置文件示例
延遲一個開銷很大的初始化操作到真正用到它的時候再執行是一個很好的實踐。因爲預先初始化一個變量(比如在init函數中完成初始化)會增加程序的啓動耗時,而且有可能實際執行過程中這個變量沒有用上,那麼這個初始化操作就不是必須要做的。我們來看一個例子:
var icons map[string]image.Image
func loadIcons() {
icons = map[string]image.Image{
"left": loadIcon("left.png"),
"up": loadIcon("up.png"),
"right": loadIcon("right.png"),
"down": loadIcon("down.png"),
}
}
// Icon 被多個goroutine調用時不是併發安全的
func Icon(name string) image.Image {
if icons == nil {
loadIcons()
}
return icons[name]
}
多個goroutine併發調用Icon函數時不是併發安全的,現代的編譯器和CPU可能會在保證每個goroutine都滿足串行一致的基礎上自由地重排訪問內存的順序。loadIcons函數可能會被重排爲以下結果:
func loadIcons() {
icons = make(map[string]image.Image)
icons["left"] = loadIcon("left.png")
icons["up"] = loadIcon("up.png")
icons["right"] = loadIcon("right.png")
icons["down"] = loadIcon("down.png")
}
在這種情況下就會出現即使判斷了icons不是nil也不意味着變量初始化完成了。考慮到這種情況,我們能想到的辦法就是添加互斥鎖,保證初始化icons的時候不會被其他的goroutine操作,但是這樣做又會引發性能問題。
使用sync.Once改造的示例代碼如下:
var icons map[string]image.Image
var loadIconsOnce sync.Once
func loadIcons() {
icons = map[string]image.Image{
"left": loadIcon("left.png"),
"up": loadIcon("up.png"),
"right": loadIcon("right.png"),
"down": loadIcon("down.png"),
}
}
// Icon 是併發安全的
func Icon(name string) image.Image {
loadIconsOnce.Do(loadIcons)
return icons[name]
}
sync.Once其實內部包含一個互斥鎖和一個布爾值,互斥鎖保證布爾值和數據的安全,而布爾值用來記錄初始化是否完成。這樣設計就能保證初始化操作的時候是併發安全的並且初始化操作也不會被執行多次。
sync.Map
Go語言中內置的map不是併發安全的。請看下面的示例:
var m = make(map[string]int)
func get(key string) int {
return m[key]
}
func set(key string, value int) {
m[key] = value
}
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
set(key, n)
fmt.Printf("k=:%v,v:=%v\n", key, get(key))
wg.Done()
}(i)
}
wg.Wait()
}
上面的代碼開啓少量幾個goroutine的時候可能沒什麼問題,當併發多了之後執行上面的代碼就會報fatal error: concurrent map writes錯誤。
像這種場景下就需要爲map加鎖來保證併發的安全性了,Go語言的sync包中提供了一個開箱即用的併發安全版map–sync.Map。開箱即用表示不用像內置的map一樣使用make函數初始化就能直接使用。同時sync.Map內置了諸如Store、Load、LoadOrStore、Delete、Range等操作方法。
var m = sync.Map{}
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
m.Store(key, n)
value, _ := m.Load(key)
fmt.Printf("k=:%v,v:=%v\n", key, value)
wg.Done()
}(i)
}
wg.Wait()
}
原子操作(atomic包)
原子操作
代碼中的加鎖操作因爲涉及內核態的上下文切換會比較耗時、代價比較高。針對基本數據類型我們還可以使用原子操作來保證併發安全,因爲原子操作是Go語言提供的方法它在用戶態就可以完成,因此性能比加鎖操作更好。Go語言中原子操作由內置的標準庫sync/atomic提供。
atomic包
方法 | 解釋 |
---|---|
func LoadInt32(addr int32) (val int32) func LoadInt64(addr `int64 ) (val int64)<br>func LoadUint32(addr uint32) (val uint32)<br>func LoadUint64(addr uint64) (val uint64)<br>func LoadUintptr(addr uintptr) (val uintptr)<br>func LoadPointer(addr unsafe.Pointer`) (val unsafe.Pointer) |
讀取操作 |
func StoreInt32(addr *int32 , val int32)func StoreInt64(addr *int64 , val int64)func StoreUint32(addr *uint32 , val uint32)func StoreUint64(addr *uint64 , val uint64)func StoreUintptr(addr *uintptr , val uintptr)func StorePointer(addr *unsafe.Pointer , val unsafe.Pointer) |
寫入操作 |
func AddInt32(addr *int32 , delta int32) (new int32)func AddInt64(addr *int64 , delta int64) (new int64)func AddUint32(addr *uint32 , delta uint32) (new uint32)func AddUint64(addr *uint64 , delta uint64) (new uint64)func AddUintptr(addr *uintptr , delta uintptr) (new uintptr) |
修改操作 |
func SwapInt32(addr *int32 , new int32) (old int32)func SwapInt64(addr *int64 , new int64) (old int64)func SwapUint32(addr *uint32 , new uint32) (old uint32)func SwapUint64(addr *uint64 , new uint64) (old uint64)func SwapUintptr(addr *uintptr , new uintptr) (old uintptr)func SwapPointer(addr *unsafe.Pointer , new unsafe.Pointer) (old unsafe.Pointer) |
交換操作 |
func CompareAndSwapInt32(addr *int32 , old, new int32) (swapped bool)func CompareAndSwapInt64(addr *int64 , old, new int64) (swapped bool)func CompareAndSwapUint32(addr *uint32 , old, new uint32) (swapped bool)func CompareAndSwapUint64(addr *uint64 , old, new uint64) (swapped bool)func CompareAndSwapUintptr(addr *uintptr , old, new uintptr) (swapped bool)func CompareAndSwapPointer(addr *unsafe.Pointer , old, new unsafe.Pointer) (swapped bool) |
比較並交換操作 |
atomic示例
我們填寫一個示例來比較下互斥鎖和原子操作的性能。
var x int64
var l sync.Mutex
var wg sync.WaitGroup
// 普通版加函數
func add() {
// x = x + 1
x++ // 等價於上面的操作
wg.Done()
}
// 互斥鎖版加函數
func mutexAdd() {
l.Lock()
x++
l.Unlock()
wg.Done()
}
// 原子操作版加函數
func atomicAdd() {
atomic.AddInt64(&x, 1)
wg.Done()
}
func main() {
start := time.Now()
for i := 0; i < 10000; i++ {
wg.Add(1)
// go add() // 普通版add函數 不是併發安全的
// go mutexAdd() // 加鎖版add函數 是併發安全的,但是加鎖性能開銷大
go atomicAdd() // 原子操作版add函數 是併發安全,性能優於加鎖版
}
wg.Wait()
end := time.Now()
fmt.Println(x)
fmt.Println(end.Sub(start))
}
atomic包提供了底層的原子級內存操作,對於同步算法的實現很有用。這些函數必須謹慎地保證正確使用。除了某些特殊的底層應用,使用通道或者sync包的函數/類型實現同步更好。
參考鏈接:http://www.topgoer.com/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/channel.html