GoLang-Scheduling In Go : Part III - Concurrency

Scheduling In Go系列文章

本文主要針對Go語言中的調(diào)度杜恰。

目錄

Part I - OS Scheduler
Part II - Go Scheduler
Part III - Concurrency

Part III - Concurrency

這是一個(gè)由三部分組成的系列文章的第三篇净蚤,這一系列文章將提供對Go中調(diào)度器背后的機(jī)制和語義的理解。本文的重點(diǎn)是并發(fā)性唠摹。

Introduction

當(dāng)我在處理一個(gè)問題的時(shí)候,一開始我不會(huì)考慮是否并發(fā)適合這個(gè)問題驯耻。我會(huì)先尋找順序化的解決方案并且確定這是有效可以工作的发皿。然后在可讀性和技術(shù)復(fù)核的時(shí)候崔慧,我會(huì)開始思考是否并發(fā)是合理并且實(shí)用的。有些時(shí)候并發(fā)是非常適合的而其他時(shí)候卻不太明顯穴墅。
在這一系列文章的第一篇文章中惶室,我解釋了OS調(diào)度器的機(jī)器和語義,我認(rèn)為你如果計(jì)劃要寫多線程代碼的話這是很重要的玄货。在第二篇文章中皇钞,我解釋了Go調(diào)度器的語義,我相信這對于理解如何在Go中編寫并發(fā)代碼很重要松捉。在這篇文章中夹界,我將開始把OS和Go調(diào)度器的機(jī)制和語義結(jié)合在一起,以便更深入地理解并發(fā)是什么和不是什么隘世。
這篇文章的目標(biāo)是:

  • 對于一個(gè)工作負(fù)荷是否適合并發(fā)提供你必須要考慮的指導(dǎo)語義
  • 向你展示不同類型的工作負(fù)荷如何更改語義可柿,從而更改你將要做出的工程決策鸠踪。

What is Concurrency

并發(fā)意味著“無順序”執(zhí)行。有一組指令复斥,它們原本要按照順序執(zhí)行营密,找到一種方式無序執(zhí)行這組命令仍然產(chǎn)生同樣的結(jié)果。你面對的問題是目锭,無序執(zhí)行是增加價(jià)值的是必須很明顯的评汰。當(dāng)我說到價(jià)值,這指的是這些復(fù)雜的花費(fèi)會(huì)增加足夠的性能收入痢虹。根據(jù)你的問題被去,無序執(zhí)行有可能是不可能實(shí)現(xiàn)的或者甚至是沒有意義的。
當(dāng)然世分,理解并發(fā)與并行是不一樣的也是很重要的。并行的意思是同時(shí)執(zhí)行兩個(gè)或者更多個(gè)指令缀辩。這個(gè)概念和并發(fā)是不一樣的臭埋。只有當(dāng)你有至少2個(gè)可用的OS和硬件線程并且你至少有兩個(gè)Goroutine時(shí),并行才是可能的臀玄,每個(gè)指令獨(dú)立的在OS或者硬件線程上執(zhí)行瓢阴。


Figure 1

在圖1中,可以看到兩個(gè)邏輯處理器(P)的關(guān)系圖健无,每個(gè)處理器的獨(dú)立操作系統(tǒng)線程(M)連接到機(jī)器上的獨(dú)立硬件線程(Core)荣恐。你可以看到兩個(gè)Goroutine(G1和G2)并行執(zhí)行,它們同時(shí)在各自的OS/硬件線程上執(zhí)行它們的指令累贤。在每個(gè)邏輯處理器中叠穆,三個(gè)Goroutine輪流共享各自的OS線程。所有這些Goroutine都是并發(fā)運(yùn)行的臼膏,它們不按特定順序執(zhí)行指令硼被,并在OS線程上共享時(shí)間。
這就是問題所在渗磅,有時(shí)在沒有并行的情況下利用并發(fā)實(shí)際上會(huì)降低吞吐量嚷硫。有趣的是,有時(shí)利用并行的并發(fā)并不能給你帶來比你認(rèn)為可以實(shí)現(xiàn)的更大的性能增益始鱼。

Workloads

你如何得知什么時(shí)候無序執(zhí)行是可行的或者有意義的呢仔掸?理解你的問題正在處理的工作負(fù)荷類型是一個(gè)很好的起點(diǎn)。當(dāng)考慮到并發(fā)医清,有兩種工作負(fù)荷是需要重點(diǎn)理解的起暮。

  • CPU密集型:這是一個(gè)永遠(yuǎn)不會(huì)造成Goroutine自然進(jìn)出等待狀態(tài)的情況的工作負(fù)荷。這是一項(xiàng)不斷進(jìn)行計(jì)算的工作会烙。將Pi計(jì)算到第n位的線程將是一種CPU密集型工作鞋怀。
  • IO密集型:這是一個(gè)導(dǎo)致Goroutine自然進(jìn)入等待狀態(tài)的工作負(fù)荷双泪。這種類型的工作包括請求通過網(wǎng)絡(luò)訪問資源、對操作系統(tǒng)進(jìn)行系統(tǒng)調(diào)用或等待事件發(fā)生密似。需要讀取文件的Goroutine就是一種IO密集型焙矛。我認(rèn)為同步事件(互斥、原子)残腌,這種會(huì)導(dǎo)致Goroutine等待的事例村斟,也算是這種類型的事件。

在CPU密集型的工作負(fù)荷下抛猫,你需要使用并行來達(dá)到并發(fā)蟆盹。單個(gè)操作系統(tǒng)/硬件線程處理多個(gè)goroutine的效率不高,因?yàn)镚oroutine不會(huì)作為其工作負(fù)荷的一部分進(jìn)入或退出等待狀態(tài)闺金。擁有超過OS或硬件線程數(shù)目的Goroutine數(shù)目會(huì)減慢工作負(fù)荷的執(zhí)行逾滥,這是由于將Goroutine換上換下OS線程會(huì)花費(fèi)延時(shí)。上下文切換會(huì)為你的工作負(fù)荷創(chuàng)建一個(gè)STW(Stop The world)事件败匹,確保在上下文切換期間沒有你的工作負(fù)荷任務(wù)被執(zhí)行寨昙,否則可能會(huì)被執(zhí)行。
在IO密集型的工作負(fù)荷下掀亩,你不需要使用并行來實(shí)現(xiàn)并發(fā)舔哪。單個(gè)OS或者硬件線程可以高效的處理多個(gè)Goroutine,因?yàn)镚oroutine可以作為其工作負(fù)荷的一部分進(jìn)入或退出等待狀態(tài)槽棍。擁有比/硬件線程更多的Goroutine可以加快工作負(fù)荷的執(zhí)行捉蚤,這是因?yàn)閷oroutine換上換下OS線程的延時(shí)花費(fèi)不會(huì)造成STW(Stop The world)事件。你的工作負(fù)載自然停止炼七,這允許不同的Goroutine有效地利用同一個(gè)OS/硬件線程缆巧,而不是讓OS/硬件線程處于空閑狀態(tài)。
你知道每個(gè)硬件線程多少個(gè)Goroutine提供最好的吞吐量么豌拙?如果Goroutine數(shù)目太少了那么你就有了更多的空閑時(shí)間盅蝗,而如果Goroutine數(shù)目太多了,你就會(huì)有更多的上下文切換延時(shí)姆蘸。這是你需要考慮的墩莫,但超出了這篇文章的范圍。
那么現(xiàn)在逞敷,復(fù)習(xí)一些代碼來強(qiáng)化你的能力就非常重要了狂秦,去判斷何時(shí)一個(gè)工作負(fù)荷可以使用并發(fā),何時(shí)不能以及何時(shí)需要使用并行推捐,何時(shí)不需要裂问。

Adding Numbers

我們不需要復(fù)雜的代碼來可視化和理解這些語義。看看下面名為add的函數(shù)堪簿,它對一組整數(shù)求和痊乾。
Listing 1
https://play.golang.org/p/r9LdqUsEzEz

36 func add(numbers []int) int {
37     var v int
38     for _, n := range numbers {
39         v += n
40     }
41     return v
42 }

在Listing 1的36行,定義了一個(gè)名為add的函數(shù)椭更,這個(gè)函數(shù)輸入一組整數(shù)并且返回這組整數(shù)的和哪审。從37行開始,它定義了一個(gè)整數(shù)v來存放這組整數(shù)的和虑瀑。然后在38行這個(gè)函數(shù)線性遍歷整數(shù)集合湿滓,在39行每個(gè)整數(shù)都加到當(dāng)前v上。最后在第41行舌狗,這個(gè)函數(shù)給調(diào)用者返回最終的和叽奥。
問題:這個(gè)add函數(shù)適合無序執(zhí)行么?我相信答案一定是肯定的痛侍。這一組整數(shù)可以分裂成更小的列表并且這些列表可以并發(fā)執(zhí)行朝氓。一旦更小的列表被相加完,這一系列和可以被加在一起最終產(chǎn)生和順序相加版本一樣的結(jié)果主届。
然而赵哲,我們也會(huì)想到另一個(gè)問題。我們應(yīng)該創(chuàng)建和處理多少個(gè)更小的列表從而獲得最好的吞吐量呢岂膳?為了回答這個(gè)問題誓竿,你必須知道add使用的是哪種類型的工作負(fù)荷磅网。add函數(shù)使用的是CPU密集型工作負(fù)荷谈截,因?yàn)檫@個(gè)算法是單純使用數(shù)學(xué)計(jì)算的,它不會(huì)做任何使它自然進(jìn)入等待狀態(tài)的事情涧偷。這說明好的吞吐量所需要的就是每個(gè)OS或者硬件線程使用一個(gè)Goroutine簸喂。
下面Listing 2是我并發(fā)版本的add。
注意:當(dāng)你編寫并發(fā)版本的add時(shí)燎潮,你可以使用幾個(gè)其他的方式喻鳄,采用其他的選擇。這時(shí)不要糾結(jié)于我自己的這種實(shí)現(xiàn)方式确封。如果你有一個(gè)更可讀的版本除呵,并且它執(zhí)行起來效果相同甚至更好,我希望你能分享它爪喘。
Listing 2
https://play.golang.org/p/r9LdqUsEzEz

44 func addConcurrent(goroutines int, numbers []int) int {
45     var v int64
46     totalNumbers := len(numbers)
47     lastGoroutine := goroutines - 1
48     stride := totalNumbers / goroutines
49
50     var wg sync.WaitGroup
51     wg.Add(goroutines)
52
53     for g := 0; g < goroutines; g++ {
54         go func(g int) {
55             start := g * stride
56             end := start + stride
57             if g == lastGoroutine {
58                 end = totalNumbers
59             }
60
61             var lv int
62             for _, n := range numbers[start:end] {
63                 lv += n
64             }
65
66             atomic.AddInt64(&v, int64(lv))
67             wg.Done()
68         }(g)
69     }
70
71     wg.Wait()
72
73     return int(v)
74 }

在Listing 2中颜曾,addConcurrent函數(shù)是add函數(shù)的并發(fā)版本,與5行的非并發(fā)版本代碼相比秉剑,并發(fā)版本使用了26行代碼泛豪。代碼函數(shù)比較多,這里我們只關(guān)注需要重點(diǎn)理解的行。
48行:每個(gè)Goroutine將要將唯一的并且比原數(shù)字集合更小的集合相加诡曙。這個(gè)更小的集合的大小等于總的集合數(shù)字?jǐn)?shù)目除以Goroutine的數(shù)目臀叙。
53行:創(chuàng)建的Goroutine池用來執(zhí)行加法工作。
57-59行:最后一個(gè)Goroutine將添加可能大于其他Goroutine的剩余數(shù)字列表价卤。
66行:小列表的總和被合并成一個(gè)最終的總和劝萤。
并發(fā)版本很明顯要比串行版本更復(fù)雜,但是這個(gè)復(fù)雜度是不是值得的呢荠雕?最好的方式就是創(chuàng)建一個(gè)壓力測試稳其。在這些壓力測試中我使用了一組10 million的數(shù)字,并且關(guān)閉了垃圾回收炸卑。串行版本使用函數(shù)add并且并行版本使用函數(shù)addConcurrent既鞠。
Listing 3

func BenchmarkSequential(b *testing.B) {
    for i := 0; i < b.N; i++ {
        add(numbers)
    }
}

func BenchmarkConcurrent(b *testing.B) {
    for i := 0; i < b.N; i++ {
        addConcurrent(runtime.NumCPU(), numbers)
    }
}

Listing 3展示了壓測函數(shù)。這里的結(jié)果是對于所有的Goroutine只有一個(gè)OS或者硬件線程可以使用盖文。串行版本只使用一個(gè)Goroutine嘱蛋,而并行版本使用runtime.NumCPU個(gè)Goroutine(對我的電腦來說是8個(gè))。在這個(gè)例子中五续,并發(fā)版本使用的是并發(fā)而不是并行洒敏。
Listing 4

10 Million Numbers using 8 goroutines with 1 core
2.9 GHz Intel 4 Core i7
Concurrency WITHOUT Parallelism
-----------------------------------------------------------------------------
$ GOGC=off go test -cpu 1 -run none -bench . -benchtime 3s
goos: darwin
goarch: amd64
pkg: github.com/ardanlabs/gotraining/topics/go/testing/benchmarks/cpu-bound
BenchmarkSequential             1000       5720764 ns/op : ~10% Faster
BenchmarkConcurrent             1000       6387344 ns/op
BenchmarkSequentialAgain        1000       5614666 ns/op : ~13% Faster
BenchmarkConcurrentAgain        1000       6482612 ns/op

注意:在你本地的機(jī)器運(yùn)行一個(gè)基準(zhǔn)測試是很復(fù)雜的。有很多因素會(huì)導(dǎo)致你的壓測是不正確的疙驾。確保你的機(jī)器足夠空閑并且運(yùn)行幾次基準(zhǔn)測試凶伙。你要確保你看到結(jié)果的一致性。由測試工具運(yùn)行兩次基準(zhǔn)測試可以得到最一致的結(jié)果它碎。
由Listing 4基準(zhǔn)測試結(jié)果我們可以看出函荣,對所有Goroutine來說只有一個(gè)OS或者硬件線程時(shí),串行版本比并發(fā)版本大約快10%~13%扳肛。這正是我所期望的傻挂,因?yàn)椴l(fā)版本在單個(gè)線程上有上下文切換的開銷,并且有Goroutine的管理挖息。
下面是當(dāng)每個(gè)Goroutine都有一個(gè)單獨(dú)的OS或者硬件線程可用時(shí)的結(jié)果金拒。串行版本只使用一個(gè)Goroutine,而并行版本使用runtime.NumCPU個(gè)Goroutine(對我的電腦來說是8個(gè))套腹。在這種情況下绪抛,并發(fā)版本利用并行性實(shí)現(xiàn)并發(fā)性。
Listing 5

10 Million Numbers using 8 goroutines with 8 cores
2.9 GHz Intel 4 Core i7
Concurrency WITH Parallelism
-----------------------------------------------------------------------------
$ GOGC=off go test -cpu 8 -run none -bench . -benchtime 3s
goos: darwin
goarch: amd64
pkg: github.com/ardanlabs/gotraining/topics/go/testing/benchmarks/cpu-bound
BenchmarkSequential-8               1000       5910799 ns/op
BenchmarkConcurrent-8               2000       3362643 ns/op : ~43% Faster
BenchmarkSequentialAgain-8          1000       5933444 ns/op
BenchmarkConcurrentAgain-8          2000       3477253 ns/op : ~41% Faster

Listing 5中的基準(zhǔn)測試結(jié)果顯示电禀,當(dāng)每個(gè)Goroutine都有一個(gè)單獨(dú)的OS或者硬件線程可用時(shí)幢码,并發(fā)版本比串行版本快大約41~43%。這是我所期望的鞭呕,因?yàn)樗蠫oroutine現(xiàn)在都并行運(yùn)行蛤育,八個(gè)Goroutine同時(shí)執(zhí)行它們的并發(fā)工作宛官。

Sorting

重要的是要理解,并非所有的CPU密集型工作負(fù)荷都適合并發(fā)性瓦糕。這主要是因?yàn)榇驍喙ぷ鞯紫矗蛘呤呛喜⑺薪Y(jié)果是代價(jià)比較大的。我們可以看到排序算法中的一個(gè)例子叫做冒泡排序咕娄『ヒ荆看看下面在Go中實(shí)現(xiàn)冒泡排序的代碼。
*Listing 6
https://play.golang.org/p/S0Us1wYBqG6

01 package main
02
03 import "fmt"
04
05 func bubbleSort(numbers []int) {
06     n := len(numbers)
07     for i := 0; i < n; i++ {
08         if !sweep(numbers, i) {
09             return
10         }
11     }
12 }
13
14 func sweep(numbers []int, currentPass int) bool {
15     var idx int
16     idxNext := idx + 1
17     n := len(numbers)
18     var swap bool
19
20     for idxNext < (n - currentPass) {
21         a := numbers[idx]
22         b := numbers[idxNext]
23         if a > b {
24             numbers[idx] = b
25             numbers[idxNext] = a
26             swap = true
27         }
28         idx++
29         idxNext = idx + 1
30     }
31     return swap
32 }
33
34 func main() {
35     org := []int{1, 3, 2, 4, 8, 6, 7, 2, 3, 0}
36     fmt.Println(org)
37
38     bubbleSort(org)
39     fmt.Println(org)
40 }

在Listing 6中是一個(gè)冒泡排序在Go語言中的實(shí)現(xiàn)圣勒,這個(gè)排序算法掃描一個(gè)整數(shù)列表费变,沒經(jīng)過一個(gè)值會(huì)進(jìn)行交換值。取決于列表的順序圣贸,在排序完成前可能需要經(jīng)過多次掃描列表挚歧。
問題:冒泡排序函數(shù)是適合無序執(zhí)行的工作負(fù)荷嗎?我想答案是否定的吁峻,整數(shù)的集合可以分解成更小的列表滑负,這些列表可以同時(shí)排序。但是用含,在完成所有并發(fā)工作之后矮慕,沒有有效的方法將較小的列表排序在一起。下面是冒泡排序的并發(fā)版本的一個(gè)例子啄骇。
Listing 8

01 func bubbleSortConcurrent(goroutines int, numbers []int) {
02     totalNumbers := len(numbers)
03     lastGoroutine := goroutines - 1
04     stride := totalNumbers / goroutines
05
06     var wg sync.WaitGroup
07     wg.Add(goroutines)
08
09     for g := 0; g < goroutines; g++ {
10         go func(g int) {
11             start := g * stride
12             end := start + stride
13             if g == lastGoroutine {
14                 end = totalNumbers
15             }
16
17             bubbleSort(numbers[start:end])
18             wg.Done()
19         }(g)
20     }
21
22     wg.Wait()
23
24     // Ugh, we have to sort the entire list again.
25     bubbleSort(numbers)
26 }

在Listing 8中痴鳄,bubbleSortConcurrent函數(shù)是bubbleSort函數(shù)的一個(gè)并發(fā)版本。它使用多個(gè)Goroutine同時(shí)對列表的部分進(jìn)行排序缸夹。但是痪寻,剩下的是一個(gè)分塊排序的值列表。給定一個(gè)36個(gè)數(shù)字的列表明未,分成12組槽华,如果整個(gè)列表沒有在第25行再次排序壹蔓,這將是結(jié)果列表趟妥。
Listing 9

Before:
  25 51 15 57 87 10 10 85 90 32 98 53
  91 82 84 97 67 37 71 94 26  2 81 79
  66 70 93 86 19 81 52 75 85 10 87 49

After:
  10 10 15 25 32 51 53 57 85 87 90 98
   2 26 37 67 71 79 81 82 84 91 94 97
  10 19 49 52 66 70 75 81 85 86 87 93

由于冒泡排序的本質(zhì)是遍歷列表,所以第25行對bubbleSort函數(shù)的調(diào)用將否定使用并發(fā)性的任何可能的好處佣蓉。使用冒泡排序時(shí)披摄,使用并發(fā)不會(huì)提高性能。

Reading Files

已經(jīng)提供了兩個(gè)CPU密集型的工作負(fù)荷勇凭,但是IO密集型的工作負(fù)荷呢疚膊?當(dāng)Goroutine自然地進(jìn)入和退出等待狀態(tài)時(shí),語義是否不同虾标?下面看一下讀取文件并執(zhí)行文本搜索的IO密集型工作負(fù)荷寓盗。
第一個(gè)版本是串行版本,函數(shù)是find。
Listing 10
https://play.golang.org/p/8gFe5F8zweN

42 func find(topic string, docs []string) int {
43     var found int
44     for _, doc := range docs {
45         items, err := read(doc)
46         if err != nil {
47             continue
48         }
49         for _, item := range items {
50             if strings.Contains(item.Description, topic) {
51                 found++
52             }
53         }
54     }
55     return found
56 }

在Listing 10中傀蚌,你可以看到串行版本的find函數(shù)基显。在43行,定義了值value用來存放指定的topic在給定文檔中出現(xiàn)的次數(shù)善炫。在44行撩幽,對文檔進(jìn)行迭代遍歷送漠,并且每個(gè)文檔在45行使用read函數(shù)進(jìn)行讀取乡摹。最后在49~53行中钻哩,使用strings包中的Contains函數(shù)來檢查給定topic是否在讀取到的items中出現(xiàn)凡橱。如果找到了topic思币,則found值增量加一棵红。
下面是由found函數(shù)調(diào)用的read函數(shù)的實(shí)現(xiàn)噪舀。
Listing 11
https://play.golang.org/p/8gFe5F8zweN

33 func read(doc string) ([]item, error) {
34     time.Sleep(time.Millisecond) // Simulate blocking disk read.
35     var d document
36     if err := xml.Unmarshal([]byte(file), &d); err != nil {
37         return nil, err
38     }
39     return d.Channel.Items, nil
40 }

read函數(shù)以time.Sleep調(diào)用開始垢油,持續(xù)一毫秒静汤。此調(diào)用用于模擬如果執(zhí)行實(shí)際的系統(tǒng)調(diào)用從磁盤讀取文檔時(shí)可能產(chǎn)生的延遲读串。此延遲的一致性對于準(zhǔn)確測量find的串行版本與并發(fā)版本的性能非常重要。然后在第35~39行撒妈,將存儲(chǔ)在全局變量文件中的模擬xml文檔解碼為結(jié)構(gòu)體進(jìn)行處理恢暖。最后,在第39行將一組items返回給調(diào)用者狰右。
有了串行版本杰捂,下面是并發(fā)版本。
注意:當(dāng)你編寫并發(fā)版本的find時(shí)棋蚌,你可以使用幾個(gè)其他的方式嫁佳,采用其他的選擇。這時(shí)不要糾結(jié)于我自己的這種實(shí)現(xiàn)方式谷暮。如果你有一個(gè)更可讀的版本蒿往,并且它執(zhí)行起來效果相同甚至更好,我希望你能分享它湿弦。
Listing 12
https://play.golang.org/p/8gFe5F8zweN

58 func findConcurrent(goroutines int, topic string, docs []string) int {
59     var found int64
60
61     ch := make(chan string, len(docs))
62     for _, doc := range docs {
63         ch <- doc
64     }
65     close(ch)
66
67     var wg sync.WaitGroup
68     wg.Add(goroutines)
69
70     for g := 0; g < goroutines; g++ {
71         go func() {
72             var lFound int64
73             for doc := range ch {
74                 items, err := read(doc)
75                 if err != nil {
76                     continue
77                 }
78                 for _, item := range items {
79                     if strings.Contains(item.Description, topic) {
80                         lFound++
81                     }
82                 }
83             }
84             atomic.AddInt64(&found, lFound)
85             wg.Done()
86         }()
87     }
88
89     wg.Wait()
90
91     return int(found)
92 }

在Listing 12中瓤漏,findConcurrent函數(shù)是find函數(shù)的并發(fā)版本。并發(fā)版本使用30行代碼颊埃,而非并發(fā)版本使用13行代碼蔬充。我實(shí)現(xiàn)并發(fā)版本的目標(biāo)是控制用于處理未知數(shù)量文檔的Goroutine的數(shù)量。我選擇了一種池模式班利,使用一個(gè)通道來為Goroutine池提供數(shù)據(jù)饥漫。
這里有很多代碼,所以我只強(qiáng)調(diào)需要理解的重要行罗标。
61~64行:創(chuàng)建一個(gè)通道庸队,并填充上所有要處理的文檔积蜻。
65行:通道已關(guān)閉,因此Goroutine池在處理完所有文檔時(shí)自然終止彻消。
70行:創(chuàng)建Goroutine池浅侨。
73~83行:池中的每個(gè)Goroutine從通道接收一個(gè)文檔,將文檔讀入內(nèi)存并檢查topic是否匹配证膨。當(dāng)存在匹配項(xiàng)時(shí)如输,局部lFound值變量將遞增。
84行:單個(gè)Goroutine計(jì)數(shù)的總和被合并為最終計(jì)數(shù)央勒。
并發(fā)版本肯定比串行版本更復(fù)雜不见,但復(fù)雜性是否值得呢?再次回答這個(gè)問題的最好方法是創(chuàng)建一個(gè)基準(zhǔn)測試崔步。對于這些基準(zhǔn)測試稳吮,我使用了1000個(gè)文檔的集合,而垃圾回收處于關(guān)閉狀態(tài)井濒。創(chuàng)建了一個(gè)使用find函數(shù)的串行版本和使用findConcurrent函數(shù)的并發(fā)版本灶似。
Listing 13

func BenchmarkSequential(b *testing.B) {
    for i := 0; i < b.N; i++ {
        find("test", docs)
    }
}

func BenchmarkConcurrent(b *testing.B) {
    for i := 0; i < b.N; i++ {
        findConcurrent(runtime.NumCPU(), "test", docs)
    }
}

Listing 13顯示了基準(zhǔn)測試函數(shù)。下面是當(dāng)所有Goroutine只有一個(gè)OS或者硬件線程可用時(shí)的結(jié)果瑞你。串行版本使用1個(gè)Goroutine酪惭,并發(fā)版本在使用runtime.NumCPU個(gè)Goroutine,這里在我的機(jī)器上是8個(gè)Goroutine者甲。在這種情況下春感,并發(fā)版本利用的是無并行性的并發(fā)性。
Listing 14

10 Thousand Documents using 8 goroutines with 1 core
2.9 GHz Intel 4 Core i7
Concurrency WITHOUT Parallelism
-----------------------------------------------------------------------------
$ GOGC=off go test -cpu 1 -run none -bench . -benchtime 3s
goos: darwin
goarch: amd64
pkg: github.com/ardanlabs/gotraining/topics/go/testing/benchmarks/io-bound
BenchmarkSequential                3    1483458120 ns/op
BenchmarkConcurrent               20     188941855 ns/op : ~87% Faster
BenchmarkSequentialAgain           2    1502682536 ns/op
BenchmarkConcurrentAgain          20     184037843 ns/op : ~88% Faster

Listing 14中的基準(zhǔn)測試顯示虏缸,當(dāng)只有一個(gè)OS或者硬件線程可用于所有Goroutines時(shí)鲫懒,并發(fā)版本比串行版本快約87到88%。這是我所期望的刽辙,因?yàn)樗蠫oroutine都有效地共享單個(gè)OS或者硬件線程窥岩。read調(diào)用中每個(gè)Goroutine的自然的進(jìn)行上下文切換使得在單個(gè)OS或者硬件線程上完成更多的工作。
下面是使用并行性的并發(fā)版本的基準(zhǔn)測試宰缤。
Listing 15

10 Thousand Documents using 8 goroutines with 1 core
2.9 GHz Intel 4 Core i7
Concurrency WITH Parallelism
-----------------------------------------------------------------------------
$ GOGC=off go test -run none -bench . -benchtime 3s
goos: darwin
goarch: amd64
pkg: github.com/ardanlabs/gotraining/topics/go/testing/benchmarks/io-bound
BenchmarkSequential-8                  3    1490947198 ns/op
BenchmarkConcurrent-8                 20     187382200 ns/op : ~88% Faster
BenchmarkSequentialAgain-8             3    1416126029 ns/op
BenchmarkConcurrentAgain-8            20     185965460 ns/op : ~87% Faster

Listing 15中的基準(zhǔn)測試結(jié)果表明颂翼,引入額外的OS或者硬件線程并不能提供更好的性能。

Conclusion

這篇文章的目的是在語義上提供一些指導(dǎo)撵溃,關(guān)于你必須要考慮的東西當(dāng)你決定一個(gè)工作負(fù)荷是否適合使用并發(fā)性的時(shí)候疚鲤。我試圖提供不同類型算法和工作負(fù)荷的示例锥累,以便你可以看到語義上的差異以及需要考慮的不同工程決策缘挑。
你可以發(fā)現(xiàn),使用IO密集型的工作負(fù)荷桶略,不需要并行性就可以大大提高性能语淘。這與你所看到的CPU密集型的工作負(fù)荷相反诲宇。當(dāng)涉及到冒泡排序之類的算法時(shí),并發(fā)性的使用會(huì)增加復(fù)雜性惶翻,而不會(huì)對性能產(chǎn)生任何真正的好處姑蓝。重要的是確定你的工作負(fù)荷是否適合并發(fā)性,然后確定必須使用正確語義的工作負(fù)荷類型吕粗。

本文系翻譯纺荧,有翻譯不當(dāng)?shù)牡胤交蛘咂渌麊栴}還請多多指教交流~
Scheduling In Go原文索引:
Scheduling In Go : Part I - OS Scheduler
Scheduling In Go : Part II - Go Scheduler
Scheduling In Go : Part III - Concurrency

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市颅筋,隨后出現(xiàn)的幾起案子宙暇,更是在濱河造成了極大的恐慌,老刑警劉巖议泵,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件占贫,死亡現(xiàn)場離奇詭異,居然都是意外死亡先口,警方通過查閱死者的電腦和手機(jī)型奥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來碉京,“玉大人厢汹,你說我怎么就攤上這事⌒持妫” “怎么了坑匠?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長卧惜。 經(jīng)常有香客問我厘灼,道長,這世上最難降的妖魔是什么咽瓷? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任设凹,我火速辦了婚禮,結(jié)果婚禮上茅姜,老公的妹妹穿的比我還像新娘闪朱。我一直安慰自己,他們只是感情好钻洒,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布奋姿。 她就那樣靜靜地躺著,像睡著了一般素标。 火紅的嫁衣襯著肌膚如雪称诗。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天头遭,我揣著相機(jī)與錄音寓免,去河邊找鬼癣诱。 笑死,一個(gè)胖子當(dāng)著我的面吹牛袜香,可吹牛的內(nèi)容都是我干的撕予。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蜈首,長吁一口氣:“原來是場噩夢啊……” “哼实抡!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起欢策,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤澜术,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后猬腰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鸟废,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年姑荷,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了盒延。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,727評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鼠冕,死狀恐怖添寺,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情懈费,我是刑警寧澤计露,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站憎乙,受9級特大地震影響票罐,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜泞边,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一该押、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧阵谚,春花似錦蚕礼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至嗡午,卻和暖如春囤躁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工割以, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留金度,地道東北人应媚。 一個(gè)月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓严沥,卻偏偏與公主長得像,于是被迫代替她去往敵國和親中姜。 傳聞我的和親對象是個(gè)殘疾皇子消玄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評論 2 354