1. 程式人生 > 程式設計 >譯 | Concurrency is not Parallelism

譯 | Concurrency is not Parallelism

來源:cyningsun.github.io/12-09-2019/…

目錄

Concurrency vs Parallelism

如果你看看今天的程式語言,可能會發現這個世界是面向物件的,但實際上並非如此,世界是並行的。你通過網路等等,從最底層(例如:多核計算機)獲取所有的東西,一路攀升到了解星球、宇宙。世界上所有的東西都在同時發生,但我們擁有的計算工具實際上並不擅長表達這種世界觀。看起來似乎是一種失敗,如果我們瞭解什麼是併發性,以及如何使用它,就可以解決這個問題。

我將假設你們中的大多數人至少聽說過 Go 程式語言,它也是我最近幾年在 Google 工作的內容。Go 是一種併發語言,也就是說它使併發性有用,有像上帝一樣同時執行事物的能力;有在同時執行的事物之間進行通訊的能力;有叫做 select
語句的東西,它是一個多路併發控制開關。如果你搞不懂是怎麼回事,不用擔心。

在大約兩年前,當我們釋出 Go 時,在場的程式設計師都說:”哦,併發工具,我知道做什麼的,並行執行,耶“。但實際並非如此,併發性和並行性是不一樣的,這是個常被誤解的問題。我在這裡試圖解釋原因,並向您展示併發性實際上更好。那些困惑的人會碰到什麼事情:他們執行的程式,在更多的處理器上會變得更慢。他們會認為有問題,不管用,想要逃開。但真正有問題的是世界觀,我希望我能改正它。

什麼是併發性?併發性,如我當前使用的一樣,用於電腦科學領域,是一種構建事物的方法。它是由獨立執行的事物組成,通常是 function,雖然不一定必須如此。我們通常稱之為互動式程式。稱其為程式,並不是指 Linux 程式,指的是一種普遍的概念,它包括執行緒、協程、程式等等,所以儘可能抽象的理解。併發性由獨立執行的程式組成;

另一方面,並行性是同時執行多個事物,可能相關,也可能無關。

如果你用語焉不詳的方式思考,併發性是指同時負責很多事情;並行性是指同時做很多事情。它們顯然是相關的,但實際上是不同的概念,如果沒有合適的工具包,嘗試思考它們會有些困惑。一個是關於結構併發性,另一個是關於執行並行性。我會告訴你為什麼這些概念很重要。併發性是一種構造事物的方法,以便可以使用並行性更好地完成工作。但並行性不是併發性的目標,併發性的目標是好的結構。

An analogy

如果你在執行一個作業系統的話,會很熟悉的一個類比。作業系統可能有滑鼠驅動程式、鍵盤驅動程式、顯示驅動程式、網路驅動程式等等,都是由作業系統管理的,核心中的獨立事物。它們都是併發的事物,卻不一定是並行的。如果只有一個處理器,同一時間其中只有一個處於執行。I/O 裝置具有一個併發模型,但本質不是並行的,它不需要是並行的。並行的事物可能類似向量點積,可以分解為微觀操作,以使得可以在一些精美的計算機上並行執行。非常不同的概念,完全不是同一個東西。

Cocurrency plus communication

為了能利用併發性,必須新增 communication 的概念。我今天不會聚焦於該概念太多,但一會兒你會看到一點點。併發性提供了一種將程式構造為獨立塊的方法,然後,必須使這些塊協調一致。要使之工作,需要某種形式的 communicationTony Hoare 在1978年寫了一篇論文叫做 《communicating sequential processes》,實在是電腦科學中最偉大的論文之一。如果你還沒讀過,要是從本次演講中真有什麼領悟的話,回家你應該讀讀那篇論文。它太不可思議了,基於此論文,很多人未進行太多考慮就遵循、並構建工具,以將其思想運用到併發語言中,比如另一種很棒的語言Erlang。GO 中也有其一些思想,關鍵點都在原稿中,除了稍後會提到的幾個小例外。

Gophers

但,你看這一切太抽象了。我們需要 Gopher 的幫忙,來一些 Gopher。

有一個真正的問題我們要解決。有一堆過時的手冊,可以是 C++98 手冊,現在已經是 C++11;或許是 C++11 書籍,但不再需要了。關鍵是我們想要清理掉它們,它們佔用了大量空間。所以我們的 Gopher 有一個任務,把書從書堆裡取出來,放到焚化爐裡清理掉。但是,如果是一大堆書,只有一個 Gopher 需要很長時間。Gopher 也不擅長搬運書籍,儘管我們提供了小車。

所以再增加一個 Gopher 來解決這個問題,只有 Gopher 不會好起來,對吧?

因為它需要工具,無可厚非,我們需要提供所有它需要的東西。Gopher 不僅需要作為 Gopher 的能力,也需要工具來完成任務。再給它一輛車,現在這樣應該會更快。在兩個 Gopher 推車的情況下,肯定能更快地搬運書。但可能存在一個小問題,因為我們必須使它們同步。來回奔波中,書堆互相妨礙,它們可能會被困在焚化爐裡,所以它們需要協調一點。所以你可以想象 Gopher 們傳送 Tony Hoare 的簡訊息,說:我到了,我需要空間把書放進焚化爐。不管是什麼,但你明白了,這很傻。但我想解釋清楚,這些概念並不深刻,它們非常好。

如何讓它們搬運得更快,我們把一切都增加一倍。我們提供兩個 Gopher,把書堆,焚化爐和 Gopher 一樣也增加一倍。現在我們可以在相同的時間裡搬運兩倍的書,這是並行,對吧?

但是,想象它不是並行,而是兩個 Gopher 的併發組合。併發性是我們表達問題的方式,兩個 Gopher 可以做到這一點。我們通過例項化 Gopher 程式的更多例項來並行,這被稱為程式(在此情況下稱為 Gopher)的併發組合。

現在這種設計不是自動並行的。確實有兩個 Gopher,但是誰說它們必須同時工作呢?我可以說,同時只有一個 Gopher 可以移動,就像單核計算機,此設計仍然是併發的,很好很正確,但它本質上不是並行的,除非讓兩個 Gopher 同時搬運。當並行出現時,有兩個事物同時執行,而不僅僅是擁有兩個事物。這是一個非常重要的模型,一旦斷定理解了它,我們就會明白可以把問題分解成併發的塊。

我們可以想出其他模型,下面有一個不同的設計。在圖中有三個 Gopher,同一堆書,同一個焚化爐,但是現在有三個 Gopher。有一個 Gopher,它的工作就是裝車;有一個 Gopher,它的工作就是推車,然後再把空的還回去;還有一個 Gopher,它的工作就是裝入焚化爐。三個 Gopher,速度理應會更快。但可能不會快多少,因為它們會被阻塞。書可能在錯誤的地方,在那裡沒有什麼需要用車來做的。

讓我們處理下這個問題,另外增加一個Gopher歸還空車,這明顯很傻。但我想指出一個相當深刻的問題,這個版本的問題實際上會比之前版本的問題執行得更好。儘管為了完成更多的工作,增加了一個 Gopher 來回奔波。因此,一旦我們理解了併發性的概念,就可以向圖片增加 Gopher,真的可以做更多的工作,使之執行得更快。因為管理的更好的塊的併發組合真的可以執行得更快。工作可能不會恰好完美地進行,但是可以想象如果所有的 Gopher 的時間都恰到好處,它們知道每次搬運多少書。併發組合真的可以讓4個 Gopher 都一直在忙碌。事實上,此版本可能比原來的版本快四倍。雖然可能性不大,但是我想讓你理解,是可能的。

此時有一個發現,它非常重要而且很微妙,有些柔性。我們在現有的設計中通過新增併發程式來提高程式的效能。我們真的添加了更多的東西,整個過程變得更快了。如果仔細想想,有點奇怪,也有點不奇怪。因為額外添加了一個 Gopher,而且 Gopher 確實工作。但是如果你忘記了它是個 Gopher 的事實,並認為只是增加了東西,設計確實可以使它更高效。並行性可以出自於對問題更好的並發表達,這是一個相當深刻的見解。因為 Gopher 們的參與所以看起來不像。但是沒關係。

此時有四個程式併發執行。一個 Gopher 將東西裝到車中;一個 Gopher 把車運到焚化爐;還有另一個 Gopher 將車中的物品卸到焚化爐中;第四個 Gopher 把空車還回來。您可以將它們視為獨立的程式,完全獨立執行的程式,我們只是將它們並行組合以構建完整的程式解決方案。這不是我們唯一可以構造的方案,以下是一個完全不同的設計。

通過增加另外一個堆書、焚化爐、和4個 Gopher,可以使該設計更加並行。但關鍵是,採用已有概念以分解問題。一旦清楚這是併發分解,就可以在不同的緯度上使其並行化。無論能否獲得更好的吞吐量,但是至少,我們得以更細粒度的理解問題,可以控制這些塊。在此情況下,如果一切剛好,會有8個 Gopher 努力以燒掉那些C++手冊。

當然,也許根本沒有發生並行,誰說這些 Gopher 必須同時工作,我可能每次只能執行一個 Gopher。在這種情況下,該設計只能像原始問題一樣,以單個 Gopher 的速率執行。它執行時,其他7個將全部空閒。但該設計仍然是正確的。這很了不得,因為意味著我們在保證併發性時不必擔心並行性。如果併發性正確,並行性實際上是一個自由變數,決定有多少個 Gopher 處於忙碌。我們可以為整個事情做一個完全不同的設計。

讓我們忘記將舊模式投入到新模式。在故事中有兩個 Gopher,不再讓一個 Gopher 從書堆一直運到焚化爐,而是在中間加入暫存區。因此,第一個 Gopher 將書籍搬運到暫存區,將它們丟下,跑回去再運另外一些。第二個 Gopher 坐在那裡等待書達到暫存區,並把書從該位置搬運到焚化爐。如果一切良好,則有兩個 Gopher 程式執行。它們是相同的型別,但有些細微不同,引數略有不同。如果系統將正常執行,一旦啟動,其執行速度就會是原始模式的兩倍。即使某些方面說它是完全不同的設計。一旦我們有了這個組合,我們可以採取另外的做法。

將以慣常的做法使其並行,同時執行整個程式的兩個版本。翻倍之後,有了4個 Gopher,吞吐量將高達四倍。

或者,我們可以採用另一種做法,在剛才的併發多 Gopher 問題中,在中間加入暫存區。因此,現在我們有8個 Gopher 在執行,書籍非常快的速度被焚燒。

但這樣還不夠好,因為我們可以在另一個維度並行,運力全開。此時,有16個 Gopher 將這些書搬運到焚化爐中。顯然,增加 Gopher 使問題解決的更好,是非常簡單和愚蠢的。但我希望您瞭解,從概念上講,這真的就是您考慮並行執行事物的方式。您無需考慮並行執行,而是考慮如何將問題以可分解、可理解、可操作的方式,分解為獨立的塊,然後組合起來以解決整個問題。

Lesson

以上就是的所有例子有什麼意義呢?

首先,有很多方法可以做到這一點,我剛剛展示了一些。如果你坐在那裡拿著一本速寫冊,你可能會想出另外50種讓 Gopher 搬運書的方法。有很多不同的設計,它們不必都相同,但它們都能工作。然後,您可以對這些併發設計進行重構、重新排列、按不同的維度進行縮放,得到不同的功能以處理問題。真是太好了,因為不管你怎麼做,處理這個問題的演演算法的正確性很容易保證。這樣做不會搞砸,我的意思它們只是 Gopher,你知道這些設計本質上是安全的,因為你是那樣做的。但是,這無疑是一個愚蠢的問題,與實際工作無關。嗯,事實上確實有關。

因為如果你拿到這個問題,把書堆換成一些網路內容;把 Gopher 換成 CPU,把推車換成網路或編碼程式碼等等;把問題變成你需要行動資料;焚化爐是網路代理,或是瀏覽器,你想到的任何的資料使用者。您剛剛構建了一個 Web 服務體系結構的設計。你可能不認為你的 Web 服務架構是這樣的,但事實上差不多就是這樣。你可以把這兩塊替換掉看看,這正是你想的那種設計。當你談論代理、轉發代理和緩衝區之類會,擴容更多的例項的東西時,它們都在這個圖上,只是不被這麼想。本質上並不難理解它們,Gopher 能做到,我們也能。

A little background about Go

現在讓我來展示如何在使用Go構建東西時採用這些概念。我不打算在這次演講中教你 Go,希望你們有些人已經知道它,希望大家在之後能去更多瞭解它。但我要教一點點 Go,希望其他人也能像我們一樣融入其中。

Goroutines

Go 有叫做 goroutine 的東西,可以認為有點像執行緒,但實際上是不同的。與其詳細地談有什麼不同,不如說說它是什麼吧。假設我們有一個函式,函式有兩個引數。如果在程式中呼叫該函式 F,則在執行下一條語句之前要等待該函式完成。很熟悉,大家都知道。但是,如果在呼叫該函式之前放置關鍵字 go。你呼叫該函式,函式開始執行,雖然不一,至少在概念上可以立即繼續執行。想想併發與並行,從概念上講,當 F 不在時,程式一直在執行,你在做 F 的事情,不用等 F 回來。如果你覺得很困惑,那就把它想象成一個很像 shell 裡的 & 符號。這就像在後臺執行 F &,確切地說是一個 goroutine。

它有點像執行緒,因為一起執行在同一個地址空間中,至少在一個程式中如此。但 goroutine 要廉價得多,建立很容易,建立成本也很低。然後根據需要,goroutine 動態地多路對映到執行中的作業系統執行緒上,所以不必擔心排程、阻塞等等,系統會幫你處理。當 goroutine 確實需要阻塞執行像 read 之類的系統呼叫時,其他 goroutine 不需要等待它,它們都是動態排程的。所以 goroutine 感覺像執行緒,卻是更輕量版本的執行緒。這不是一個原始的概念,其他語言和系統已經實現了類似的東西。我們給它起了自己的名字來說明它是什麼。所以稱之為 goroutine。

Channels

剛剛已經提到需要在 goroutine 之間通訊。為了做到這一點,在 Go 中,稱之為 channel。它有點像 shell 中的管道,但它有型別,還有其他一些很棒的特性,今天就不深入了。但以下有一個相當小的例子。我們建立了一個timer channel,顯然它是一個時間值的 channel;然後在後臺啟動這個函式;sleep 一定的時間 deltaT,然後在 timer channel 上傳送當時的時間 time.now()。因為此函式是用 go 語句啟動的,不需要等待它。它可以做任何想做的事情,當需要知道其他 goroutine 完成時,它說我想從 timer channel 接收那個值。該 goroutine 會阻塞直到有一個值被傳遞過來。一旦完成,它將設定為得到的時間,即其他 goroutine 完成的時間。小例子,但你需要的一切都在那張小幻燈片裡。

Select

最後一部分叫做 select。它讓你可以通過同時監聽多個 channel 控制程式的行為。一旦就能看出誰準備好通訊了,你就可以讀取。在這種情況下,channel 1channel 2,程式的行為將不同,取決於 channel 1channel 2 是否準備就緒。在這種情況下,如果兩個都沒有準備好,那麼 default 子句將執行,這意味著,如果沒有人準備好進行通訊,那麼你會 fall through。如果 default 子句不存在,執行 select,需要等待其中一個或另一個 channel 就緒。如果它們都準備好了,系統會隨機挑選一個。所以這種要晚一點才能結束。像 switch 語句,但用於通訊場景。如果你知道 Dijkstra 的監督命令,應該會很熟悉

當我說 Go 支援併發,是說它確實支援併發,在 Go 程式中建立數千個 goroutine 是常規操作。我們曾經在會議現場除錯一個go程式,它執行在生產環境,已經建立了130萬個 goroutine,並且在除錯它的時候,有1萬個活躍的。當然,要做到如此,goroutine 必須比執行緒廉價得多,這是重點。goroutine 不是免費的,涉及到記憶體分配,但不多。它們根據需要增長和縮小,而且管理得很好。它們非常廉價,你可以認為和 Gopher 一樣廉價。

Closures

你還需要閉包,我剛在前面的頁面展示過閉包,這只是在 Go 語言中可以使用它的證據。因為它們是非常方便的並發表達式,可以建立匿名的 procedure。因此,您可以建立一個函式,在本例中,組合多個函式返回一個函式。這只是一個有效的證明,它是真正的閉包,可以使用 go 語句執行。

讓我們使用這些元素來構建一些示例。我希望你能潛移默化的學習一些 Go 併發程式設計,這是最好的學習方法。

Some examples

Launching daemons

從啟動一個守護程式開始,您可以使用閉包來包裝一些您希望完成但不想等待的後臺操作。在這種情況下,我們有兩個 channel 輸入和輸出,無論出於什麼原因,我們需要將輸入傳遞到輸出,但不想等到複製完成。所以我們使用 go func 和 閉包,然後有一個 for 迴圈,它讀取輸入值並寫入輸出,Go 中的 for range 子句將耗盡 channel。它一直執行直到 channel 為空,然後退出。所以這一小段程式碼會自動耗盡 channel。因為在後臺執行,所以你不需要等待它。這是一個小小的範例,但你知道它還不錯,而且已經習慣了。

A simple load balancer

現在讓我向您展示一個非常簡單的 Load Balancer。如果有時間的話,我會給你看另一個例子。這個例子很簡單,想象一下你有一大堆工作要完成。我們將它們抽象出來,將它們具體化為一個包含三個整數值的 Work 結構體,您需要對其執行一些操作。

worker 要做的是根據這些值執行一些計算。然後我在此處加入 Sleep,以保證我們考慮阻塞。因為 worker 可能會被阻塞的一定的時間。我們構造它的方式是讓 worker 從 input channel 讀取要做的工作,並通過 output channel 傳遞結果,它們是這個函式的引數。在迴圈中,遍歷輸入值,執行計算,sleep 一段任意長的時間,然後將響應傳遞給輸出,傳遞給等待的任務,所以我們得操心阻塞。那一定很難,對吧,以下就是全部的解決方案。

之所以如此簡單,是因為channel 以及它與語言的其他元素一起工作的方式,讓您能夠表達併發的東西,並很好地組合它們。做法是建立兩個 channel, input channel 和 output channel,連線到 worker。 所有 worker 從 input channel 讀取,然後傳輸到 output channel;然後啟動任意數量的 worker。注意中間的 go 子句,所有 worker 都在併發執行,也許是並行執行;然後你開始另一項工作,如螢幕顯示為這些 worker 創造大量的工作,然後在函式呼叫中掛起,接收大量的結果,即從 ouput channel 中按照結果完成的順序讀取其值。因為作業結構化的方式,不管是在一個處理器上執行還是在一千個處理器上執行,都會正確而完整地執行。任何人使用該資源都可以同樣完成,系統已經為你做好了一切。如果你思考這個問題,它很微不足道。但實際上,在大多數語言中,如果沒有併發性,很難簡潔地編寫。併發性使得做這種事情,可以非常緊湊。

更重要的是,它是隱式並行性的(儘管不是,如果你不想,可以不必考慮該問題),它也能很好地擴充套件。沒有同步或不同步。worker 數量可能是一個巨大的數字,而且它仍然可以高效地工作,因此併發工具使得為較大問題構建此類解決方案變得很容易。

還要注意,沒有鎖了,沒有互斥鎖了,所有這些都是在考慮舊的併發模型時需要考慮的,新模型沒有了,你看不到它們的存在。然而,一個正確的無鎖的併發、並行演演算法,一定很好,對吧?

但這太容易了,我們有時間看一個更難的。

Load balancer

此例子有點棘手,相同的基本概念,但做的事情更符合現實。假設我們要寫一個 Loader Balancer,有一堆 Requester 生成實際的工作,有一堆工作任務。希望將所有這些 Requester 的工作負載分配給任意數量的 Worker,並使其達到某種負載平衡,所以工作會分配給負荷最少的Worker。 所以你可以認為 Worker 們一次可能有大量的工作要做。他們可能同時要做的不止一個,可能有很多。因為有很多請求在處理,所以這會是一個很忙碌的系統。正如我演示的一樣,它們也許是在同一臺機器上。您也可以想象,其中一些線代表了正在進行適當負載均衡的網路連線,從結構上講,我們的設計仍然是安全的。

Request 現在看起來很不一樣了。有一個任意數量函式的閉包,表示我們要做的計算;有一個 channel 可以返回結果。請注意,不像其他一些類似 Erlang 的語言,在 Go 中 channel 是 Reuqest 的一部分,channel 的概念就在那裡,它是語言中 first-class 的東西,使得可以到處傳遞 channel。它在某種意義上類似於檔案描述符,持有 channel 的物件就可以和其他物件通訊,但沒有 channel 的物件是做不到的。就好像打電話給別人,或者通過檔案描述符傳遞檔案一樣,是一個相當有影響的概念。想法是,要傳送一個需要計算的請求,它包含一個計算完成返回結果的 channel。

以下是一個虛構但能起到說明作用的版本的 Requester。所做的是,有一個請求可以進入的 channel,在這個 work channel 上生成要做的要做的任務;建立了一個 channel,並放入每個請求的內部,以便返回給我們答案。做了一些工作,使用 Sleep 代表(誰知道實際上在做什麼)。你在 work channel 上傳送一個帶有用於計算的函式的請求物件,不管是什麼,我不在乎;還有一個把答案發回去的 channel;然後你在那個 channel 等待結果返回。一旦你得到結果,你可能得對結果做些什麼。這段程式碼只是按照一定速度生成工作。它只是產生結果,但是通過使用 input 和 output channel 通訊來實現的。

然後是 Worker,在前面的頁面,記得麼?有一些 Requester,右邊的是Worker,它被提供給 balancer,是我最後要給你看的。Worker 擁有一個接收請求的 channel;一個等待任務的計數,Worker 擁有任務的數量代表其負載,它註定很忙;然後是一個 index,是堆結構的一部分,我們馬上展示給你看。Worker 所做的就是從它的 Requester 那裡接收工作。Request channel 是 Worker 物件的一部分。

呼叫 Worker 的函式,把請求傳遞給它,把從 Requester 生成的實際的函式通過均衡器傳遞給 WorkerWorker 計算答案,然後在 channel 上返回答案。請注意,與許多其他負載均衡架構不同,從 Worker 返回給 Requester 的 channel 不通過 Loader Balancer。一旦 RequesterWorker 建立連線,圖中的“中介”就會消失,請求上的工作直接進行通訊。因為在系統執行時,系統內部可以來回傳遞 channel。如果願意,也可以在裡面放一個 goroutine,在這裡放一個 go 語句,然後在 Worker 上並行地處理所有的請求。如果這樣做的話,一樣會工作的很好,但這已經足夠了。

Balancer 有點神奇,你需要一個 Workerpool; 需要一些 Balancer 物件,以繫結一些方法到 BalancerBalancer 包含一個 pool;一個 done channel,用以讓 Worker 告訴 Loader Balancer 它已經完成了最近的計算。

所以 balance 很簡單,它所做的只是永遠執行一個 select 語句,等待做更多來自 Requester 的工作。在這種情況下,它會分發請求給負載最輕的 Worker;或者 Worker 告知,它已經完成計算,在這種情況下,可以通過更新資料結構表明 Worker 完成了它的任務。所以這只是一個簡單的兩路 select。然後,我們需要增加這兩個函式,而要做到這一點,實際上要做的就是構造一個堆。

我跳過這些令人很興奮的片段,你們已經知道什麼意思。

Dispatch,dispatch 要做的就是找到負載最少的 Worker,它是基於堆實現的一個標準優先順序佇列。所以你把負載最少的 Worker 從堆裡取出來,通過將請求寫入 request channel 來傳送任務。因為增加了一個任務,需要增加負載,這會影響負載分佈。然後你把它放回堆的同一個地方,就這樣。你剛剛排程了它,並且在結構上進行了更新,這就是可執行程式碼行的內容。

然後是 complete 的任務,也就是工作完成後,必須做一些相反的事情。 Worker 的佇列中減少了一個任務,所以減少了它的等待計數。從堆裡彈出 Worker,然後把它放回堆中,優先順序佇列會把它放回中它所屬的位置,這是一個半現實的 Loader Balancer 的完整實現。此處的關鍵點是資料結構使用的是 channel 和 goroutine 來構造併發的東西。

Lesson

結果是可伸縮的,是正確的,很簡單,沒有顯式的鎖,而架構讓它得以實現。因此,併發性使此例子的內在並行性成為可能。你可以執行這個程式,我有這個程式,它是可編譯、可執行的,而且工作正常,負載均衡也做得很好。物體保持在均勻的負載下,按照模組量化,很不錯。我從來沒說過有多少 Worker,有多少問題。可能每個都有一個,另一個有數10個;或者每個都有一千,或者每個都有一百萬,擴縮容仍然有效,並且仍然高效。

One more example

再舉一個例子,這個例子有點令人驚訝,但它適合一張幻燈片就可以完成。

想象一下如何複製資料庫,你得到了幾個資料庫,每個資料庫中有相同的資料,谷歌稱之為分片,稱呼相同的例項。您要做的是向所有資料庫傳遞一個請求,一個查詢,並返回結果。結果會是一樣的,你選擇第一個應答請求來加快速度,因為首先要回來的是你想要的答案。如果其中一個壞了,斷開了或者什麼的,你不在乎。因為會有其他響應回來,這就是如何做到這一點。這就是它的全部實現。您有一些連線陣列和一些要執行的查詢,您建立一個 channel,該 channel 緩衝查詢資料庫中的元素數、副本內的副本數大小的內容,然後您只需在資料庫的所有連線上執行。對於其中的每一個,您啟動一個 goroutine 以將查詢傳遞到該資料庫,然後獲取答案。但是通過這個 DoQuery 呼叫,將答案傳遞到唯一的 channel,這個 channel 儲存所有請求的結果。然後,在你執行之後,所有的 goroutine 都只需在底部這行等待。我們等待第一個回到 channel 的請求,就是你想要的答案。返回它,就完成了。這看起來像個玩具,而且有點像。但這實際上是一個完全正確的實現,唯一缺少的是乾淨的回收。你想告訴那些還沒回來的伺服器關閉。當你已經得到答案,不再需要它們。你可以做,增加更多且合理的程式碼,但那就不適合放在幻燈片上了。所以我只想告訴你,在很多系統中,這是一個相當複雜的問題,但在這裡,它只是自然地脫離了架構。因為你已經有了併發工具來表示一個相當大的複雜的分散式問題,它執行得非常好。

Conclusion

還剩五秒鐘,很好。結論:併發性是強大的,但它不是並行性的,但它支援並行性,而且它使並行性變得容易。如果你明白了,那我就完成了我的工作。

For more information

如果你想看更多,這裡有很多連結。golang.org 有關於 GO 你想知道的一切。有一份很好的歷史 paper,連結如上。幾年前我做了一個演講,讓我們真正開始開發Go語言,你可能會覺得很有趣。CMU 的 Bob Harper 有一篇非常不錯的部落格文章,叫做“並行不是併發”,這與“併發不是並行”的觀點非常相似,雖然不完全一樣。還有一些其他的東西,最令人驚訝的是,道格·馬圖爾(Doug Mathur),我在貝爾實驗室(Bell Labs)的老闆,所做的並行冪級數的工作,這是一篇了不起的論文。但如果你想與眾不同的話。幻燈片上的最後一個連結是到另一種語言 sawzall,我從貝爾實驗室(Bell Labs)來到谷歌後不久做的,這很了不起,因為它是不可思議的並行的語言,但它絕對沒有併發性。現在我想你可能明白了這是可能的,所以非常感謝你的傾聽和感謝 Hiroko 給我寫信。我想是時候喝點什麼了。

視訊:vimeo.com/49718712
Slide:talks.golang.org/2012/waza.s…
原始碼:github.com/golang/talk…