Java并发编程实战~协程
Golang 是一門號(hào)稱從語言層面支持并發(fā)的編程語言,支持并發(fā)是 Golang 一個(gè)非常重要的特性。在上一篇文章《44 | 協(xié)程:更輕量級(jí)的線程》中我們介紹過,Golang 支持協(xié)程,協(xié)程可以類比 Java 中的線程,解決并發(fā)問題的難點(diǎn)就在于線程(協(xié)程)之間的協(xié)作。
那 Golang 是如何解決協(xié)作問題的呢?
總的來說,Golang 提供了兩種不同的方案:一種方案支持協(xié)程之間以共享內(nèi)存的方式通信,Golang 提供了管程和原子類來對(duì)協(xié)程進(jìn)行同步控制,這個(gè)方案與 Java 語言類似;另一種方案支持協(xié)程之間以消息傳遞(Message-Passing)的方式通信,本質(zhì)上是要避免共享,Golang 的這個(gè)方案是基于 CSP(Communicating Sequential Processes)模型實(shí)現(xiàn)的。Golang 比較推薦的方案是后者。
什么是 CSP 模型
我們?cè)凇?2 | Actor 模型:面向?qū)ο笤牟l(fā)模型》中介紹了 Actor 模型,Actor 模型中 Actor 之間就是不能共享內(nèi)存的,彼此之間通信只能依靠消息傳遞的方式。Golang 實(shí)現(xiàn)的 CSP 模型和 Actor 模型看上去非常相似,Golang 程序員中有句格言:“不要以共享內(nèi)存方式通信,要以通信方式共享內(nèi)存(Don’t communicate by sharing memory, share memory by communicating)。”雖然 Golang 中協(xié)程之間,也能夠以共享內(nèi)存的方式通信,但是并不推薦;而推薦的以通信的方式共享內(nèi)存,實(shí)際上指的就是協(xié)程之間以消息傳遞方式來通信。
下面我們先結(jié)合一個(gè)簡(jiǎn)單的示例,看看 Golang 中協(xié)程之間是如何以消息傳遞的方式實(shí)現(xiàn)通信的。我們示例的目標(biāo)是打印從 1 累加到 100 億的結(jié)果,如果使用單個(gè)協(xié)程來計(jì)算,大概需要 4 秒多的時(shí)間。單個(gè)協(xié)程,只能用到 CPU 中的一個(gè)核,為了提高計(jì)算性能,我們可以用多個(gè)協(xié)程來并行計(jì)算,這樣就能發(fā)揮多核的優(yōu)勢(shì)了。
在下面的示例代碼中,我們用了 4 個(gè)子協(xié)程來并行執(zhí)行,這 4 個(gè)子協(xié)程分別計(jì)算[1, 25 億]、(25 億, 50 億]、(50 億, 75 億]、(75 億, 100 億],最后再在主協(xié)程中匯總 4 個(gè)子協(xié)程的計(jì)算結(jié)果。主協(xié)程要匯總 4 個(gè)子協(xié)程的計(jì)算結(jié)果,勢(shì)必要和 4 個(gè)子協(xié)程之間通信,Golang 中協(xié)程之間通信推薦的是使用 channel,channel 你可以形象地理解為現(xiàn)實(shí)世界里的管道。另外,calc() 方法的返回值是一個(gè)只能接收數(shù)據(jù)的 channel ch,它創(chuàng)建的子協(xié)程會(huì)把計(jì)算結(jié)果發(fā)送到這個(gè) ch 中,而主協(xié)程也會(huì)將這個(gè)計(jì)算結(jié)果通過 ch 讀取出來。
import ("fmt""time" ) func main() {// 變量聲明var result, i uint64// 單個(gè)協(xié)程執(zhí)行累加操作start := time.Now()for i = 1; i <= 10000000000; i++ {result += i}// 統(tǒng)計(jì)計(jì)算耗時(shí)elapsed := time.Since(start)fmt.Printf("執(zhí)行消耗的時(shí)間為:", elapsed)fmt.Println(", result:", result)// 4個(gè)協(xié)程共同執(zhí)行累加操作start = time.Now()ch1 := calc(1, 2500000000)ch2 := calc(2500000001, 5000000000)ch3 := calc(5000000001, 7500000000)ch4 := calc(7500000001, 10000000000)// 匯總4個(gè)協(xié)程的累加結(jié)果result = <-ch1 + <-ch2 + <-ch3 + <-ch4// 統(tǒng)計(jì)計(jì)算耗時(shí)elapsed = time.Since(start)fmt.Printf("執(zhí)行消耗的時(shí)間為:", elapsed)fmt.Println(", result:", result) } // 在協(xié)程中異步執(zhí)行累加操作,累加結(jié)果通過channel傳遞 func calc(from uint64, to uint64) <-chan uint64 {// channel用于協(xié)程間的通信ch := make(chan uint64)// 在協(xié)程中執(zhí)行累加操作go func() {result := fromfor i := from + 1; i <= to; i++ {result += i}// 將結(jié)果寫入channelch <- result}()// 返回結(jié)果是用于通信的channelreturn ch }CSP 模型與生產(chǎn)者 - 消費(fèi)者模式
你可以簡(jiǎn)單地把 Golang 實(shí)現(xiàn)的 CSP 模型類比為生產(chǎn)者 - 消費(fèi)者模式,而 channel 可以類比為生產(chǎn)者 - 消費(fèi)者模式中的阻塞隊(duì)列。不過,需要注意的是 Golang 中 channel 的容量可以是 0,容量為 0 的 channel 在 Golang 中被稱為無緩沖的 channel,容量大于 0 的則被稱為有緩沖的 channel。
無緩沖的 channel 類似于 Java 中提供的 SynchronousQueue,主要用途是在兩個(gè)協(xié)程之間做數(shù)據(jù)交換。比如上面累加器的示例代碼中,calc() 方法內(nèi)部創(chuàng)建的 channel 就是無緩沖的 channel。
而創(chuàng)建一個(gè)有緩沖的 channel 也很簡(jiǎn)單,在下面的示例代碼中,我們創(chuàng)建了一個(gè)容量為 4 的 channel,同時(shí)創(chuàng)建了 4 個(gè)協(xié)程作為生產(chǎn)者、4 個(gè)協(xié)程作為消費(fèi)者。
Golang 中的 channel 是語言層面支持的,所以可以使用一個(gè)左向箭頭(<-)來完成向 channel 發(fā)送數(shù)據(jù)和讀取數(shù)據(jù)的任務(wù),使用上還是比較簡(jiǎn)單的。Golang 中的 channel 是支持雙向傳輸?shù)?#xff0c;所謂雙向傳輸,指的是一個(gè)協(xié)程既可以通過它發(fā)送數(shù)據(jù),也可以通過它接收數(shù)據(jù)。
不僅如此,Golang 中還可以將一個(gè)雙向的 channel 變成一個(gè)單向的 channel,在累加器的例子中,calc() 方法中創(chuàng)建了一個(gè)雙向 channel,但是返回的就是一個(gè)只能接收數(shù)據(jù)的單向 channel,所以主協(xié)程中只能通過它接收數(shù)據(jù),而不能通過它發(fā)送數(shù)據(jù),如果試圖通過它發(fā)送數(shù)據(jù),編譯器會(huì)提示錯(cuò)誤。對(duì)比之下,雙向變單向的功能,如果以 SDK 方式實(shí)現(xiàn),還是很困難的。
CSP 模型與 Actor 模型的區(qū)別
同樣是以消息傳遞的方式來避免共享,那 Golang 實(shí)現(xiàn)的 CSP 模型和 Actor 模型有什么區(qū)別呢?
第一個(gè)最明顯的區(qū)別就是:Actor 模型中沒有 channel。雖然 Actor 模型中的 mailbox 和 channel 非常像,看上去都像個(gè) FIFO 隊(duì)列,但是區(qū)別還是很大的。Actor 模型中的 mailbox 對(duì)于程序員來說是“透明”的,mailbox 明確歸屬于一個(gè)特定的 Actor,是 Actor 模型中的內(nèi)部機(jī)制;而且 Actor 之間是可以直接通信的,不需要通信中介。但 CSP 模型中的 channel 就不一樣了,它對(duì)于程序員來說是“可見”的,是通信的中介,傳遞的消息都是直接發(fā)送到 channel 中的。
第二個(gè)區(qū)別是:Actor 模型中發(fā)送消息是非阻塞的,而 CSP 模型中是阻塞的。Golang 實(shí)現(xiàn)的 CSP 模型,channel 是一個(gè)阻塞隊(duì)列,當(dāng)阻塞隊(duì)列已滿的時(shí)候,向 channel 中發(fā)送數(shù)據(jù),會(huì)導(dǎo)致發(fā)送消息的協(xié)程阻塞。
第三個(gè)區(qū)別則是關(guān)于消息送達(dá)的。在《42 | Actor 模型:面向?qū)ο笤牟l(fā)模型》這篇文章中,我們介紹過 Actor 模型理論上不保證消息百分百送達(dá),而在 Golang 實(shí)現(xiàn)的 CSP 模型中,是能保證消息百分百送達(dá)的。不過這種百分百送達(dá)也是有代價(jià)的,那就是有可能會(huì)導(dǎo)致死鎖。
比如,下面這段代碼就存在死鎖問題,在主協(xié)程中,我們創(chuàng)建了一個(gè)無緩沖的 channel ch,然后從 ch 中接收數(shù)據(jù),此時(shí)主協(xié)程阻塞,main() 方法中的主協(xié)程阻塞,整個(gè)應(yīng)用就阻塞了。這就是 Golang 中最簡(jiǎn)單的一種死鎖。
總結(jié)
Golang 中雖然也支持傳統(tǒng)的共享內(nèi)存的協(xié)程間通信方式,但是推薦的還是使用 CSP 模型,以通信的方式共享內(nèi)存。
Golang 中實(shí)現(xiàn)的 CSP 模型功能上還是很豐富的,例如支持 select 語句,select 語句類似于網(wǎng)絡(luò)編程里的多路復(fù)用函數(shù) select(),只要有一個(gè) channel 能夠發(fā)送成功或者接收到數(shù)據(jù)就可以跳出阻塞狀態(tài)。鑒于篇幅原因,我就點(diǎn)到這里,不詳細(xì)介紹那么多了。
CSP 模型是托尼·霍爾(Tony Hoare)在 1978 年提出的,不過這個(gè)模型這些年一直都在發(fā)展,其理論遠(yuǎn)比 Golang 的實(shí)現(xiàn)復(fù)雜得多,如果你感興趣,可以參考霍爾寫的Communicating Sequential Processes這本電子書。另外,霍爾在并發(fā)領(lǐng)域還有一項(xiàng)重要成就,那就是提出了霍爾管程模型,這個(gè)你應(yīng)該很熟悉了,Java 領(lǐng)域解決并發(fā)問題的理論基礎(chǔ)就是它。
Java 領(lǐng)域可以借助第三方的類庫JCSP來支持 CSP 模型,相比 Golang 的實(shí)現(xiàn),JCSP 更接近理論模型,如果你感興趣,可以下載學(xué)習(xí)。不過需要注意的是,JCSP 并沒有經(jīng)過廣泛的生產(chǎn)環(huán)境檢驗(yàn),所以并不建議你在生產(chǎn)環(huán)境中使用。
?
總結(jié)
以上是生活随笔為你收集整理的Java并发编程实战~协程的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ActiveMQ消费者平滑关闭
- 下一篇: Java技术回顾之JNDI--JNDI