天天看點

Go語言之goroutine基礎

一、goroutine基礎介紹

goroutine是Golang中的協程,它是一種微線程,比起線程它耗費更少的資源。線程的作用就是可以進行并發或者并行,完全利用電腦多核的資源。

  • 并發 多個任務跑在一個cpu上,在某一時刻隻處理一個任務,任務之間來回切換的時間極短
  • 并行 多個任務跑在多個cpu上,在某一時刻多個cpu處理多個任務,達到并行的效果

那麼在Golang中是如何來使用goroutine的呢?Golang的主線程中可以啟動多個協程。比如現在有一個案例:

  • 主線程啟動一個協程來每隔1秒輸出"Hello goroutine",輸出10次
  • 主線程每隔1秒輸出“Hello main”,輸出10次
  • 主線程與協程同時工作
package main

import (
    "fmt"
    "time"
)

func printGoroutine() {
    // 協程執行函數
    for i := 0; i < 5; i++ {
        time.Sleep(time.Second)
        fmt.Printf("Hello goroutine %v \n", i)
    }

}

func main() {
    // 主線程
    // 開啟一個協程
    go printGoroutine()

    // 繼續執行主協程代碼
    for i := 0; i < 5; i++ {
        time.Sleep(time.Second)
        fmt.Printf("Hello main %v \n", i)
    }

}

/*
輸出:
Hello main 0
Hello goroutine 0
Hello goroutine 1
Hello main 1
Hello main 2
Hello goroutine 2
Hello goroutine 3
Hello main 3
Hello main 4
*/      

可以看到主線程和協程同時工作,流程圖如下:

Go語言之goroutine基礎
  • 一旦通過go指令相當于開啟一個協程,和主線程是同步進行的,相當于主線程中開啟另一個分支
  • 如果主線程結束,其它協程即使還沒有執行完畢,也會結束
  • 協程也可以在主線程結束之前先完成自己的任務
  • 線程的開啟是直接作用與實體級别的cpu,非常耗費資源,而協程可輕松駕馭百萬級别

二、同步

(一)WaitGroup

上述的執行個體中出現一個明顯的問題,就是printGoroutine應該列印5次,但是顯然少了1次,這時為什麼呢?其實在圖中很明顯的看出主協程中運作的main結束,那麼其餘的協程都會終止掉,是以如何在所有的協程結束後再終止主協程就是很重要的事情。

sync.WaitGroup可以解決這個問題,聲明:

var wg sync.WaitGroup      

之後就可以正常使用wg變量了,該類型有3個指針方法,Add、Done、Wait

sync.WaitGroup是一個結構體類型,内部有一個計數字段,當sync.WaitGroup類型的變量被聲明後,該字段的值時0,可以通過Add方法增大或者減少其中的計數值。而該值就是啟動的協程數量。

Wait方法用于阻塞調用它的goroutine,直到計數值為0才被喚醒。

package main

import (
    "fmt"
    "sync"
    "time"
)

var wg sync.WaitGroup

func printGoroutine() {
    // 協程執行函數
    defer wg.Done() // 每執行完1次該協程,Add中的參數減1
    for i := 0; i < 5; i++ {
        time.Sleep(time.Second)
        fmt.Printf("Hello goroutine %v \n", i)
    }

}

func main() {
    // 主協程中運作main函數
    // 開啟一個協程
    wg.Add(1) // Add中的參數是開啟的協程數量
    go printGoroutine()

    // 繼續執行主協程代碼
    for i := 0; i < 5; i++ {
        time.Sleep(time.Second)
        fmt.Printf("Hello main %v \n", i)
    }
    wg.Wait() // 阻塞,直到Add中的參數變為0結束該主協程

}

/*
輸出:
Hello main 0
Hello goroutine 0
Hello goroutine 1
Hello main 1
Hello main 2
Hello goroutine 2
Hello goroutine 3
Hello main 3
Hello main 4
Hello goroutine 4
*/      

可以看到輸出結果,多輸出1行,主協程會等待所有的協程執行完畢才結束。

(二)鎖機制

1、問題引入

進行多協程并發操作時,就會涉及到資料安全問題,比如:

package main

import (
    "fmt"
    "sync"
)

var num int
var wg sync.WaitGroup

func sub() {
    defer wg.Done() 
    for i := 0; i < 100000; i++ {
        num -= 1
    }
}

func add() {
    defer wg.Done()
    for i := 0; i < 100000; i++ {
        num += 1
    }
}

func main() {
    wg.Add(2)

    // 啟動兩個協程,分别是加1和減1兩個函數
    go add()
    go sub()
    // 此時等待兩個協程執行完畢
    wg.Wait()
    // 接着執行主協程的邏輯
    fmt.Println(num)

}      

上面的輸出結果按理說應該是0,因為一個函數增加,一個函數減少,但是結果且完全不是預期的結果,每次執行的結果都不一樣:

PS D:\go_practice\go_tutorial\day19\MutexDemo01> go run .\main.go
-36503
PS D:\go_practice\go_tutorial\day19\MutexDemo01> go run .\main.go
-60812
PS D:\go_practice\go_tutorial\day19\MutexDemo01> go run .\main.go
-47786
PS D:\go_practice\go_tutorial\day19\MutexDemo01> go run .\main.go
89516
PS D:\go_practice\go_tutorial\day19\MutexDemo01> go run .\main.go
-97086      

那麼這時為什麼呢?這就是因為多協程産生資源競争的問題:

Go語言之goroutine基礎

  因為兩個對num的操作協程是同時進行的,是以兩個協程可能拿到的數字都是0,這時add函數對其進行加法操作,假如加到6000,而減法也在不斷的對num操作,擷取的num可能就是6000;相反add的num也有可能擷取的正是sub函數的值就是負數。是以num的值是不可掌控的。 那麼如何解決這個問題呢?

在Go中可以使用鎖,鎖分為:

  • 互斥鎖
  • 讀寫鎖

2、互斥鎖

互斥鎖在并發程式中對共享資源進行通路控制,是成對出現的,即Lock和Unlock。隻有當一個協程擷取到鎖時它才能執行後面的代碼塊。

package main

import (
    "fmt"
    "sync"
)

var num int
var wg sync.WaitGroup
var lock sync.Mutex // 聲明一個互斥鎖變量

func sub() {
    defer wg.Done()
    for i := 0; i < 100000; i++ {
        lock.Lock() // 加鎖,目前隻有該協程執行
        num -= 1
        lock.Unlock() // 解鎖
    }
}

func add() {
    defer wg.Done()
    for i := 0; i < 100000; i++ {
        lock.Lock() // 讀資料時加鎖,因為此時可能還會出現資源競争
        num += 1
        lock.Unlock()
    }
}

func main() {
    wg.Add(2)

    // 啟動兩個協程,分别是加1和減1兩個函數
    go add()
    go sub()
    // 此時等待兩個協程執行完畢
    wg.Wait()
    // 接着執行主協程的邏輯
    fmt.Println(num)

}      

在之前的代碼中假如互斥鎖即可解決問題,那麼為什麼在讀的地方也要加上互斥鎖呢?因為還有可能出現資源競争。互斥鎖的文法:

...
var lock sync.Mutex // 聲明一個互斥鎖變量

func main() {
  ...
  lock.Lock() // 加鎖,目前隻有該協程執行
  ...
  代碼塊
  ...
  lock.Unlock() // 解鎖
}      

互斥鎖針對于上述都是寫操作,可以很安全的解決這個問題,但是如果是讀寫混合的話,它的力度比較大,容易影響程式的性能。對于讀操作并不會對資料産生更改的影響,是以完全可以多人同時并發的讀,而上述的互斥鎖同一時刻隻能一個協程去讀或者去寫。是以這種問題可以使用讀寫鎖來解決。那麼下面使用一個例子來示範讀寫鎖,一邊讀資料,一邊寫資料。

2、讀寫鎖

  讀寫鎖是針對于讀寫操作的互斥鎖,它與普通互斥鎖的不同在于它可以分别針對讀操作于寫操作進行加鎖于解鎖。它允許任意個讀操作同時進行,但是在同一時刻它隻允許有一個寫操作在進行,并且在寫操作被進行的過程中,讀操作也是不被允許的。也就是說.讀寫鎖中多個寫操作之間是互斥的,并且寫操作與讀操作之間也是互斥的-但是.多個讀操作之間卻不存在互斥關系。這樣可以大大改善程式的性能。

使用文法:

var rwlock sync.RWMutex // 聲明一個讀寫鎖變量

func read() {

  rwlock.RLock()
  ....
  ...
  rwlock.RUnlock()  

}

func write() {

  rwlock.Lock()
  ....
  ...
  rwlock.Unlock()  

}      

 下面是讀寫鎖的一個執行個體:

package main

import (
    "fmt"
    "sync"
    "time"
)

var mapNum = make(map[int]int, 50)
var wg sync.WaitGroup
var rwlock sync.RWMutex // 聲明一個讀寫鎖變量

func write() {
    defer wg.Done()
    rwlock.Lock()
    fmt.Printf("寫入資料\n")
    time.Sleep(time.Second * 2)
    fmt.Printf("寫入結束\n")
    rwlock.Unlock()
}

func read() {
    defer wg.Done()
    rwlock.RLock()
    fmt.Printf("讀取資料\n")
    time.Sleep(time.Second)
    fmt.Printf("讀取結束\n")
    rwlock.RUnlock()
}

func main() {
    // 啟動10個讀,10個寫的協程
    wg.Add(22)

    for i := 0; i < 20; i++ {
        go write()
    }

    for i := 0; i < 2; i++ {
        go read()
    }

    // 此時等待20個協程執行完畢
    wg.Wait()

}

/*
寫入資料
寫入結束
讀取資料
讀取資料
讀取結束
讀取結束
寫入資料
寫入結束
寫入資料
寫入結束
寫入資料
寫入結束
寫入資料
*/      

從輸出中可以看到寫資料時先寫入再結束,而讀取資料可同時多個并發。

作者:iveBoy

出處:http://www.cnblogs.com/shenjianping/

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須在文章頁面給出原文連接配接,否則保留追究法律責任的權利。

go