日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 综合教程 >内容正文

综合教程

golang 之并发

發(fā)布時(shí)間:2024/5/24 综合教程 47 生活家
生活随笔 收集整理的這篇文章主要介紹了 golang 之并发 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

在了解之前,要注意golang是并發(fā)語言而不是并行語言

并發(fā)和并行

并發(fā)是一次性做大量事情的能力(兩個(gè)或多個(gè)事件在同一時(shí)間間隔發(fā)生)
并行同一時(shí)間執(zhí)行多個(gè)任務(wù)的能力(兩個(gè)或者多個(gè)事件在同一時(shí)刻發(fā)生)

舉例說明:

  每天早上10分鐘我洗臉,刷牙,吃早飯等等很多事情,這就是并發(fā)。 我一邊刷牙的同時(shí)在燒水做飯這就是并行。

技術(shù)層面來說:假如一個(gè)web網(wǎng)頁中有視頻播放和文件下載兩個(gè)動作,當(dāng)瀏覽器在單核的處理器下運(yùn)行時(shí),CPU核心會在這兩個(gè)事件中來回切換,(同時(shí))播放視頻和下載,這就稱為并發(fā)。并發(fā)進(jìn)程在不同的時(shí)間點(diǎn)開始并有著重疊的執(zhí)行周期。假如你的CPU是多核處理器,那么下載和播放會在不同的CPU核心同時(shí)執(zhí)行,這就是并行。

goroutine

  在go中,每一個(gè)并發(fā)執(zhí)行的操作都稱為goroutine,當(dāng)一個(gè)程序啟動時(shí),只有一個(gè)goroutine來調(diào)用main函數(shù),稱它為主goroutine。新的goroutine通過go語法來創(chuàng)建。

f()  // 調(diào)用f(); 等待它返回
go f()  //新建一個(gè)調(diào)用f()的goroutine,不用等待。

 調(diào)度模型

groutine能擁有強(qiáng)大的并發(fā)實(shí)現(xiàn)是通過GPM調(diào)度模型實(shí)現(xiàn):

G:代表一個(gè)goroutine,它有自己的棧,instruction pointer和其他信息(正在等待的channel等等),用于調(diào)度。
M: 代表內(nèi)核級線程,一個(gè)M就是一個(gè)線程,goroutine就是跑在M之上的;M是一個(gè)很大的結(jié)構(gòu),里面維護(hù)小對象內(nèi)存cache(mcache)、當(dāng)前執(zhí)行的goroutine、隨機(jī)數(shù)發(fā)生器等等非常多的信息
P: 全程processor,處理器,它的主要作用來執(zhí)行g(shù)oroutine,所以它維護(hù)了一個(gè)goroutine隊(duì)列。里面存儲了所有需要它來執(zhí)行的goroutine。
Sched:代表調(diào)度器,它維護(hù)有存儲M和G的隊(duì)列以及調(diào)度器的一些狀態(tài)信息等。

調(diào)度實(shí)現(xiàn)

有2個(gè)物理線程M,每一個(gè)M都擁有一個(gè)處理器P,每一個(gè)也都有一個(gè)正在運(yùn)行的goroutine。
P的數(shù)量可以通過GOMAXPROCS()來設(shè)置,它其實(shí)也就代表了真正的并發(fā)度,即有多少個(gè)goroutine可以同時(shí)運(yùn)行。圖中灰色的那些goroutine并沒有運(yùn)行,而是出于ready的就緒態(tài),正在等待被調(diào)度。P維護(hù)著這個(gè)隊(duì)列(稱之為runqueue),
Go語言里,啟動一個(gè)goroutine很容易:go function 就行,所以每有一個(gè)go語句被執(zhí)行,runqueue隊(duì)列就在其末尾加入一個(gè)goroutine,在下一個(gè)調(diào)度點(diǎn),就從runqueue中取出(如何決定取哪個(gè)goroutine?)一個(gè)goroutine執(zhí)行。

如果一個(gè)線程阻塞會發(fā)生什么情況呢?如下圖

  從上圖中可以看出,一個(gè)線程放棄了它的上下文讓其他的線程可以運(yùn)行它。M1可能僅僅為了讓它處理圖中系統(tǒng)調(diào)用而被創(chuàng)建出來,或者它可能來自一個(gè)線程池。這個(gè)處于系統(tǒng)調(diào)用中的線程將會保持在這個(gè)導(dǎo)致系統(tǒng)調(diào)用的goroutine上,因?yàn)閺募夹g(shù)上來說,它仍然在執(zhí)行,雖然阻塞在OS里了。

  另一種情況是P所分配的任務(wù)G很快就執(zhí)行完了(分配不均),這就導(dǎo)致了這個(gè)處理器P很忙,但是其他的P還有任務(wù),此時(shí)如果global runqueue沒有任務(wù)G了,那么P不得不從其他的P里拿一些G來執(zhí)行。一般來說,如果P從其他的P那里要拿任務(wù)的話,一般就拿run queue的一半,這就確保了每個(gè)OS線程都能充分的使用,

使用goroutine

package main

import (
    "fmt"
    "time"
)

func cal(a int , b int )  {
    c := a+b
    fmt.Printf("%d + %d = %d
",a,b,c)
}

func main() {
  
    for i :=0 ; i<10 ;i++{
        go cal(i,i+1)  //啟動10個(gè)goroutine 來計(jì)算
    }
    time.Sleep(time.Second * 2) // sleep作用是為了等待所有任務(wù)完成
} 

GOMAXPROCS

設(shè)置goroutine運(yùn)行的CPU數(shù)量,最新版本的go已經(jīng)默認(rèn)已經(jīng)設(shè)置了。

num := runtime.NumCPU()    //獲取主機(jī)的邏輯CPU個(gè)數(shù)
runtime.GOMAXPROCS(num)    //設(shè)置可同時(shí)執(zhí)行的最大CPU數(shù)

 也可以根據(jù)個(gè)人手動設(shè)置,例如

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)

 上面GOMAXPROCS設(shè)置為1,當(dāng)遇到兩個(gè)go調(diào)度時(shí),就會發(fā)生等待。如果設(shè)置為2就會并行執(zhí)行(前提是你的cpu數(shù)量>=2),如下例子

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)
}

 在執(zhí)行上面代碼時(shí)細(xì)心的同學(xué)會發(fā)現(xiàn),每一步最后都會睡眠一秒,才能打印結(jié)果,這是因?yàn)椴l(fā)執(zhí)行,goroutine還沒來得及返回結(jié)果,主線程已經(jīng)執(zhí)行完了。

  那么如果不會面有沒有其他的方法?當(dāng)然有。第一種便是采用sync.WaitGroup來實(shí)現(xiàn)

var wg sync.WaitGroup

func hello(i int) {
	defer wg.Done() // goroutine結(jié)束就登記-1
	fmt.Println("Hello Goroutine!", i)
}
func main() {

	for i := 0; i < 10; i++ {
		wg.Add(1) // 啟動一個(gè)goroutine就登記+1
		go hello(i)
	}
	wg.Wait() // 等待所有登記的goroutine都結(jié)束
}

 詳細(xì)用法詳見sync包。另外一種便是channel

channel

channel是用來傳遞數(shù)據(jù)的一個(gè)數(shù)據(jù)結(jié)構(gòu),同map一樣使用內(nèi)置的make來創(chuàng)建。如

ch := make(chan int)  // 無緩沖通道
ch1 := make(chan int, 10) //緩沖為10的通道

channel類型

定義格式 

ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .

它表示三種類型的定義,可選的<-表示channel的方向。如果沒有指定,即表示雙向通道,既可以接收,也可以發(fā)送。

chan T          // 可以接收和發(fā)送類型為 T 的數(shù)據(jù)
chan<- float64  // 只可以用來發(fā)送 float64 類型的數(shù)據(jù)
<-chan int      // 只可以用來接收 int 類型的數(shù)據(jù)

 <-總是最優(yōu)先與最左邊類型結(jié)合。如

chan<- chan int    // 等價(jià) chan<- (chan int)
chan<- <-chan int  // 等價(jià) chan<- (<-chan int)
<-chan <-chan int  // 等價(jià) <-chan (<-chan int)
chan (<-chan int)

channel操作

常見三種操作,接收,發(fā)送和關(guān)閉

ch := make(chan int)

發(fā)送:ch <- 1 //將1發(fā)送到ch通道中
接收:x := <-ch // 從ch接收值并賦給x。也可以直接拋棄:<-ch
關(guān)閉:close(ch)

  close時(shí)可以通過i, ok := <-c可以查看Channel的狀態(tài),判斷值是零值還是正常讀取的值。

c := make(chan int, 10)
close(c)
i, ok := <-c
fmt.Printf("%d, %t", i, ok) //0, false

無緩沖通道

需要注意的是,無緩沖通道上的發(fā)送操作將會阻塞,值到另一個(gè)goroutine在對立的通道上執(zhí)行接收操作。這時(shí)值才算完成。相反,如果接收操作先執(zhí)行,接收方goroutine將阻塞,直到另一個(gè)goroutine在同一個(gè)通道發(fā)送一個(gè)值。

package main

import "fmt"

func sum(s []int, c chan int) {
        sum := 0
        for _, v := range s {
                sum += v
        }
        c <- sum // 把 sum 發(fā)送到通道 c
}

func main() {
        s := []int{7, 2, 8, -9, 4, 0}

        c := make(chan int)
        go sum(s[:len(s)/2], c)
        go sum(s[len(s)/2:], c)
        x, y := <-c, <-c // 從通道 c 中接收

        fmt.Println(x, y, x+y)
}

 打印結(jié)果

-5 17 12

單向通道

  當(dāng)程序演進(jìn)時(shí),將大的函數(shù)拆分為多個(gè)更小的是很自然的,在當(dāng)一個(gè)通道用作函數(shù)的行參時(shí),它幾乎總是被有意地限制不能發(fā)送或接收。為了將這種意圖可以比避免誤用,在go的類型系統(tǒng)提供了單向通道。僅僅導(dǎo)出發(fā)送或者接收操作。如類型chan <- int是一個(gè)只能發(fā)送的通道。反之 <- chan int是一個(gè)只能接收int類型通道。

func sum(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 sum(ch1)
	go squarer(ch2, ch1)
}

有緩沖通道  

  緩沖通道有一個(gè)元素隊(duì)列,隊(duì)列的最大長度在創(chuàng)建時(shí)通過make的容量參數(shù)來設(shè)置。

ch := make(chan string, 3)

  

  一個(gè)空的緩沖通道

緩沖通道上的發(fā)送操作在隊(duì)列的尾部插入一個(gè)元素,接收操作從隊(duì)列的頭部移除一個(gè)元素。如果通道滿了,發(fā)送操作將會阻塞所在的goroutine直到另一個(gè)goroutine對它進(jìn)行移除操作留出可用的空間。反過來,如果通道空了。執(zhí)行接收操作的goroutine阻塞,直到另一個(gè)goroutine在通道上發(fā)送數(shù)據(jù)。

func main() {
	ch := make(chan string, 3)
	ch <- "a"
        ch <- "b"
        ch <- "c"
	fmt.Println("發(fā)送成功")
        x := <-ch // 打印a
}

range 

func main() {
    go func() {
        time.Sleep(1 * time.Hour)
    }()
    c := make(chan int)
    go func() {
        for i := 0; i < 10; i = i + 1 {
            c <- i
        }
        close(c)
    }()
    for i := range c {
        fmt.Println(i)
    }
    fmt.Println("Finished")
}

 如上面的例子,range c 產(chǎn)生的迭代值為channel中發(fā)送的值,它會一直迭代直到channel關(guān)閉。如果此時(shí)close(c)關(guān)掉。程序會一直阻塞在for....range c 這一行。

select

  select語句選擇一組可能的send操作和receive操作去處理。它類似switch,但是只是用來處理通訊(communication)操作。它的case可以是send語句,也可以是receive語句,亦或者defaultreceive語句可以將值賦值給一個(gè)或者兩個(gè)變量。它必須是一個(gè)receive操作。最多允許有一個(gè)default case,它可以放在case列表的任何位置,盡管我們大部分會將它放在最后。

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}
func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

  如果有同時(shí)多個(gè)case去處理,比如同時(shí)有多個(gè)channel可以接收數(shù)據(jù),那么Go會偽隨機(jī)的選擇一個(gè)case處理(pseudo-random)。如果沒有case需要處理,則會選擇default去處理,如果default case存在的情況下。如果沒有default case,則select語句會阻塞,直到某個(gè)case需要處理。

  特別注意的是nil channel會一直被阻塞。如果沒有default: nil chanel會一直阻塞。

最后列出channel的幾種常用關(guān)系

小結(jié):

  在處理并發(fā)時(shí)會發(fā)生數(shù)據(jù)錯(cuò)亂的情況,這時(shí)候就會用到鎖機(jī)制,如上面一開始介紹sync包。鎖將會在sync包中描述。

總結(jié)

以上是生活随笔為你收集整理的golang 之并发的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。