golang併發編程goroutine+channel(一)

go語言的設計初衷除了在不影響程序性能的狀況下減小複雜度,另外一個目的是在當今互聯網大量運算下,如何讓程序的併發性能和代碼可讀性達到極致。go語言的併發關鍵詞 "go"

go dosomething() //走,兄弟咱們搞點事情

案例一:併發編程

func say(s string) {
    fmt.Printf("%s say\n", s)
}
func main() {
    go say("lisi")
    say("zhangsan")
}

執行結果html

zhangsan say

上面的案例執行了2次say方法,但只有zhangsan執行成功了。緣由是由於lisi是開了一個goroutine去執行,還沒執行完但此時的main函數已經退出了。程序員

案例二:併發編程

lisi估計是有點害羞,說話語速比較慢,所以咱們要等lisi一下,拋開串行執行和sleep外咱們用一個消息管道類通知,這裏咱們就要zhangsan和lisi一塊兒說golang

func say(s string, c chan int) {
    fmt.Printf("%s say\n", s)
    c <- 1 //在消息管道里傳1,表明我已經說過了
}

func main() {
    c := make(chan int)
    go say("lisi", c)
    go say("zhangsan", c)
    v1, v2 := <-c, <-c   
    fmt.Printf("lisi:%d , zhangsan:%d\n", v1, v2)
}

執行結果以下,固然也有可能lisi say 在zhangsan say的前面,等於1表明他倆都說過話了express

zhangsan say
lisi say
lisi:1 , zhangsan:1

過程分解
一、建立一個無緩衝的channel
二、異步執行 go say("lisi", c)
三、異步執行 go say("zhangsan", c)
四、假設zhangsan先執行,那麼zhangsan的1先放入管道c,若是這時候正好lisi在執行,很差意思管道c只有1個長度放不下了。此時lisi: c<- 1阻塞
五、v1 := <- c 執行,zhangsan的值1從管道里拿出來了。
六、lisi執行 c <- 1
七、v2 := <- c執行,lisi的值1也從管道里拿出來了
八、執行fmt.Printf編程

併發編程就這麼用的,不過你們發現問題沒有,過程分解步驟4有阻塞,同一時刻5和7也是阻塞的(等待管道里拿值遲遲拿不到)
適當改版一下,以下:c#

func say(s string, c chan int) {
    fmt.Printf("%s say\n", s)
    c <- 1 //在消息管道里傳1,表明我已經說過了
}

func main() {
    c := make(chan int, 2)  //改動點,管道長度設成了2
    go say("lisi", c)
    go say("zhangsan", c)
    v1, v2 := <-c, <-c
    fmt.Printf("lisi:%d , zhangsan:%d\n", v1, v2)
}

這時候的過程分解
一、建立一個緩衝爲2的channel
二、異步執行 go say("lisi", c)
三、異步執行 go say("zhangsan", c)
四、假設zhangsan先執行,那麼zhangsan的1先放入管道c,若是這時候正好lisi在執行,lisi的1也放入管道c
五、v1 := <- c 執行,zhangsan的值1從管道里拿出來了。
六、v2 := <- c執行,lisi的值1也從管道里拿出來了
七、執行fmt.Printf安全

理論上來講應該是少了一步,實際狀況可能會更好一些,由於步驟4沒有阻塞(也就是zhangsan和lisi的值1能夠同時放進去)。網絡

步驟5和6雖然有阻塞(這裏的阻塞跟c#裏的await是一個意思),可是管道c一旦有值會立馬拿出來,等v1和v2都有值了而後執行fmt.Printf併發

又有問題了!

  1. 若是say方法有返回值怎麼辦? 以下代碼案例說明
func say(s string) int {
    fmt.Printf("%s say\n", s)
    return 1
}
func main() {
    msg:= go say("lisi", c)  //PS:這裏會報錯syntax error: unexpected go, expecting expression
}
  1. 這個chan只能傳int或者string 若是個人返回只是一個struct結構體(實體)怎麼辦?
  2. 若是say方法是別人寫的,他的參數沒有chan管道我又想併發執行怎麼辦?

仍是看代碼吧異步

package main

import (
    "fmt"
)

//學生結構體(實體)
type Stu struct {
    Name string
    Age  int
}

func say(name string) Stu {
    fmt.Printf("%s say\n", name)
    stu := Stu{Name: name, Age: 18}
    return stu
}
func main() {
    c := make(chan int)
    go func() {
        stu := say("lisi") //返回一個學生實體
        fmt.Printf("我叫%s,年齡%d\n", stu.Name, stu.Age)
        c <- 1 //信號位表示調用完畢
    }()
    fmt.Println("go func")
    <-c
    fmt.Println("end")
}

執行結果:
go func
lisi say
我叫lisi,年齡18
end

錯誤示範:死鎖

func say(s string, c chan int) {
    fmt.Printf("%s say\n", s)
    //c <- 1 這裏原本應該給c管道傳值的,結果沒傳
}
func main() {
    c := make(chan int)
    go say("lisi", c)
    v1 := <-c //這裏會一直阻塞,致使死鎖
    fmt.Printf("lisi:%d\n", v1)  //前面死鎖,這裏沒法輸出
}

執行報錯內容:

fatal error: all goroutines are asleep - deadlock!

goroutine簡析:

goroutine也叫協程是一種輕量級別用戶空間線程,不受操做系統的調度,因此須要用戶自行調度(通常是加鎖和信道),協程能作的事情進程和線程一樣能作。進程和線程的切換主要依賴於時間片的控制,而協程的切換則主要依賴於自身,這樣的好處是避免了無心義的調度,由此能夠提升性能,但也所以,程序員必須本身承擔調度的責任

什麼是協程:from百科
協程與子例程同樣,協程(coroutine)也是一種程序組件。相對子例程而言,協程更爲通常和靈活,但在實踐中使用沒有子例程那樣普遍。協程源自 Simula 和 Modula-2 語言,但也有其餘語言支持

ps:子例程是某個主程序的一部分代碼

goroutine能夠看做是協程的go語言實現,它是語言原生支持的,相對於通常由庫實現協程的方式,goroutine更增強大,它的調度必定程度上是由go運行時(runtime)管理。其好處之一是,當某goroutine發生阻塞時(例如同步IO操做等),會自動出讓CPU給其它goroutine。

後面會單獨的在介紹進程、線程、協程以前的關係,也能夠參考如下幾篇文章

  1. 進程、線程、輕量級進程、協程和go中的Goroutine 那些事兒
  2. golang的goroutine是如何實現的?

channel 簡析

channel是Go語言在語言級別提供的goroutine間的通訊方式。咱們可使用channel在兩個或 多個goroutine之間傳遞消息。channel是進程內的通訊方式,所以經過channel傳遞對象的過程和調用函數時的參數傳遞行爲比較一致,好比也能夠傳遞指針等。若是須要跨進程通訊,咱們建議用 分佈式系統的方法來解決,好比使用Socket或者HTTP等通訊協議。Go語言對於網絡方面也有很是完善的支持。 channel是類型相關的。也就是說,一個channel只能傳遞一種類型的值,這個類型須要在聲明channel時指定。若是對Unix管道有所瞭解的話,就不難理解channel,能夠將其認爲是一種類 型安全的管道。

關於channel有必要詳細瞭解下。能夠參考
golang的channel使用

重點來了,goroutine號稱是輕鬆開上萬個併發

package main

import (
    "fmt"
    "time"
)

var sum int = 0

func todo(i int, c chan int) {
    //c <- 1 //執行一次放一個值1
    c <- i //把i的值放進去
}
func getSum(count int, c chan int, ce chan int) {
    for i := 0; i <= count; i++ {
        sum += <-c
        //      k, isopen := <-c
        //      if !isopen {
        //          fmt.Printf("channel is close")
        //          break
        //      } else {
        //          fmt.Printf("sum:%d,k:%d\n", sum, k)
        //          sum += k
        //      }
    }
    ce <- 1
}
func main() {
    count := 100000            //10W個goroutine
    c := make(chan int, count) //有緩衝channel
    ce := make(chan int)       //計算getSum信號量
    //開始計時
    begin := time.Now()
    fmt.Println("開始時間:", begin)
    for i := 0; i <= count; i++ {
        go todo(i, c)
    }
    //再開一個goroutine去計算channel裏的值求Sum
    go getSum(count, c, ce)
    <-ce   //這裏是getSum方法執行結束信號量
    end := time.Now()
    fmt.Println("結束時間:", end, time.Since(begin))
    fmt.Println(sum)

}

硬件信息
環境:THINKPAD L460、WIN7x6四、8G內存、i5-6200U 2.3GHz 雙核4線程
語言:LiteIDE X3三、golang 1.9.2

屢次執行結果:38.5ms - 51ms之間

再次改版下

把 c := make(chan int, count) 改成 c := make(chan int) 改爲無緩衝

c := make(chan int)    //重點,這裏改爲無緩衝的

屢次執行結果:304-325ms之間

結論:明顯無緩衝區耗時多了接近300ms,這部分時間實際是channel讀取阻塞的時間,所以在大量併發的狀況下channel的緩衝區大小會直接影響程序的性能,這也是前面提到須要用戶自行調度的緣由之一!!!

順便來一發.net core 的併發代碼實驗,和上面goroutine一樣的機器和環境

class Program
    {
        private static readonly object obj = new object();
        static void Main(string[] args)
        {
            DateTime begin = DateTime.Now;
            long sum = 0;
            Parallel.For(1, 100001, (i) =>
            {
                lock (obj)
                {
                    sum += i;
                }
            });
            TimeSpan ts = DateTime.Now - begin;
            Console.WriteLine($"{sum},耗時:{ts.TotalMilliseconds}ms");
            Console.ReadLine();
        }
    }

運行結果 : 大約在90-120ms左右,雖然數值上差了2倍左右,其實差異並非很大,也沒有直接的可比性,由於線程和協程並非一個數量級別,上面goroutine用到了channel通道,net core 用的lock鎖,所以僅供參考。整體看來.net core的性能總體仍是蠻高的
PS:題外話 其實c#裏也有協程"fiber",網上資料比較少了解很少。

相關文章
相關標籤/搜索