<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                企業??AI智能體構建引擎,智能編排和調試,一鍵部署,支持知識庫和私有化部署方案 廣告
                Goroutine是Go中最基本的組織單位之一,所以了解它是什么以及它如何工作是非常重要的。事實上,每個Go程序至少擁有一個:main gotoutine,當程序開始時會自動創建并啟動。在幾乎所有Go程序中,你都可能會發現自己遲早加入到一個gotoutine中,以幫助自己解決問題。那么它到底是什么? 簡單來說,gotoutine是一個并發的函數(記住:不一定是并行)和其他代碼一起運行。你可以簡單的通過將go關鍵字放在函數前面來啟動它: ``` func main() { go sayHello() // continue doing other things } func sayHello() { fmt.Println("hello") } ``` 對于匿名函數,同樣也能這么干,從下面這個例子你可以看得很明白。在下面的例子中,我們不是從一個函數建立一個goroutine,而是從一個匿名函數創建一個goroutine: ``` go func() { fmt.Println("hello") }()// 1 // continue doing other things ``` 1. 注意這里的(),我們必須立刻調用匿名函數來使go關鍵字有效。 或者,你可以將函數分配給一個變量,并像這樣調用它: ``` sayHello := func() { fmt.Println("hello") } go sayHello() // continue doing other things ``` 看起來很簡單,對吧。我們可以用一個函數和一個關鍵字創建一個并發邏輯塊,這就是啟動goroutine所需要知道的全部。當然,關于如何正確使用它,對它進行同步以及如何組織它還有很多需要說明的內容。本章接下來的部分會深入介紹goroutine及它是如何工作的。如果你只想編寫一些可以在goroutine中正確運行的代碼,那么可以考慮直接跳到下一章。 那么讓我們來看看發生在幕后的事情:goroutine實際上是如何工作的? 是OS線程嗎? 綠色線程? 我們可以創建多少個? Goroutines對Go來說是獨一無二的(盡管其他一些語言有類似的并發原語)。它們不是操作系統線程,它們不完全是綠色的線程(由語言運行時管理的線程),它們是更高級別的抽象,被稱為協程(coroutines)。協程是非搶占的并發子程序,也就是說,它們不能被中斷。 Go的獨特之處在于goutine與Go的運行時深度整合。Goroutine沒有定義自己的暫停或再入點; Go的運行時觀察著goroutine的行為,并在阻塞時自動掛起它們,然后在它們變暢通時恢復它們。在某種程度上,這使得它們可以搶占,但只是在goroutine被阻止的地方。它是運行時和goroutine邏輯之間的一種優雅合作關系。 因此,goroutine可以被認為是一種特殊的協程。 協程,因此可以被認為是goroutine的隱式并發構造,但并發并非協程自帶的屬性:某些東西必須能夠同時托管幾個協程,并給每個協程執行的機會,否則它們無法實現并發。當然,有可能有幾個協程按順序執行,但看起來就像并行一樣,在Go中這樣的情況比較常見。 Go的宿主機制實現了所謂的M:N調度器,這意味著它將M個綠色線程映射到N個系統線程。 Goroutines隨后被安排在綠色線程上。 當我們擁有比綠色線程更多的goroutine時,調度程序處理可用線程間goroutines的分布,并確保當這些goroutine被阻塞時,可以運行其他goroutines。我們將在第六章討論所有這些機制是如何工作的,但在這里我們將介紹Go如何對并發進行建模。 Go遵循稱為fork-join模型的并發模型.fork這個詞指的是在程序中的任何一點,它都可以將一個子執行的分支分離出來,以便與其父代同時運行。join這個詞指的是這樣一個事實,即在將來的某個時候,這些并發的執行分支將重新組合在一起。子分支重新加入的地方稱為連接點。這里有一個圖形表示來幫助你理解它: :-: ![](https://box.kancloud.cn/2b791a7e9db2c667d1d86337b4fe9171_625x559.png) go關鍵字為Go程序實現了fork,fork的執行者是goroutine,讓我們回到之前的例子: ``` sayHello := func() { fmt.Println("hello") } go sayHello() // continue doing other things ``` sayHello函數會在屬于它的goroutine上運行,與此同時程序的其他部分繼續執行。在這個例子中,沒有連接點。執行sayHello的goroutine將在未來某個不確定的時間退出,并且該程序的其余部分將繼續執行。 然而,這個例子存在一個問題:我們不確定sayHello函數是否可以運行。goroutine將被創建并交由Go的運行時安排執行,但在main goroutine退出前它實際上可能沒有機會運行。 事實上,由于我們為了簡單而省略了其他主要功能部分,所以當我們運行這個小例子時,幾乎可以肯定的是,程序將在主辦sayHello調用的goroutine開始之前完成執行。 因此,你不會看到打印到標準輸出的單詞“hello”。 你可以在創建goroutine之后為main goroutine添加一段休眠時間,但請記住,這實際上并不創建一個連接點,只是一個競爭條件。如果你記得第一章,你會增加退出前goroutine將運行的可能性,但你無法保證它。加入連接點是確保程序正確性并消除競爭條件的保證。 為了創建一個連接點,你必須同步main goroutine和sayHello goroutine。 這可以通過多種方式完成,但我將使用sync包中提供的一個解決方案:sync.WaitGroup。現在了解這個示例如何創建一個連接點并不重要,只是需要清楚它在兩個goroutine之間創建了一個連接點。 這是我們的示例版本: ``` var wg sync.WaitGroup sayHello := func() { defer wg.Done() fmt.Println("hello") } wg.Add(1) go sayHello() wg.Wait() //1 ``` 1. 在這里加入連接點。 這會輸出: ``` hello ``` 這個例子明確的阻塞了main goroutine,直到承載sayHello函數的main goroutine終止。你將在隨后的sync包章節了解到更詳細的內容。 我們在示例中使用了匿名函數。讓我們把注意力轉移到閉包。閉包圍繞它們創建的詞法范圍,從而捕捉變量。如果在goroutine中使用閉包,閉包是否在這些變量或原始引用的副本上運行?讓我們試試看: ``` var wg sync.WaitGroup salutation := "hello" wg.Add(1) go func() { defer wg.Done() salutation = "welcome" // 1 }() wg.Wait() fmt.Println(salutation) ``` 你認為salutation的值是"hello"還是"welcome"?運行后會看到: ``` wlecome ``` 有趣!事實證明,goroutine在它創建的同一地址空間內執行,因此我們的程序打印出“welcome”。讓我們再來嘗試一個例子。 你認為這個程序會輸出什么? ``` var wg sync.WaitGroup for _, salutation := range []string{"hello", "greetings", "good day"} { wg.Add(1) go func() { defer wg.Done() fmt.Println(salutation) // 1 }() } wg.Wait() ``` 1. 這里我們測試打印字符串切片創建的循環變量salutation。 答案比大多數人所預期的不同,而且是Go中為數不多的令人驚訝的事情之一。 大多數人直覺上認為這會以某種不確定的順序打印出“hello”,”greeting”和“good day”,但實際上: ``` good day good day good day ``` 這有點令人驚訝。讓我們來看看這里發生了什么。 在這個例子中,goroutine正在運行一個已經關閉迭代變量salutation的閉包,它有一個字符串類型。 當我們的循環迭代時,salutation被分配給切片中的下一個字符串值。 由于運行時調度器安排的goroutine可能會在將來的任何時間點運行,因此不確定在goroutine內將打印哪些值。 在我的機器上,在goroutines開始之前,循環很可能會退出。 這意味著salutation變量超出了范圍。 然后會發生什么? goroutines仍然可以引用已經超出范圍的東西嗎? 這個goroutine會訪問可能已經被回收的內存嗎? 這是關于Go如何管理內存的一個有趣的側面說明。Go運行時足夠敏銳地知道對salutation變量的引用仍然保留,因此會將內存傳輸到堆中,以便goroutine可以繼續訪問它。 在這個例子中,循環在任何goroutines開始運行之前退出,所以salutation轉移到堆中,并保存對字符串切片“good day”中最后一個值的引用。所以會看到“good day”打印三次 。 編寫該循環的正確方法是將salutation的副本傳遞給閉包,以便在運行goroutine時,它將對來自其循環迭代的數據進行操作: ``` var wg sync.WaitGroup for _, salutation := range []string{"hello", "greetings", "good day"} { wg.Add(1) go func(salutation string) { // 1 defer wg.Done() fmt.Println(salutation) }(salutation) // 2 } wg.Wait() ``` 1. 在這里我們聲明了一個參數,和其他的函數看起來差不多。我們將原始的salutation變量映射到更加明顯的位置。 2. 在這里,我們將當前迭代的變量傳遞給閉包。 一個字符串的副本被創建,從而確保當goroutine運行時,我們引用正確的字符串。 正如我們所看到的,我們得到的輸出看起來沒那么奇怪了: ``` good day hello greetings ``` 這個例子的行為和我們預期的一樣,只是稍微更冗長。多運行幾次,輸出順序可能不同。 goroutine在相同的地址空間內運行,Go的編譯器很好地處理了內存中的固定變量,因此goroutine不會意外地訪問釋放的內存,這允許開發人員專注于他們的問題而不是內存管理。 由于多個goroutine可以在相同的地址空間上運行,我們仍然需要擔心同步問題。正如我們已經討論過的,可以選擇同步訪問共享內存的例程訪問,也可以使用CSP原語通過通信共享內存。 goroutines的另一個好處是它們非常輕巧。 這是官方FAQ的摘錄: >新建立一個goroutine有幾千字節,這樣的大小幾乎總是夠用的。 如果出現不夠用的情況,運行時會自動增加(并縮小)用于存儲堆棧的內存,從而允許許多goroutine存在適量的內存中。CPU開銷平均每個函數調用大約三個廉價指令。 在相同的地址空間中創建數十萬個goroutines是可以的。如果goroutines只是執行等同于線程的任務,那么系統資源的占用會更小。 每個goroutine幾kb,那根本不是個事兒。讓我們來親手試著確認下。在此之前,我們必須了解一個關于goroutine的有趣的事:垃圾收集器不會收集以下形式的goroutines。如果我寫出以下代碼: ``` go func() { // <操作會在這里永久阻塞> }() // Do work ``` 這個goroutine將一直存在,直到整個程序退出。我們會在第四章的“防止Goroutine泄漏”中詳細的聊一聊這個話題。 接下來,讓我們回來看看該怎么寫個例子來衡量一個goroutine的實際大小。 我們將goroutine不被垃圾收集的事實與運行時的自省能力結合起來,并測量在goroutine創建之前和之后分配的內存量: ``` memConsumed := func() uint64 { runtime.GC() var s runtime.MemStats runtime.ReadMemStats(&s) return s.Sys } var c <-chan interface{} var wg sync.WaitGroup noop := func() { wg.Done(); <-c } // 1 const numGoroutines = 1e4 // 2 wg.Add(numGoroutines) before := memConsumed() // 3 for i := numGoroutines; i > 0; i-- { go noop() } wg.Wait() after := memConsumed() // 4 fmt.Printf("%.3fkb", float64(after-before)/numGoroutines/1000) ``` 1. 我們需要一個永不退出的goroutine,以便我們可以將它們中的一部分保存在內存中進行測量。 不要擔心我們目前如何實現這一目標。 只知道這個goroutine不會退出,直到這個過程結束。 2. 這里我們定義要創建的goroutines的數量。 我們將使用大數定律漸近地逼近一個goroutine的大小。 3. 這里測量創建分區之前所消耗的內存量。 4. 這里測量創建goroutines后消耗的內存量。 在控制臺會輸出: ``` 2.817kb ``` ***2017年7月這本書出版,go1.9發布于2017年8月24日,那么假設作者用的是當時最新的1.8版。譯者用windows系統,go 1.10.1版,這個數字在8.908kb~9.186kb上下浮動。在centos6.4上測試,這個數字為2.748kb。*** 看起來文檔是正確的。這個例子雖然有些理想化,但仍然讓我們了解可能創建多少個goroutines有了大致的了解。 在我的筆記本上,我有8G內存,這意味著理論上我可以支持數百萬的goroutines。當然,這忽略了在電腦上運行的其他東西。但這個快速估算的結果表明了goroutine是多么的輕量級。 存在一些可能會影響我們的goroutine規模的因素,例如上下文切換,即當某個并發進程承載的某些內容必須保存其狀態以切換到其他進程時。如果我們有太多的并發進程,上下文切換可能花費所有的CPU時間,并且無法完成任何實際工作。在操作系統級別,使用線程,這樣做代價可能會非常高昂。操作系統線程必須保存寄存器值,查找表和內存映射等內容,才能在操作成功后切換回當前線程。 然后它必須為傳入線程加載相同的信息。 在軟件中的上下文切換代價相對小得多。在軟件定義的調度程序下,運行時可以更具選擇性地進行持久檢索,例如如何持久化以及何時發生持續化。我們來看看操作系統線程和goroutines之間上下文切換的相對性能。 首先,我們將利用Linux內置的基準測試套件來測量在同一內核的兩個線程之間發送消息需要多長時間: ``` taskset -c 0 perf bench sched pipe -T ``` 這會輸出: ``` # Running 'sched/pipe' benchmark: # Executed 1000000 pipe operations between two threads Total time: 2.935 [sec] 2.935784 usecs/op 340624 ops/sec ``` 這個基準測量實際上是衡量在一個線程上發送和接收消息所需的時間,所以我們將把結果分成兩部分。 每個上下文切換1.467微秒。 這看起來不算太壞,但讓我們先別急著下判斷,再來比較下goroutine之間的上下文切換。 我們將使用Go構建一個類似的基準測試。下面的代碼涉及到一些尚未討論過的東西,所以如果有什么困惑的話,只需根據注釋關注結果即可。 以下示例將創建兩個goroutine并在它們之間發送消息: ``` func BenchmarkContextSwitch(b *testing.B) { var wg sync.WaitGroup begin := make(chan struct{}) c := make(chan struct{}) var token struct{} sender := func() { defer wg.Done() <-begin //1 for i := 0; i < b.N; i++ { c <- token //2 } } receiver := func() { defer wg.Done() <-begin //1 for i := 0; i < b.N; i++ { <-c //3 } } wg.Add(2) go sender() go receiver() b.StartTimer() //4 close(begin) //5 wg.Wait() } ``` 1. 這里會被阻塞,直到接受到數據。我們不希望設置和啟動goroutine影響上下文切換的度量。 2. 在這里向接收者發送數據。struct{}{}是空結構體且不占用內存;這樣我們就可以做到只測量發送信息所需要的時間。 3. 在這里,我們接收傳遞過來的數據,但不做任何事。 4. 開始啟動計時器。 5. 在這里我們通知發送和接收的goroutine啟動。 我們運行該基準測試,指定只使用一個CPU,以便與之前的Linux基準測試想比較,我們來看看結果: ``` go test -bench=. -cpu=1 /src/gos-concurrency-building-blocks/goroutines/fig-ctx-switch_test.go ``` | BenchmarkContextSwitch | 5000000 | 225ns/op | | --- | --- | --- | | PASS | | | | ok | command-line-arguments | 1.393s | 每個上下文切換225 ns,哇! 這是0.225μs,比我機器上的操作系統上下文切換快92%,如果你記得1.467μs的話。很難說有多少goroutines會導致過多的上下文切換,但我們可以很自然地說上限可能不會成為使用goroutines的障礙。 * * * * * 學識淺薄,錯誤在所難免。我是長風,歡迎來Golang中國的群(211938256)就本書提出修改意見。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看