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

Source: https://habr.com/ru/post/J195574/


All Articles