新聞中心
Go實(shí)現(xiàn)了兩種并發(fā)形式,第一種是大家普遍認(rèn)知的多線程共享內(nèi)存,其實(shí)就是 Java 或 C++ 等語(yǔ)言中的多線程開發(fā);另外一種是Go語(yǔ)言特有的,也是Go語(yǔ)言推薦的 CSP(communicating sequential processes)并發(fā)模型。

CSP 并發(fā)模型是上個(gè)世紀(jì)七十年代提出的,用于描述兩個(gè)獨(dú)立的并發(fā)實(shí)體通過(guò)共享 channel(管道)進(jìn)行通信的并發(fā)模型。
Go語(yǔ)言就是借用 CSP 并發(fā)模型的一些概念為之實(shí)現(xiàn)并發(fā)的,但是Go語(yǔ)言并沒(méi)有完全實(shí)現(xiàn)了 CSP 并發(fā)模型的所有理論,僅僅是實(shí)現(xiàn)了 process 和 channel 這兩個(gè)概念。
process 就是Go語(yǔ)言中的 goroutine,每個(gè) goroutine 之間是通過(guò) channel 通訊來(lái)實(shí)現(xiàn)數(shù)據(jù)共享。
這里我們要明確的是“并發(fā)不是并行”。并發(fā)更關(guān)注的是程序的設(shè)計(jì)層面,并發(fā)的程序完全是可以順序執(zhí)行的,只有在真正的多核 CPU 上才可能真正地同時(shí)運(yùn)行;并行更關(guān)注的是程序的運(yùn)行層面,并行一般是簡(jiǎn)單的大量重復(fù),例如 GPU 中對(duì)圖像處理都會(huì)有大量的并行運(yùn)算。
為了更好地編寫并發(fā)程序,從設(shè)計(jì)之初Go語(yǔ)言就注重如何在編程語(yǔ)言層級(jí)上設(shè)計(jì)一個(gè)簡(jiǎn)潔安全高效的抽象模型,讓開發(fā)人員專注于分解問(wèn)題和組合方案,而且不用被線程管理和信號(hào)互斥這些煩瑣的操作分散精力。
在并發(fā)編程中,對(duì)共享資源的正確訪問(wèn)需要精確地控制,在目前的絕大多數(shù)語(yǔ)言中,都是通過(guò)加鎖等線程同步方案來(lái)解決這一困難問(wèn)題,而Go語(yǔ)言卻另辟蹊徑,它將共享的值通過(guò)通道傳遞(實(shí)際上多個(gè)獨(dú)立執(zhí)行的線程很少主動(dòng)共享資源)。
并發(fā)編程的核心概念是同步通信,但是同步的方式卻有多種。先以大家熟悉的互斥量 sync.Mutex 來(lái)實(shí)現(xiàn)同步通信,示例代碼如下所示:
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
go func() {
fmt.Println("C語(yǔ)言中文網(wǎng)")
mu.Lock()
}()
mu.Unlock()
}
由于 mu.Lock() 和 mu.Unlock() 并不在同一個(gè) Goroutine 中,所以也就不滿足順序一致性內(nèi)存模型。同時(shí)它們也沒(méi)有其他的同步事件可以參考,也就是說(shuō)這兩件事是可以并發(fā)的。
因?yàn)榭赡苁遣l(fā)的事件,所以 main() 函數(shù)中的 mu.Unlock() 很有可能先發(fā)生,而這個(gè)時(shí)刻 mu 互斥對(duì)象還處于未加鎖的狀態(tài),因而會(huì)導(dǎo)致運(yùn)行時(shí)異常。
下面是修復(fù)后的代碼:
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
mu.Lock()
go func() {
fmt.Println("C語(yǔ)言中文網(wǎng)")
mu.Unlock()
}()
mu.Lock()
}
修復(fù)的方式是在 main() 函數(shù)所在線程中執(zhí)行兩次 mu.Lock(),當(dāng)?shù)诙渭渔i時(shí)會(huì)因?yàn)殒i已經(jīng)被占用(不是遞歸鎖)而阻塞,main() 函數(shù)的阻塞狀態(tài)驅(qū)動(dòng)后臺(tái)線程繼續(xù)向前執(zhí)行。
當(dāng)后臺(tái)線程執(zhí)行到 mu.Unlock() 時(shí)解鎖,此時(shí)打印工作已經(jīng)完成了,解鎖會(huì)導(dǎo)致 main() 函數(shù)中的第二個(gè) mu.Lock() 阻塞狀態(tài)取消,此時(shí)后臺(tái)線程和主線程再?zèng)]有其他的同步事件參考,它們退出的事件將是并發(fā)的,在 main() 函數(shù)退出導(dǎo)致程序退出時(shí),后臺(tái)線程可能已經(jīng)退出了,也可能沒(méi)有退出。雖然無(wú)法確定兩個(gè)線程退出的時(shí)間,但是打印工作是可以正確完成的。
使用 sync.Mutex 互斥鎖同步是比較低級(jí)的做法,我們現(xiàn)在改用無(wú)緩存通道來(lái)實(shí)現(xiàn)同步:
package main
import (
"fmt"
)
func main() {
done := make(chan int)
go func() {
fmt.Println("C語(yǔ)言中文網(wǎng)")
<-done
}()
done <- 1
}
根據(jù)Go語(yǔ)言內(nèi)存模型規(guī)范,對(duì)于從無(wú)緩存通道進(jìn)行的接收,發(fā)生在對(duì)該通道進(jìn)行的發(fā)送完成之前。因此,后臺(tái)線程
<-done 接收操作完成之后,main 線程的
done <- 1 發(fā)送操作才可能完成(從而退出 main、退出程序),而此時(shí)打印工作已經(jīng)完成了。
上面的代碼雖然可以正確同步,但是對(duì)通道的緩存大小太敏感,如果通道有緩存,就無(wú)法保證 main() 函數(shù)退出之前后臺(tái)線程能正常打印了,更好的做法是將通道的發(fā)送和接收方向調(diào)換一下,這樣可以避免同步事件受通道緩存大小的影響:
package main
import (
"fmt"
)
func main() {
done := make(chan int, 1) // 帶緩存通道
go func() {
fmt.Println("C語(yǔ)言中文網(wǎng)")
done <- 1
}()
<-done
}
對(duì)于帶緩存的通道,對(duì)通道的第 K 個(gè)接收完成操作發(fā)生在第 K+C 個(gè)發(fā)送操作完成之前,其中 C 是通道的緩存大小。雖然通道是帶緩存的,但是 main 線程接收完成是在后臺(tái)線程發(fā)送開始但還未完成的時(shí)刻,此時(shí)打印工作也是已經(jīng)完成的。
基于帶緩存通道,我們可以很容易將打印線程擴(kuò)展到 N 個(gè),下面的示例是開啟 10 個(gè)后臺(tái)線程分別打?。?br />
package main
import (
"fmt"
)
func main() {
done := make(chan int, 10) // 帶10個(gè)緩存
// 開N個(gè)后臺(tái)打印線程
for i := 0; i < cap(done); i++ {
go func() {
fmt.Println("C語(yǔ)言中文網(wǎng)")
done <- 1
}()
}
// 等待N個(gè)后臺(tái)線程完成
for i := 0; i < cap(done); i++ {
<-done
}
}
對(duì)于這種要等待 N 個(gè)線程完成后再進(jìn)行下一步的同步操作有一個(gè)簡(jiǎn)單的做法,就是使用 sync.WaitGroup 來(lái)等待一組事件:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
// 開N個(gè)后臺(tái)打印線程
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
fmt.Println("C語(yǔ)言中文網(wǎng)")
wg.Done()
}()
}
// 等待N個(gè)后臺(tái)線程完成
wg.Wait()
}
其中 wg.Add(1) 用于增加等待事件的個(gè)數(shù),必須確保在后臺(tái)線程啟動(dòng)之前執(zhí)行(如果放到后臺(tái)線程之中執(zhí)行則不能保證被正常執(zhí)行到)。當(dāng)后臺(tái)線程完成打印工作之后,調(diào)用 wg.Done() 表示完成一個(gè)事件,main() 函數(shù)的 wg.Wait() 是等待全部的事件完成。
文章標(biāo)題:創(chuàng)新互聯(lián)GO教程:Go語(yǔ)言CSP:通信順序進(jìn)程簡(jiǎn)述
本文URL:http://m.fisionsoft.com.cn/article/cojijip.html


咨詢
建站咨詢
