Go:マルチスレッドと並列処理

私は囲loveが大好きで、それを賞賛するのが大好きです(たまたまスラングをだます)、それについての記事が大好きです。 「 Go:2 years in production 」という記事を読んで、コメントします。 それはハブで明らかになりました-楽観主義者! 彼らは最高を信じたい。



デフォルトでは、Goはシェダーと非同期呼び出しを使用して単一のスレッドで実行されます。 (プログラマーはマルチスレッドと並列処理の感覚を持っています。)この場合、チャネルは非常に高速に動作します。 ただし、Goに2つ以上のスレッドを使用するように指示すると、Goがロックの使用を開始し、チャネルのパフォーマンスが低下する場合があります。 チャンネルの使用を制限したくありません。 さらに、ほとんどのサードパーティライブラリはあらゆる機会にチャネルを使用します。 したがって、デフォルトで行われているように、Goを単一のスレッドで実行すると効果的です。



channel01.go

package main import "fmt" import "time" import "runtime" func main() { numcpu := runtime.NumCPU() fmt.Println("NumCPU", numcpu) //runtime.GOMAXPROCS(numcpu) runtime.GOMAXPROCS(1) ch1 := make(chan int) ch2 := make(chan float64) go func() { for i := 0; i < 1000000; i++ { ch1 <- i } ch1 <- -1 ch2 <- 0.0 }() go func() { total := 0.0 for { t1 := time.Now().UnixNano() for i := 0; i < 100000; i++ { m := <-ch1 if m == -1 { ch2 <- total } } t2 := time.Now().UnixNano() dt := float64(t2 - t1) / 1000000.0 total += dt fmt.Println(dt) } }() fmt.Println("Total:", <-ch2, <-ch2) }
      
      







 users-iMac:channel user$ go run channel01.go NumCPU 4 23.901 24.189 23.957 24.072 24.001 23.807 24.039 23.854 23.798 24.1 Total: 239.718 0
      
      







行をコメントアウトして、すべてのカーネルをアクティブにしましょう。



  runtime.GOMAXPROCS(numcpu) //runtime.GOMAXPROCS(1)
      
      







 users-iMac:channel user$ go run channel01.go NumCPU 4 543.092 534.985 535.799 533.039 538.806 533.315 536.501 533.261 537.73 532.585 Total: 5359.113 0
      
      







20倍遅い? キャッチは何ですか? デフォルトのチャネルサイズは1です。



  ch1 := make(chan int)
      
      







100を入れます。



  ch1 := make(chan int, 100)
      
      







結果1ストリーム

 users-iMac:channel user$ go run channel01.go NumCPU 4 9.704 9.618 9.178 9.84 9.869 9.461 9.802 9.743 9.877 9.756 Total: 0 96.848
      
      







4ストリーム結果

 users-iMac:channel user$ go run channel01.go NumCPU 4 17.046 17.046 16.71 16.315 16.542 16.643 17.69 16.387 17.162 15.232 Total: 0 166.77300000000002
      
      







遅いのは2倍だけですが、常に使用できるとは限りません。



チャネルチャネルの例




 package main import "fmt" import "time" import "runtime" func main() { numcpu := runtime.NumCPU() fmt.Println("NumCPU", numcpu) //runtime.GOMAXPROCS(numcpu) runtime.GOMAXPROCS(1) ch1 := make(chan chan int, 100) ch2 := make(chan float64, 1) go func() { t1 := time.Now().UnixNano() for i := 0; i < 1000000; i++ { ch := make(chan int, 100) ch1 <- ch <- ch } t2 := time.Now().UnixNano() dt := float64(t2 - t1) / 1000000.0 fmt.Println(dt) ch2 <- 0.0 }() go func() { for i := 0; i < 1000000; i++ { ch := <-ch1 ch <- i } ch2 <- 0.0 }() <-ch2 <-ch2 }
      
      







結果1ストリーム

 users-iMac:channel user$ go run channel03.go NumCPU 4 1041.489
      
      





4ストリーム結果

 users-iMac:channel user$ go run channel03.go NumCPU 4 11170.616
      
      





したがって、8つのコアがあり、Goでサーバーを作成する場合、Goだけに依存してプログラムを並列化することはできません。また、8つのシングルスレッドプロセスを開始することもできます。その前に、Goでも記述できるバランサーが必要です。 本番環境にサーバーがあり、シングルコアサーバーから4倍に切り替えたときに、処理が10%少なくなりました。



これらの数字はどういう意味ですか? 1つのコンテキストで毎秒3000リクエストを処理するタスクに直面しました(たとえば、各リクエストに連続して番号を付けます:1、2、3、4、5 ...もう少し複雑かもしれません)、毎秒3000リクエストのパフォーマンスは主にチャネルによって制限されます。 スレッドとコアを追加しても、パフォーマンスは期待したほど熱心に向上しません。 Goの1秒あたり3000リクエストは、最新の機器の特定の制限です。



ナイトアップデート:最適化の方法





Go:2 Years in Production 」という記事からのコメントは、この記事を書くように促しましたが、このコメントは最初のコメントを上回りました。



サイバーグラインドプロテクターは、次の最適化を提案しました。 8人の他のhabrazhitelamiに既に気に入っています。 彼らがコードを読んだのか、それともダイバーであり、直感的にすべてを行うのかはわかりませんが、説明します。 したがって、この記事はより完全で有益なものになります。

コードは次のとおりです。



 package main import "fmt" import "time" import "runtime" func main() { numcpu := runtime.NumCPU() fmt.Println("NumCPU", numcpu) //runtime.GOMAXPROCS(numcpu) runtime.GOMAXPROCS(1) ch3 := make(chan int) ch1 := make(chan int, 1000000) ch2 := make(chan float64) go func() { for i := 0; i < 1000000; i++ { ch1 <- i } ch3 <- 1 ch1 <- -1 ch2 <- 0.0 }() go func() { fmt.Println("TT", <-ch3) total := 0.0 for { t1 := time.Now().UnixNano() for i := 0; i < 100000; i++ { m := <-ch1 if m == -1 { ch2 <- total } } t2 := time.Now().UnixNano() dt := float64(t2 - t1) / 1000000.0 total += dt fmt.Println(dt) } }() fmt.Println("Total:", <-ch2, <-ch2) }
      
      







この最適化の本質は何ですか?



1.チャネルch3を追加しました。 このチャネルは、最初のゴルチンの終わりまで、2番目のゴルチンをブロックします。

2. 2番目のゴルチンはチャネルch1から読み取らないため、充填中に最初のゴルチンをブロックします。 したがって、ch1は必要な1,000,000に増加します



つまり、コードはもはや並列ではなく、順番に機能し、チャネルは配列として使用されます。 そしてもちろん、このコードは2番目のコアを使用できません。 このコードのコンテキストでは、「N倍の理想的な加速」について話すことはできません。



主なことは、そのようなコードは最初に定義されたデータ量でのみ機能し、継続的に機能することはできず、生涯にわたって情報を無期限に処理することです。



更新2:Go 1.1.2でのテスト





バッファ1でテスト番号1(channel01.go)



  ch1 := make(chan chan int, 1)
      
      







1スレッド

 go runchannel01.go NumCPU 4 66.0038 66.0038 67.0038 66.0038 67.0038 66.0038 65.0037 67.0038 67.0039 76.0043 Total: 0 673.0385000000001
      
      







4スレッド

 go run channel01.go NumCPU 4 116.0066 186.0106 112.0064 117.0067 175.01 115.0066 114.0065 148.0084 133.0076 153.0088 Total: 0 1369.0782
      
      





結論:はるかに良い。 なぜバッファ1を配置するのか想像するのは難しいですが、おそらくそのようなバッファのアプリケーションがあります。



バッファ100でテスト番号1(channel01.go)



  ch1 := make(chan chan int, 100)
      
      







1スレッド

 go run channel01.go NumCPU 4 16.0009 17.001 16.0009 16.0009 16.0009 16.0009 17.001 16.0009 17.001 16.0009 Total: 0 163.00930000000002
      
      







4スレッド

 go runchannel01.go NumCPU 4 66.0038 66.0038 67.0038 66.0038 67.0038 66.0038 65.0037 67.0038 67.0039 76.0043 Total: 0 673.0385000000001
      
      





結論:バージョン1.0.2より2倍悪い



テスト番号2(channel03.go)



1スレッド

 go run channel03.go NumCPU 4 1568.0897
      
      







4スレッド

 go run channel03.go NumCPU 4 12119.6932
      
      







バージョン1.0.2とほぼ同じですが、わずかに優れています。 1:8対1:10



All Articles