溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Golang中的并發(fā)性是什么

發(fā)布時(shí)間:2023-03-15 14:07:40 來(lái)源:億速云 閱讀:99 作者:iii 欄目:開發(fā)技術(shù)

這篇文章主要介紹了Golang中的并發(fā)性是什么的相關(guān)知識(shí),內(nèi)容詳細(xì)易懂,操作簡(jiǎn)單快捷,具有一定借鑒價(jià)值,相信大家閱讀完這篇Golang中的并發(fā)性是什么文章都會(huì)有所收獲,下面我們一起來(lái)看看吧。

    什么是并發(fā)性,為什么它很重要

    并發(fā)是指在同一時(shí)間運(yùn)行多個(gè)事物的能力。你的電腦有一個(gè)CPU。一個(gè)CPU有幾個(gè)線程。每個(gè)線程通常一次運(yùn)行一個(gè)程序。當(dāng)我們通常寫代碼時(shí),這些代碼是按順序運(yùn)行的,也就是說(shuō),每項(xiàng)工作都是背對(duì)背運(yùn)行的。在并發(fā)代碼中,這些工作是由線程同時(shí)運(yùn)行的。

    一個(gè)很好的比喻是對(duì)一個(gè)家庭廚師的比喻。我還記得我第一次嘗試煮意大利面的時(shí)候。我按照菜譜一步步地做。我切了蔬菜,做了醬汁,然后煮了意大利面條,再把兩者混合起來(lái)。在這里,每一步都是按順序進(jìn)行的,所以下一項(xiàng)工作必須等到當(dāng)前工作完成后才能進(jìn)行。

    快進(jìn)到現(xiàn)在,我在烹飪意大利面條方面變得更有經(jīng)驗(yàn)。我現(xiàn)在先開始做意大利面,然后在這期間進(jìn)行醬汁的制作。烹飪時(shí)間幾乎減少到一半,因?yàn)榕腼円獯罄鏃l和醬汁是同時(shí)進(jìn)行的。

    并發(fā)性與平行性

    并發(fā)性與并行性有些不同。并行性與并發(fā)性類似,即同時(shí)發(fā)生多項(xiàng)工作。然而,在并行性中,多個(gè)線程分別在進(jìn)行不同的工作,而在并發(fā)性中,一個(gè)線程在不同的工作之間游走。

    因此,并發(fā)性和并行性是兩個(gè)不同的概念。一個(gè)程序既可以并發(fā)地運(yùn)行,也可以并行地運(yùn)行。你的代碼可以按順序?qū)?,也可以按并發(fā)寫。該代碼可以在單核機(jī)器或多核機(jī)器上運(yùn)行。把并發(fā)性看作是你的代碼的一個(gè)特征,而把并行性看作是執(zhí)行的一個(gè)特征。

    Goroutines, the worker Mortys

    Go使編寫并發(fā)代碼變得非常簡(jiǎn)單。每個(gè)并發(fā)的工作都由一個(gè)goroutine來(lái)表示。你可以通過(guò)在函數(shù)調(diào)用前使用go關(guān)鍵字來(lái)啟動(dòng)一個(gè)goroutine??催^(guò)《瑞克和莫蒂》嗎?想象一下,你的主函數(shù)是一個(gè)Rick,他把任務(wù)委托給goroutine Mortys。

    讓我們從一個(gè)連續(xù)的代碼開始。

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func main() {
        simple()
    }
    
    func simple() {
        fmt.Println(time.Now(), "0")
        time.Sleep(time.Second)
    
        fmt.Println(time.Now(), "1")
        time.Sleep(time.Second)
    
        fmt.Println(time.Now(), "2")
        time.Sleep(time.Second)
    
        fmt.Println("done")
    }

    2022-08-14 16:22:46.782569233 +0900 KST m=+0.000033220 0
    2022-08-14 16:22:47.782728963 +0900 KST m=+1.000193014 1
    2022-08-14 16:22:48.782996361 +0900 KST m=+2.000460404 2
    done

    上面的代碼打印出當(dāng)前時(shí)間和一個(gè)字符串。每條打印語(yǔ)句的運(yùn)行時(shí)間為一秒??偟膩?lái)說(shuō),這段代碼大約需要三秒鐘的時(shí)間來(lái)完成。

    現(xiàn)在讓我們把它與一個(gè)并發(fā)的代碼進(jìn)行比較。

    func main() {
        simpleConc()
    }
    
    func simpleConc() {
        for i := 0; i < 3; i++ {
            go func(index int) {
                fmt.Println(time.Now(), index)
            }(i)
        }
    
        time.Sleep(time.Second)
        fmt.Println("done")
    }

    2022-08-14 16:25:14.379416226 +0900 KST m=+0.000049175 2
    2022-08-14 16:25:14.379446063 +0900 KST m=+0.000079012 0
    2022-08-14 16:25:14.379450313 +0900 KST m=+0.000083272 1
    done

    上面的代碼啟動(dòng)了三個(gè)goroutines,分別打印當(dāng)前時(shí)間和i。這段代碼花了大約一秒鐘完成。這比順序版本快了三倍左右。

    "等一下,"我聽(tīng)到你問(wèn)。"為什么要等整整一秒?難道我們不能刪除這一行以使程序盡可能快地運(yùn)行嗎?"好問(wèn)題!讓我們看看會(huì)發(fā)生什么。

    func main() {
        simpleConcFail()
    }
    
    func simpleConcFail() {
        for i := 0; i < 3; i++ {
            go func(index int) {
                fmt.Println(time.Now(), index)
            }(i)
        }
    
        fmt.Println("done")
    }

    done

    嗯......。程序確實(shí)在沒(méi)有任何慌亂的情況下退出了,但我們?nèi)鄙賮?lái)自goroutines的輸出。為什么它們被跳過(guò)?

    這是因?yàn)樵谀J(rèn)情況下,Go并不等待goroutine的完成。你知道m(xù)ain也是在goroutine里面運(yùn)行的嗎?主程序通過(guò)調(diào)用simpleConcFail來(lái)啟動(dòng)工作程序,但它在工作程序完成工作之前就退出了。

    讓我們回到烹飪的比喻上。想象一下,你有三個(gè)廚師,他們分別負(fù)責(zé)烹飪醬料、意大利面和肉丸。現(xiàn)在,想象一下,如果戈登-拉姆齊命令廚師們做一盤意大利面條和肉丸子。這三位廚師將努力工作,烹制醬汁、意大利面條和肉丸。但是,在廚師們還沒(méi)有完成的時(shí)候,戈登就按了鈴,命令服務(wù)員上菜。很明顯,食物還沒(méi)有準(zhǔn)備好,顧客只能得到一個(gè)空盤子。

    這就是為什么我們?cè)谕顺龉?jié)目前等待一秒鐘。我們并不總是確定每項(xiàng)工作都會(huì)在一秒鐘內(nèi)完成。有一個(gè)更好的方法來(lái)等待工作的完成,但我們首先需要學(xué)習(xí)另一個(gè)概念。

    總結(jié)一下,我們學(xué)到了這些東西:

    • 工作被委托給goroutines。

    • 使用并發(fā)性可以提高你的性能。

    • 主goroutine默認(rèn)不等待工作goroutine完成。

    • 我們需要一種方法來(lái)等待每個(gè)goroutine完成。

    Channels, the green portal

    goroutines之間是如何交流的?當(dāng)然是通過(guò)通道。通道的作用類似于門戶。你可以通過(guò)通道發(fā)送和接收數(shù)據(jù)。下面是你如何在Go中制作一個(gè)通道。

    ch := make(chan int)

    每個(gè)通道都是強(qiáng)類型的,并且只允許該類型的數(shù)據(jù)通過(guò)。讓我們看看我們?nèi)绾问褂眠@個(gè)。

    func main() {
        unbufferedCh()
    }
    
    func unbufferedCh() {
        ch := make(chan int)
    
        go func() {
            ch <- 1
        }()
    
        res := <-ch
        fmt.Println(res)
    }

    1

    很簡(jiǎn)單,對(duì)嗎?我們做了一個(gè)名為ch的通道。我們有一個(gè)goroutine,向ch發(fā)送1,我們接收該數(shù)據(jù)并將其保存到res。

    你問(wèn),為什么我們?cè)谶@里需要一個(gè)goroutine?因?yàn)椴贿@樣做會(huì)導(dǎo)致死鎖。

    func main() {
        unbufferedChFail()
    }
    
    func unbufferedChFail() {
        ch := make(chan int)
        ch <- 1
        res := <-ch
        fmt.Println(res)
    }

    fatal error: all goroutines are asleep - deadlock!

    我們碰到了一個(gè)新詞。什么是死鎖?死鎖就是你的程序被卡住了。為什么上面的代碼會(huì)卡在死鎖中?

    為了理解這一點(diǎn),我們需要知道通道的一個(gè)重要特性。我們創(chuàng)建了一個(gè)無(wú)緩沖的通道,這意味著在某一特定時(shí)間內(nèi)沒(méi)有任何東西可以被存儲(chǔ)在其中。這意味著發(fā)送方和接收方都必須同時(shí)準(zhǔn)備好,才能在通道上傳輸數(shù)據(jù)。

    在失敗的例子中,發(fā)送和接收的動(dòng)作依次發(fā)生。我們發(fā)送1到ch,但在那個(gè)時(shí)候沒(méi)有人接收數(shù)據(jù)。接收發(fā)生在稍后的一行,這意味著在接收行運(yùn)行之前,1不能被發(fā)送??杀氖牵?不能先被發(fā)送,因?yàn)閏h是沒(méi)有緩沖的,沒(méi)有空間來(lái)容納任何數(shù)據(jù)。

    在這個(gè)工作例子中,發(fā)送和接收的動(dòng)作同時(shí)發(fā)生。主函數(shù)啟動(dòng)了goroutine,并試圖從ch中接收,此時(shí)goroutine正在向ch發(fā)送1。

    另一種從通道接收而不發(fā)生死鎖的方法是先關(guān)閉通道。

    func main() {
        unbufferedCh()
    }
    
    func unbufferedCh() {
        ch3 := make(chan int)
        close(ch3)
        res2 := <-ch3
        fmt.Println(res2)
    }

    0

    關(guān)閉通道意味著不能再向它發(fā)送數(shù)據(jù)。我們?nèi)匀豢梢詮脑撏ǖ乐薪邮账?。?duì)于未緩沖的通道,從一個(gè)關(guān)閉的通道接收將返回一個(gè)通道類型的零值。

    總結(jié)一下,我們學(xué)到了這些東西:

    • 通道是goroutines之間相互交流的方式。

    • 你可以通過(guò)通道發(fā)送和接收數(shù)據(jù)。

    • 通道是強(qiáng)類型的。

    • 沒(méi)有緩沖的通道沒(méi)有空間來(lái)存儲(chǔ)數(shù)據(jù),所以發(fā)送和接收必須同時(shí)進(jìn)行。否則,你的代碼就會(huì)陷入死鎖。

    • 一個(gè)封閉的通道將不接受任何數(shù)據(jù)。

    • 從一個(gè)封閉的非緩沖通道接收數(shù)據(jù)將返回一個(gè)零值。

    如果通道能保持?jǐn)?shù)據(jù)一段時(shí)間,那不是很好嗎?這里就是緩沖通道發(fā)揮作用的地方。

    Buffered channels, the portal that is somehow cylindrical?

    緩沖通道是帶有緩沖器的通道。數(shù)據(jù)可以存儲(chǔ)在其中,所以發(fā)送和接收不需要同時(shí)進(jìn)行。

    func main() {
        bufferedCh()
    }
    
    func bufferedCh() {
        ch := make(chan int, 1)
        ch <- 1
        res := <-ch
        fmt.Println(res)
    }

    1

    在這里,1被儲(chǔ)存在ch里面,直到我們收到它。

    很明顯,我們不能向一個(gè)滿了緩沖區(qū)的通道發(fā)送更多的信息。你需要在緩沖區(qū)內(nèi)有空間才能發(fā)送更多。

    func main() {
        bufferedChFail()
    }
    
    func bufferedChFail() {
        ch := make(chan int, 1)
        ch <- 1
        ch <- 2
        res := <-ch
        fmt.Println(res)
    }

    fatal error: all goroutines are asleep - deadlock!

    你也不能從一個(gè)空的緩沖通道接收。

    func main() {
        bufferedChFail2()
    }
    
    func bufferedChFail2() {
        ch := make(chan int, 1)
        ch <- 1
        res := <-ch
        res2 := <-ch
        fmt.Println(res, res2)
    }

    fatal error: all goroutines are asleep - deadlock!

    如果一個(gè)通道已滿,發(fā)送操作將等待,直到有可用的空間。這在這段代碼中得到了證明。

    func main() {
        bufferedCh3()
    }
    
    func bufferedCh3() {
        ch := make(chan int, 1)
        ch <- 1
        go func() {
            ch <- 2
        }()
        res := <-ch
        fmt.Println(res)
    }

    1

    我們接收一次是為了取出1,這樣goroutine就可以發(fā)送2到通道。我們沒(méi)有從ch接收兩次,所以只接收1。

    我們也可以從封閉的緩沖通道接收。在這種情況下,我們可以在封閉的通道上設(shè)置范圍來(lái)迭代里面的剩余項(xiàng)目。

    func main() {
        bufferedChRange()
    }
    
    func bufferedChRange() {
        ch := make(chan int, 3)
        ch <- 1
        ch <- 2
        ch <- 3
        close(ch)
        for res := range ch {
            fmt.Println(res)
        }
        // you could also do this
        // fmt.Println(<-ch)
        // fmt.Println(<-ch)
        // fmt.Println(<-ch)
    }

    1
    2
    3

    在一個(gè)開放的通道上測(cè)距將永遠(yuǎn)不會(huì)停止。這意味著在某些時(shí)候,通道將是空的,測(cè)距循環(huán)將試圖從一個(gè)空的通道接收,從而導(dǎo)致死鎖。

    總結(jié)一下:

    • 緩沖通道是有空間容納項(xiàng)目的通道。

    • 發(fā)送和接收不一定要同時(shí)進(jìn)行,與非緩沖通道不同。

    • 向一個(gè)滿的通道發(fā)送和從一個(gè)空的通道接收將導(dǎo)致一個(gè)死鎖。

    • 你可以在一個(gè)封閉的通道上進(jìn)行迭代,以接收緩沖區(qū)內(nèi)的剩余值。

    等待戈多...我的意思是,goroutines來(lái)完成,使用通道

    通道可以用來(lái)同步goroutines。還記得我告訴過(guò)你,在通過(guò)無(wú)緩沖通道傳輸數(shù)據(jù)之前,發(fā)送方和接收方必須都準(zhǔn)備好了嗎?這意味著接收方將等待,直到發(fā)送方準(zhǔn)備好。我們可以說(shuō),接收是阻斷的,意思是接收方將阻斷其他代碼的運(yùn)行,直到它收到東西。讓我們用這個(gè)巧妙的技巧來(lái)同步我們的goroutines。

    func main() {
        basicSyncing()
    }
    
    func basicSyncing() {
        done := make(chan struct{})
    
        go func() {
            for i := 0; i < 5; i++ {
                fmt.Printf("%s worker %d start\n", fmt.Sprint(time.Now()), i)
                time.Sleep(time.Duration(rand.Intn(5)) * time.Second)
            }
            close(done)
        }()
    
        <-done
        fmt.Println("exiting...")
    }

    我們做了一個(gè)done通道,負(fù)責(zé)阻斷代碼,直到goroutine完成。done可以是任何類型,但struct{}經(jīng)常被用于這些類型的通道。它的目的不是為了傳輸結(jié)構(gòu),所以它的類型并不重要。

    一旦工作完成,worker goroutine 將關(guān)閉 done。此時(shí),我們可以從 done 中接收,它將是一個(gè)空結(jié)構(gòu)。接收動(dòng)作解除了代碼的阻塞,使其可以退出。

    這就是我們使用通道等待goroutine完成的方式。

    關(guān)于“Golang中的并發(fā)性是什么”這篇文章的內(nèi)容就介紹到這里,感謝各位的閱讀!相信大家對(duì)“Golang中的并發(fā)性是什么”知識(shí)都有一定的了解,大家如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道。

    向AI問(wèn)一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI