譯 | Concurrency is not Parallelism

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

目錄

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,你知道這些設計本質上是安全的,由於你是那樣作的。可是,這無疑是一個愚蠢的問題,與實際工做無關。嗯,事實上確實有關。git

由於若是你拿到這個問題,把書堆換成一些網絡內容;把 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…程序員

相關文章
相關標籤/搜索