19.Go語言基礎之併發

1.1併發與並行

併發:同一時間段執行多個任務(使用微信和多個朋友聊天)
並行:同一時刻執行多個任務(windows中360在殺毒,同時你也在寫代碼)
Go語言的併發通過goroutine實現。goroutine類似於線程,屬於用戶態的線程,我們可以根據需要創建成千上萬個goroutine併發工作。
goroutine是由Go語言的運行時(runtime)調度完成,而線程是由操作系統調度完成。
Go語言還提供channel在多個goroutine間進行通信。goroutine和channel是Go語言秉承的CSP(Communication Sequential Process)併發模式的重要實現基礎。

1.2goroutine

在java/Python中,我們實現併發編程的時候,通常需要自己維護一個線程池,並且需要自己去包裝一個又一個的任務,同時需要自己去調度線程執行任務並維護上下文切換,這一切需要耗費很多。
Go語言中的goroutine,類似於線程,但goroutine是由Go的運行時(runtime)調度和管理的。Go程序能夠只能的將goroutine中的任務合理的分配到每個CPU。Go語言被稱爲現代化語言的原因,就是因爲Go在語言層面就已經內置了調度和上下文切換的機制。
在Go語言編程中,不需要自己寫進程、線程、協程,你的技能只有一個,就是goroutine。

1.2.1使用goroutine

Go語言中使用goroutine非常簡單,只需要在調用函數前面加上"go"關鍵字,就可以爲一個函數創建一個goroutine。
一個goroutine必定對應一個函數,可以創建多個goroutine去執行相同的函數。

1.2.2啓動單個goroutine

沒有使用goroutine時,程序是順序運行的。

//

package main

import (
    "fmt"
)

func hello()  {
    fmt.Println("Hello Goroutine!")
}
func main() {
    hello()
    fmt.Println("main goroutine done!")
}

結果:
Hello Goroutine!
main goroutine done!

Process finished with exit code 0

使用go關鍵字

Mac系統上實驗
package main

import (
    "fmt"
)

func hello()  {
    fmt.Println("Hello Goroutine!")
}
func main() {
    go hello()
    fmt.Println("main goroutine done!")
}

結果1:
main goroutine done!

Process finished with exit code 0

結果2:
main goroutine done!
Hello Goroutine!

Process finished with exit code 0

結果3:
Hello Goroutine!
main goroutine done!

Process finished with exit code 0
會發現,出現了只打印了main goroutine done的現象,是因爲main函數也是一個goroutine,main函數執行完了,整個程序就結束了。

1.2.3啓動多個goroutine

Go語言中實現併發就是這麼簡單,可以啓動多個goroutine。
這裏使用sync.WaitGroup來實現goroutine的同步。
package main

import (
    "fmt"
    "sync"
)
var wg sync.WaitGroup

func hello(i interface{})  {
    defer wg.Done() //goroutine結束就登記-1
    fmt.Println("Hello Goroutine! i:",i)
}
func main() {
    for i:=0;i<10;i++{
        wg.Add(1) //啓動一個goroutine就登記+1
        go hello(i)
    }
    wg.Wait()//等待所有等級的goroutine都結束
}

結果:
Hello Goroutine! i: 9
Hello Goroutine! i: 7
Hello Goroutine! i: 2
Hello Goroutine! i: 0
Hello Goroutine! i: 3
Hello Goroutine! i: 5
Hello Goroutine! i: 1
Hello Goroutine! i: 6
Hello Goroutine! i: 4
Hello Goroutine! i: 8

Process finished with exit code 0
多次執行上面的代碼,會發現每次打印的數字順序都不一樣。這是因爲10個goroutine是併發執行的,而goroutine的調度室隨機的。

1.3goroutine與線程

1.3.1可增長的棧

OS線程(操作系統線程)一般都有固定的棧內存(通常爲2MB),一個goroutine的棧在其生命週期開始時只有很小的棧(典型情況下2KB),goroutine的棧不是固定的,他可以按需增大和縮小,goroutine的棧大小限制可以達到1GB,雖然很少會用到這麼大。

1.3.2goroutine調度

GPM是Go語言運行時(runtime)層面的實現,是go語言自己實現的一套調度系統。區別於操作系統調度OS線程。

G很好理解,就是個goroutine的,裏面除了存放本goroutine信息外 還有與所在P的綁定等信息。
P管理着一組goroutine隊列,P裏面會存儲當前goroutine運行的上下文環境(函數指針,堆棧地址及地址邊界),P會對自己管理的goroutine隊列做一些調度(比如把佔用CPU時間較長的goroutine暫停、運行後續的goroutine等等)當自己的隊列消費完了就去全局隊列裏取,如果全局隊列裏也消費完了會去其他P的隊列裏搶任務。
M(machine)是Go運行時(runtime)對操作系統內核線程的虛擬, M與內核線程一般是一一映射的關係, 一個groutine最終是要放到M上執行的;
P與M一般也是一一對應的。他們關係是: P管理着一組G掛載在M上運行。當一個G長久阻塞在一個M上時,runtime會新建一個M,阻塞G所在的P會把其他的G 掛載在新建的M上。當舊的G阻塞完成或者認爲其已經死掉時 回收舊的M。

P的個數是通過runtime.GOMAXPROCS設定(最大256),Go1.5版本之後默認爲物理線程數。 在併發量大的時候會增加一些P和M,但不會太多,切換太頻繁的話得不償失。

單從線程調度講,Go語言相比起其他語言的優勢在於OS線程是由OS內核來調度的,goroutine則是由Go運行時(runtime)自己的調度器調度的,這個調度器使用一個稱爲m:n調度的技術(複用/調度m個goroutine到n個OS線程)。
其一大特點是goroutine的調度是在用戶態下完成的, 不涉及內核態與用戶態之間的頻繁切換,包括內存的分配與釋放,都是在用戶態維護着一塊大的內存池, 不直接調用系統的malloc函數(除非內存池需要改變),成本比調度OS線程低很多。 
另一方面充分利用了多核的硬件資源,近似的把若干goroutine均分在物理線程上, 再加上本身goroutine的超輕量,以上種種保證了go調度方面的性能。

1.3.3GOMAXPROCS

Go運行時的調度器使用GOMAXPROCS參數來確定需要使用多少個OS線程來同時執行Go代碼。默認值是機器上的CPU核心數。
例如在一個8個CPU的機器上,調度器會把Go代碼同時調度到8個OS線程上(GOMAXPROCS是m:n中的n)
Go語言通過runtime.GOMAXPROCS()函數設置當前程序併發時佔用的CPU邏輯核心數。
我們可以通過將任務分配到不同的CPU邏輯核心上實現並行的效果,這裏舉個例子:
設置GOMAXPROCS=1,goroutine啓動兩個任務,此時是一個任務執行完了才能執行另一個任務

package main

import (
    "fmt"
    "runtime"
    "time"
)
func a() {
    for i := 1; i < 10; i++ {
        fmt.Println("A:", i)
    }
}

func b() {
    for i := 1; i < 10; i++ {
        fmt.Println("B:", i)
    }
}

func main() {
    runtime.GOMAXPROCS(1)
    go a()
    go b()
    time.Sleep(time.Second)
}

結果:
A: 1
A: 2
A: 3
A: 4
A: 5
A: 6
A: 7
A: 8
A: 9
B: 1
B: 2
B: 3
B: 4
B: 5
B: 6
B: 7
B: 8
B: 9

Process finished with exit code 0
設置GOMAXPROCS=2,goroutine啓動兩個任務,兩個任務同時執行,出現兩個任務交互打印現象,要多試幾次,需要筆記本是多個CPU哦!我在mac上測試成功的。
package main

import (
    "fmt"
    "runtime"
    "time"
)
func a() {
    for i := 1; i < 10; i++ {
        fmt.Println("A:", i)
    }
}

func b() {
    for i := 1; i < 10; i++ {
        fmt.Println("B:", i)
    }
}

func main() {
    runtime.GOMAXPROCS(2)
    go a()
    go b()
    time.Sleep(time.Second)
}

結果:
A: 1
B: 1
B: 2
B: 3
B: 4
B: 5
B: 6
B: 7
B: 8
B: 9
A: 2
A: 3
A: 4
A: 5
A: 6
A: 7
A: 8
A: 9

Process finished with exit code 0
//Go語言中的操作系統線程和goroutine的關係:
1.一個操作系統線程對應用戶態多個goroutine。
2.go程序可以同時使用多個操作系統線程。
3.goroutine和OS線程是多對多的關係,即m:n。

1.4channel

單純地將函數併發執行是沒有意義的。函數與函數間需要交換數據才能體現併發執行函數的意義。

雖然可以使用共享內存進行數據交換,但是共享內存在不同的goroutine中容易發生競態問題。爲了保證數據交換的正確性,必須使用互斥量對內存進行加鎖,這種做法勢必造成性能問題。

Go語言的併發模型是CSP(Communicating Sequential Processes),提倡通過通信共享內存而不是通過共享內存而實現通信。

如果說goroutine是Go程序併發的執行體,channel就是它們之間的連接。channel是可以讓一個goroutine發送特定值到另一個goroutine的通信機制。

Go 語言中的通道(channel)是一種特殊的類型。通道像一個傳送帶或者隊列,總是遵循先入先出(First In First Out)的規則,保證收發數據的順序。每一個通道都是一個具體類型的導管,也就是聲明channel的時候需要爲其指定元素類型。

1.4.1channel類型

channel是一種類型,一種引用類型。生命通道類型的格式如下:
var 變量 chan 元素類型
package main

import (
    "fmt"
)

func main() {
    var ch1 chan int   // 聲明一個傳遞整型的通道
    var ch2 chan bool  // 聲明一個傳遞布爾型的通道
    var ch3 chan []int // 聲明一個傳遞int切片的通道

    fmt.Printf("v:%v type:%T\n",ch1,ch1)
    fmt.Printf("v:%v type:%T\n",ch2,ch2)
    fmt.Printf("v:%v type:%T\n",ch3,ch3)
}

結果:
v:<nil> type:chan int
v:<nil> type:chan bool
v:<nil> type:chan []int

Process finished with exit code 0

1.4.2創建channel

通道是引用類型,通道類型的空值是nil。
var ch chan int
fmt.Println(ch) // <nil>

聲明的通道後需要使用make函數初始化後才能使用。
創建channel的格式如下:
make(chan 元素類型, [緩衝大小])
channel的緩衝大小是可選的。
package main

import (
    "fmt"
)

func main() {
    ch4 := make(chan int)
    ch5 := make(chan bool)
    ch6 := make(chan []int)

    fmt.Printf("v:%v type:%T\n",ch4,ch4)
    fmt.Printf("v:%v type:%T\n",ch5,ch5)
    fmt.Printf("v:%v type:%T\n",ch6,ch6)
}

結果:
v:0xc000012060 type:chan int
v:0xc0000120c0 type:chan bool
v:0xc000012120 type:chan []int

Process finished with exit code 0

1.4.3channel操作

通道有發送(send)、接收(receive)和關閉(close)三種操作。
發送和接收都使用<-
定義通道:ch := make(chan int)

發送:將一個值發送到通道中。
           ch <- 10 //把10發送到通道中
接收:從一個通道中接收值。
           a := <- ch //從ch中接收值,並賦值給a
                  <- ch        //從ch中接收值,忽略結果
關閉:關閉通道。
           close(ch)
                     注意:
                         1.只有在通知接收方goroutine所有的數據都發送完畢的時候,才需要關閉通道。
                         2.通道是可以被垃圾回收機制回收的,與關閉文件不一樣,文件操作結束後文件是必須關閉的,但通道不是必須關閉的。
                     關閉後的通道有以下特點:
                         1.對一個關閉的通道再發送值會導致panic。
                         2.對一個關閉的通道進行接收值,會一直獲取值直到通道爲空。
                         3.對一個關閉的並且沒有值得通道執行接收操作,會得到對應類型的零值。
                         4.關閉一個已經關閉的通道會導致panic。

1.4.4無緩衝的通道

無緩衝通道稱爲阻塞通道。無緩衝通道必須在發送數據的同時有人接收值,否則會阻塞在那裏,直到報錯。
//無緩衝通道,只發送值不接收值的時候會出現deadlock錯誤。

package main

import "fmt"

func main() {
    ch := make(chan int)
    ch <- 10
    fmt.Printf("發送成功!")
}

結果:
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
        /Users/tongchao/Desktop/gopath/src/test/test.go:7 +0x54

Process finished with exit code 2

因爲我們使用ch := make(chan int)創建的是無緩衝通道,無緩衝通道只有在有人接收值的時候才能發送值。
上買呢代碼會阻塞在ch <- 10,這一行代碼會形成死鎖。
解決方法:使用goroutine去接收值
package main

import (
    "fmt"
    "sync"
)
var wg sync.WaitGroup
func recv(ch chan int)  {
    defer wg.Done()
    i := <- ch
    fmt.Println("接收的值是:",i)
}
func main() {
    ch := make(chan int)
    wg.Add(1)
    go recv(ch)

    ch <- 10
    wg.Wait()

    fmt.Printf("發送成功!\n")
}

結果:
接收的值是: 10
發送成功!

Process finished with exit code 0

無緩衝通道上的發送操作會阻塞,直到另一個goroutine在該通道上執行接收操作,這時才能發送成功,兩個goroutine將繼續執行。
如果接收操作限制性,接收方的goroutine將會阻塞,直到另一個goroutine在該通道上發送一個值。
//使用無緩衝通道進行通信,將會導致發送和接收的goroutine同步化。因此,無緩衝通道也被稱爲同步通道。

1.4.5有緩衝的通道

解決上面問題的方法還有一種就是使用有緩衝的通道。我們可以在使用make函數初始化通道的時候爲其制定通道的容量,只要通道的容量大於零,就是有緩衝的通道,通道的容量表示通道中能存放的元素的數量。
可以使用len()獲取通道內元素的數量,使用cap函數獲取通道的容量。
package main

import (
    "fmt"
)

func main() {
    ch := make(chan int,1) //創建一個容量爲1的有緩衝區通道

    ch <- 10

    fmt.Printf("發送成功!\n")
    fmt.Println("len(ch):",len(ch))
    fmt.Println("cap(ch)",cap(ch))
}

結果:
發送成功!
len(ch): 1
cap(ch) 1

Process finished with exit code 0

1.4.6for range從通道循環取值

當向通道中發送完數據時,我們可以通過close函數關閉通道。
當通道被關閉時,再往該通道發送值會引發panic,從該通道里接收值一直都是類型0值。那麼如何判斷一個通道是否被關閉了呢?
方法一:
    i, ok := <-ch1 // 通道關閉後再取值ok=false
方法二:
    for range遍歷通道,通道被關閉時就會退出for range。
package main

import "fmt"

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    //開啓goroutine將0-100的數發送到ch1中
    go func() {
        for i:=0;i<101;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)

    }
}

結果:
0
1
4
9
16
25
...
9604
9801
10000

Process finished with exit code 0

1.4.7單向通道

有的時候我們會將通道作爲參數在多個任務函數間傳遞,很多時候我們在不同的任務函數中使用通道都會對其進行限制,比如限制通道在函數中只能發送或只能接收。
chan <- int是一個只寫單向通道(只能對其寫入int類型值),可以對其進行發送操作但不能執行接收操作;
<- chan int是一個只讀單向通道(只能從通道讀取int類型值),可以對其執行接收操作但不能執行發送操作。
在函數傳參及任何賦值操作中,可以將雙向通道轉換爲單向通道,但反過來是不可以的。
package main

import "fmt"

func counter(out chan <- int)  {
    for i:=0;i<101;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)
}

結果:
0
1
4
9
16
25
...
9604
9801
10000

Process finished with exit code 0

1.4.8通道總結

19.Go語言基礎之併發

1.5worker pool(go routine池)

在工作中,我們通常會使用可以指定啓動的goroutine數量-worker pool 模式,控制go routine的數量,防止go routine泄露和暴漲。
一個簡單的work pool 示例代碼如下:
package main

import (
    "fmt"
    "time"
)

func worker(id int,jobs <- chan int,results chan <- int )  {
    for j:= range jobs{
        fmt.Printf("worker:%d start job:%d\n", id, j)
        time.Sleep(time.Second)
        fmt.Printf("worker:%d end job:%d\n", id, j)
        results <- j * 2
    }
}
func main() {
    jobs := make(chan int,100)
    results := make(chan int,100)

    //開啓3個goroutine
    for w:=1;w<=3;w++{
        go worker(w,jobs,results)
    }
    //5個任務
    for j:=1;j<=5;j++{
        jobs <- j
    }
    close(jobs)
    // 輸出結果
    for a := 1; a <= 5; a++ {
        <-results
    }
}

結果:
worker:1 start job:2
worker:3 start job:1
worker:2 start job:3
worker:1 end job:2
worker:3 end job:1
worker:3 start job:4
worker:2 end job:3
worker:1 start job:5
worker:1 end job:5
worker:3 end job:4

Process finished with exit code 0

1.6select多路複用

在某些場景下,我們需要同時從多個通道接收數據。通道在接收數據時,如果沒有數據可以接收將會發生阻塞。
//可以使用遍歷方式,實現同時從多個通道中獲取數據
package main

import (
    "fmt"
)

var ch1 chan int
var ch2 chan int

func main() {
    ch1 = make(chan int, 100)
    ch2 = make(chan int, 100)

    go func() {
        ch1 <- 10
        close(ch1)
    }()
    go func() {
        ch2 <- 11
        close(ch2)
    }()

    for{
        //從ch1接收值
        c1,ok := <- ch1
        if !ok{
            fmt.Println("ch1數據取完了")

        }
        if c1!=0{
            fmt.Println(c1)
        }

        //從ch2接收值
        c2,ok := <- ch2
        if !ok{
            fmt.Println("ch2數據取完了")
            break
        }
        fmt.Println(c2)
    }
    fmt.Println("操作完成!")
}

結果:
10
11
ch1數據取完了
ch2數據取完了
操作完成!

Process finished with exit code 0
//可以使用goroutine實現同時從多個通道中接收數據
package main

import (
    "fmt"
    "sync"
)
var wg sync.WaitGroup
var ch1 chan int
var ch2 chan int

func getFromCh1()  {
    defer wg.Done()
    c1 := <- ch1
    fmt.Println(c1)
}
func getFromCh2()  {
    defer wg.Done()
    c2 := <- ch2
    fmt.Println(c2)
}
func main() {
    ch1 = make(chan int, 100)
    ch2 = make(chan int, 100)
    wg.Add(2)
    go getFromCh1()
    go getFromCh2()
    go func() {
        ch1 <- 10
    }()
    go func() {
        ch2 <- 11
    }()

    wg.Wait()
    fmt.Println("操作完成!")
}

結果:
11
10
操作完成!

Process finished with exit code 0
使用select關鍵字實現多個通道接收值的需求。
select的使用類似於switch語句,他有一系列case分支和一個默認分支。每個case會對應一個通道的通信(接收或發送)過程。select會一直等待,直到某個case的通信操作完成時,就會執行case對應的語句。
格式如下:
select{
    case <-ch1:
        ...
    case data := <-ch2:
        ...
    case ch3<-data:
        ...
    default:
        默認操作
}

select語句能提高代碼的可讀性。
1.可處理一個或多個channel的發送/接收操作。
2.如果多個case同時滿足,select會隨機選擇一個。
3.對於沒有case的select,會一直等待,可用於阻塞main函數。
package main

import "fmt"

func main() {
    ch1 := make(chan int,1)
    ch2 := make(chan int,1)
    for i:=0;i<10;i++{
        select {
        case x1 := <- ch1:
            fmt.Printf("循環第%d次,ch1取出%d:\n",i,x1)
        case ch1 <- i:
            fmt.Printf("循環第%d次,ch1存入:%d\n",i,i)
        case x2 := <- ch2:
            fmt.Printf("循環第%d次,ch2取出%d:\n",i,x2)
        case ch2 <- i:
            fmt.Printf("循環第%d次,ch2存入:%d\n",i,i)
        }
    }

}

結果:
循環第0次,ch1存入:0
循環第1次,ch1取出0:
循環第2次,ch1存入:2
循環第3次,ch1取出2:
循環第4次,ch1存入:4
循環第5次,ch1取出4:
循環第6次,ch1存入:6
循環第7次,ch2存入:7
循環第8次,ch2取出7:
循環第9次,ch1取出6:

Process finished with exit code 0

//上面的結果完美的體現出了 多個case同時滿足時,select會隨機選擇一個執行。

1.7併發安全和鎖

有時候在Go代碼中會存在多個goroutine同時操作一個資源(臨界區),這種情況會發生竟態問題(數據竟態)。
package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup
var x int64
func add()  {
    for i:=0;i<5000;i++{
        x=x+1
    }
    wg.Done()
}
func main() {
    wg.Add(2)
    go add()
    go add()
    wg.Wait()
    fmt.Println(x)

}

結果1:
7281

Process finished with exit code 0

結果2:
10000

Process finished with exit code 0

//上面的代碼中,我們開啓了兩個goroutine去累加變量x的值,這兩個goroutine在訪問和修改x變量的時候就會存在數據競爭,導致最後結果與期待的不符。

1.8互斥鎖

互斥鎖是一種常用的控制共享資源訪問的方法,它能夠保證同時只有一個goroutine可以訪問共享資源。
Go語言中使用sync包的Mutex類型來實現互斥鎖。使用互斥鎖來修復上面代碼的問題:
package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup
var lock sync.Mutex
var x int64
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

Process finished with exit code 0

使用互斥鎖能夠保證同一時間有且只有一個goroutine進入臨界區,其他的goroutine則在等待鎖;
當互斥鎖釋放後,等待的goroutine才能獲取鎖進入臨界區,多個goroutine同時等待一個鎖時,喚醒策略是隨機的。

1.9讀寫互斥鎖

互斥鎖是完全互斥的,但是有很多實際的場景下是讀多寫少的,當我們併發的去讀取一個資源不涉及資源修改的時候是沒有必要加鎖的。
這種場景下使用讀寫鎖時更好的一種選擇。

讀寫鎖分爲兩種:讀鎖和寫鎖。
當一個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))
}

互斥鎖的時間:
1.404974744s

Process finished with exit code 0

讀寫互斥鎖的時間:
109.371376ms

Process finished with exit code 0

1.10sync.WaitGroup

在代碼中生硬的使用time.Sleep是不合適的,Go語言中可以使用sync.WaitGoup來實現併發任務的同步。
sync.WaitGroup有以下幾個方法:

19.Go語言基礎之併發

sync.WaitGroup內部維護着一個計數器,計數器的值可以增加和減少。
例如
我們啓動了N個併發任務時,就使用Add(N)將計數器值增加N。
每個任務完成時,調用Done(),會將計數器減1。
調用Wait()來等待併發任務執行完。
當計數器值爲0時,表示所有併發任務已經完成。

sync.WaitGroup是一個結構體,傳遞的時候要傳遞指針。
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()
}

1.11sync.Once

在編程的很多場景下我們需要確保某些操作在高併發的場景下只執行一次,例如:只加載一次配置文件、只關閉一次通道等。
Go語言中的sync包提供了一個針對只執行一次場景的解決方案-sync.Once。
sync.Onece只有一個Do方法,
func (o *Once) Do(f func()) {}

如果要執行的函數f需要傳遞參數,需要搭配閉包來使用。

1.11.1加載配置文件示例

延遲一個開銷很大的初始化操作到真正用到它的時候在執行是一個很好地實踐。
因爲預先初始化一個變量(比如在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,也不意味着變量初始化完成了。
考慮到這種情況,我們能想到的辦法一:可以添加互斥鎖;方法二:使用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]
}

1.11.2併發安全的單例模式


package singleton

import (
    "sync"
)

type singleton struct {}

var instance *singleton
var once sync.Once

func GetInstance() *singleton {
    once.Do(func() {
        instance = &singleton{}
    })
    return instance
}
sync.Once其實內部包含一個互斥鎖和一個布爾值,互斥鎖保證布爾值和數據的安全,而布爾值用來記錄初始化是否完成。
這樣設計就能保證初始化操作的時候是併發安全的,並且初始化操作也不會被執行多次。

1.12sync.Map

Go語言中內置的map不是併發安全的。

package main

import (
    "fmt"
    "strconv"
    "sync"
)

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(i)
            set(key,i)
            fmt.Printf("k=:%v,v:=%v\n", key, get(key))
            wg.Done()
        }(i)
    }
    wg.Wait()
}

結果:
fatal error: concurrent map writes

goroutine 6 [running]:

        /usr/local/go/src/runtime/panic.go:617 +0x72 fp=0xc0000326b8 sp=0xc000032688 pc=0x1028282
runtime.mapassign_faststr(0x10aca40, 0xc000060180, 0x10cd3a2, 0x1, 0x0)
        /usr/local/go/src/runtime/map_faststr.go:211 +0x42a fp=0xc000032720 sp=0xc0000326b8 pc=0x101031a
main.set(...)
        /Users/tongchao/Desktop/gopath/src/test/test.go:15
main.main.func1(0xc000014080, 0xc000014070, 0x2)
        /Users/tongchao/Desktop/gopath/src/test/test.go:23 +0x8e fp=0xc0000327c8 sp=0xc000032720 pc=0x1094fee
runtime.goexit()
        /usr/local/go/src/runtime/asm_amd64.s:1337 +0x1 fp=0xc0000327d0 sp=0xc0000327c8 pc=0x1051451
created by main.main
        /Users/tongchao/Desktop/gopath/src/test/test.go:21 +0xa2

goroutine 1 [runnable]:
sync.(*WaitGroup).Add(0xc000014070, 0x1)
        /usr/local/go/src/sync/waitgroup.go:53 +0x13c
main.main()
        /Users/tongchao/Desktop/gopath/src/test/test.go:20 +0x6e

goroutine 4 [runnable]:
main.get(...)
        /Users/tongchao/Desktop/gopath/src/test/test.go:12
main.main.func1(0xc000014080, 0xc000014070, 0x0)
        /Users/tongchao/Desktop/gopath/src/test/test.go:24 +0xcc
created by main.main
        /Users/tongchao/Desktop/gopath/src/test/test.go:21 +0xa2

goroutine 5 [runnable]:
main.main.func1(0xc000014080, 0xc000014070, 0x1)
        /Users/tongchao/Desktop/gopath/src/test/test.go:21
created by main.main
        /Users/tongchao/Desktop/gopath/src/test/test.go:21 +0xa2

goroutine 7 [runnable]:
main.main.func1(0xc000014080, 0xc000014070, 0x3)
        /Users/tongchao/Desktop/gopath/src/test/test.go:21
created by main.main
        /Users/tongchao/Desktop/gopath/src/test/test.go:21 +0xa2

goroutine 8 [runnable]:
main.main.func1(0xc000014080, 0xc000014070, 0x4)
        /Users/tongchao/Desktop/gopath/src/test/test.go:21
created by main.main
        /Users/tongchao/Desktop/gopath/src/test/test.go:21 +0xa2

goroutine 9 [runnable]:
main.main.func1(0xc000014080, 0xc000014070, 0x5)
        /Users/tongchao/Desktop/gopath/src/test/test.go:21
created by main.main
        /Users/tongchao/Desktop/gopath/src/test/test.go:21 +0xa2

goroutine 10 [runnable]:
main.main.func1(0xc000014080, 0xc000014070, 0x6)
        /Users/tongchao/Desktop/gopath/src/test/test.go:21
created by main.main
        /Users/tongchao/Desktop/gopath/src/test/test.go:21 +0xa2

goroutine 11 [runnable]:
main.main.func1(0xc000014080, 0xc000014070, 0x7)
        /Users/tongchao/Desktop/gopath/src/test/test.go:21
created by main.main
        /Users/tongchao/Desktop/gopath/src/test/test.go:21 +0xa2

goroutine 12 [runnable]:
main.main.func1(0xc000014080, 0xc000014070, 0x8)
        /Users/tongchao/Desktop/gopath/src/test/test.go:21
created by main.main
        /Users/tongchao/Desktop/gopath/src/test/test.go:21 +0xa2

goroutine 13 [runnable]:
main.main.func1(0xc000014080, 0xc000014070, 0x9)
        /Users/tongchao/Desktop/gopath/src/test/test.go:21
created by main.main
        /Users/tongchao/Desktop/gopath/src/test/test.go:21 +0xa2

goroutine 14 [runnable]:
main.main.func1(0xc000014080, 0xc000014070, 0xa)
        /Users/tongchao/Desktop/gopath/src/test/test.go:21
created by main.main
        /Users/tongchao/Desktop/gopath/src/test/test.go:21 +0xa2

goroutine 15 [runnable]:
main.main.func1(0xc000014080, 0xc000014070, 0xb)
        /Users/tongchao/Desktop/gopath/src/test/test.go:21
created by main.main
        /Users/tongchao/Desktop/gopath/src/test/test.go:21 +0xa2

goroutine 16 [runnable]:
main.main.func1(0xc000014080, 0xc000014070, 0xc)
        /Users/tongchao/Desktop/gopath/src/test/test.go:21
created by main.main
        /Users/tongchao/Desktop/gopath/src/test/test.go:21 +0xa2

Process finished with exit code 2
Go語言的sync包中提供了一開箱即用的併發安全的map-sync.Map。
開箱即用表示不用像內置的map一樣使用make函數初始化就能直接使用。
同時sync.Map內置了諸如Store、Load、LoadOrStore、Delete、Range等操作方法。
package main

import (
    "fmt"
    "strconv"
    "sync"
)
var m = sync.Map{}

func main() {
    wg := sync.WaitGroup{}
    for i:=0;i<20;i++{
        wg.Add(1)
        go func() {
            key := strconv.Itoa(i)
            m.Store(key,i)
            value,_ := m.Load(key)
            fmt.Printf("k=:%v,v:=%v\n", key, value)
            wg.Done()
        }()
    }
    wg.Wait()
}

結果:
k=:8,v:=8
k=:20,v:=20
k=:20,v:=20
k=:20,v:=20
k=:8,v:=20
k=:20,v:=20
k=:20,v:=20
k=:20,v:=20
k=:20,v:=20
k=:20,v:=20
k=:20,v:=20
k=:20,v:=20
k=:20,v:=20
k=:8,v:=8
k=:20,v:=20
k=:20,v:=20
k=:20,v:=20
k=:20,v:=20
k=:20,v:=20
k=:20,v:=20

Process finished with exit code 0

1.13原子操作

代碼中的加鎖操作因爲涉及內核態的上下文切換會比較耗時、代價比較高。
針對"基本數據類型",我們可以使用原子操作來保證併發安全,因爲原子操作是Go 語言提供的方法,在用戶態就可以完成,因此性能比加鎖操作更好。
Go語言中原子操作由內置的標準庫sync/atomic提供。
atomic包提供了底層的原子級內存操作,對於同步算法的實現很有用。這些函數必須謹慎地保證正確使用。除了某些特殊的底層應用,使用通道或者sync包的函數/類型實現同步更好。

19.Go語言基礎之併發
19.Go語言基礎之併發

一個示例來比較下互斥鎖和原子操作的性能。

package main

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

type Counter interface {
    Inc()
    Load() int64
}

// 普通版
type CommonCounter struct {
    counter int64
}

func (c CommonCounter) Inc() {
    c.counter++
}

func (c CommonCounter) Load() int64 {
    return c.counter
}

// 互斥鎖版
type MutexCounter struct {
    counter int64
    lock    sync.Mutex
}

func (m *MutexCounter) Inc() {
    m.lock.Lock()
    defer m.lock.Unlock()
    m.counter++
}

func (m *MutexCounter) Load() int64 {
    m.lock.Lock()
    defer m.lock.Unlock()
    return m.counter
}

// 原子操作版
type AtomicCounter struct {
    counter int64
}

func (a *AtomicCounter) Inc() {
    atomic.AddInt64(&a.counter, 1)
}

func (a *AtomicCounter) Load() int64 {
    return atomic.LoadInt64(&a.counter)
}

func test(c Counter) {
    var wg sync.WaitGroup
    start := time.Now()
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            c.Inc()
            wg.Done()
        }()
    }
    wg.Wait()
    end := time.Now()
    fmt.Println(c.Load(), end.Sub(start))
}

func main() {
    c1 := CommonCounter{} // 非併發安全
    test(c1)
    c2 := MutexCounter{} // 使用互斥鎖實現併發安全
    test(&c2)
    c3 := AtomicCounter{} // 併發安全且比互斥鎖效率更高
    test(&c3)
}

結果:
0 1.099595ms
1000 907.118µs
1000 456.326µs

Process finished with exit code 0
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章