Goチャンネルを使用する興味深い方法(翻訳)

Gary Willoughby 記事「Goチャンネルを使用する興味深い方法」を翻訳することをお勧めします。



この記事は、Goにもう少し精通している人を対象としています。



ホリネズミ



Goチャンネルを使用する興味深い方法



GopherCon 2014カンファレンスでJohn Graham Cumming Goフィードを文書化するためにこの投稿を書きました。



レポート全体を通して、Goチャンネルを使用して競合プログラミングの可能性と利点を明らかにする興味深い方法を紹介しています。 個人的に、このレポートは、複数のプロセッサコア間で同期するためのプログラムおよび新しい手法を構成するためのいくつかの新しい方法に目を開きました。



次の例は、Goでフィードを使用するためのさまざまな手法を示しています。 コードは、それらを理解するために特別に簡素化されています。 量産バージョンには使用しないでください。 たとえば、すべてのエラー処理はスキップされます。



信号



待機中のイベント



この例では、ゴルーチンが開始し、いくつかの作業を行い(この場合は5秒待機します)、チャネルを閉じます。 バッファされていないチャネルは、メッセージが到着するまで常に現在のゴルーチンの実行を停止します。 チャネルを閉じると、データを取得できなくなるため、実行を継続できることをゴルーチンに通知します。 閉じたチャンネルがゴルーチンを停止することはありません。



package main import ( "fmt" "time" ) func main() { c := make(chan bool) go func() { // ...  - time.Sleep(time.Second * 5) close(c) }() //          . <-c fmt.Println("Done") }
      
      





いくつかのゴルーチンの調整



この例では、100のゴルーチンが開始され、開始チャネルを介してデータが送信されるのを待機します(または閉じます)。 閉じられた場合、すべてのゴルーチンが開始されます。



 package main func worker(start chan bool) { <-start // ...  - } func main() { start := make(chan bool) for i := 0; i < 100; i++ { go worker(start) } close(start) // ...  worker's   }
      
      





労働者の協調的解雇



この例では、100個のゴルーチンが起動し、データがダイチャネルを介して送信されるのを待っています(または閉じています)。 閉じられた場合、すべてのゴルーチンは実行を停止します。



 package main func worker(die chan bool) { for { select { // ...  -   case case <-die: return } } } func main() { die := make(chan bool) for i := 0; i < 100; i++ { go worker(die) } //   worker'. close(die) }
      
      





労働者の終了の確認



この例では、ゴルーチンが開始され、ダイチャネルからのデータ(またはその閉鎖)を待機します。 信号が到着した場合、ゴルーチンは最終ステップを実行し、メイン関数に(同じダイチャネルを介して)完了したことを知らせる信号を送信します。



 package main func worker(die chan bool) { for { select { // ...  -   case case <-die: // ...     . die <- true return } } } func main() { die := make(chan bool) go worker(die) die <- true // ,      <-die }
      
      





状態をカプセル化する



一意のサービスID



この例では、ゴルーチンが実行されて一意の16進IDが生成されます。 各idはidチャネルを介して送信され、goroutinはチャネルからのメッセージが読み取られるまで一時停止します。 チャネルが読み取られるたびに、ゴルーチンはカウンターを増分し、その値を送信します。



 package main import "fmt" func main() { id := make(chan string) go func() { var counter int64 = 1 for { id <- fmt.Sprintf("%x", counter) counter += 1 } }() fmt.Printf("%s\n", <-id) // will be 1 fmt.Printf("%s\n", <-id) // will be 2 }
      
      





メモリの再利用



この例では、メモリバッファを再利用するためにゴルーチンが開始されます。 ギブチャネルは古いメモリバッファを受信し、リストに保存します。 この時点で、getチャネルはこれらのバッファーを使用するために配布します。 リストに使用可能なバッファがない場合、新しいバッファが1つ作成されます。



翻訳者から
簡単に言えば、メモリを積極的に再利用して、再度割り当てないようにします(ご存じのとおり、OSは非常に長い時間メモリを割り当てることができます)。 常に少なくとも1つのバッファを持つリストが使用されます。 そして、すでに使用されているバッファを同じリストに送り返します。



 package main import "container/list" func main() { give := make(chan []byte) get := make(chan []byte) go func() { q := new(list.List) for { if q.Len() == 0 { q.PushFront(make([]byte, 100)) } e := q.Front() select { case s := <-give: q.PushFront(s) case get <- e.Value.([]byte): q.Remove(e) } } }() //    buffer := <-get //   give <- buffer //    buffer = <-get }
      
      





限られたメモリの再利用



この例では、バッファリングされたチャネルがバッファストレージとして使用されます。 チャネルは、常に5つのバッファーを格納するように構成されています。 これは、チャネルがもう1つのレコードのためのスペースを持っている場合、現在のゴルーチンをブロックしないことを意味します。



Selectは、このチャネルがいっぱいの場合、このブロックへのノンブロッキングアクセスを提供します。 最初の選択では、リポジトリから取得できない場合、新しいバッファが作成されます。 ストレージにバッファを配置できない場合、2番目の選択はデフォルトで何も行いません。これにより、GCはこのバッファをクリアします。



翻訳者から
ここでも、自分で追加したいと思います。 「再利用」したいバッファの数を制限するバッファチャネルを作成します。 そして、「余分な」バッファを取得した場合、それを無視して、ガベージコレクタを処理できるようにします。 この場合、両方の関数は非ブロッキングです。つまり、誰からもデータを受信することを期待していません。



 package main func get(store chan []byte) []byte { select { case b := <-store: return b default: return make([]byte, 100) } } func give(store chan []byte, b []byte) { select { case store <- b: default: return } } func main() { //   . store := make(chan []byte, 5) //     . buffer := get(store) //     . give(store, buffer) //      . buffer = get(store) }
      
      





無チャンネル



caseステートメントでのメッセージ取得の無効化



この例では、ゴルーチンが開始され、selectを使用して2つのチャネルからメッセージを受信します。 チャネルが閉じると、nilに設定されます。 nilチャネルは常に実行をブロックするため、このケースは実行されなくなりました。 両方のチャネルがnilに設定されている場合、何も受信できないためゴルーチンを終了します。



翻訳者から
この例では、c1を閉じた後、x = falseおよびok = falseになったため、2番目の出力値はfalseです。 値cilをチャネルc1に割り当てなかった場合、無限ループでx = false、ok = falseを無限に受け取り続けます。



重要な考え。 チャネルを閉じた場合、その場合は常にデフォルト値が使用されます。 したがって、閉じたチャネルにnilを割り当てる必要があります。 その後、nilのすべてのチャンネルをチェックすることを忘れないでください。そうしないと、ゴルチンを永久にブロックできます。



 package main import "fmt" func main() { c1 := make(chan bool) c2 := make(chan bool) go func() { for { select { case x, ok := <-c1: if !ok { c1 = nil } fmt.Println(x) case x, ok := <-c2: if !ok { c2 = nil } fmt.Println(x) } if c1 == nil && c2 == nil { return } } }() c1 <- true //   case  select'a . close(c1) c2 <- true }
      
      





caseステートメントでのメッセージ送信の無効化



この例では、ゴルーチンが開始され、乱数を生成してチャネルcに送信するために使用されます。 メッセージがチャネルdに到着すると、チャネルcはnilに設定され、対応するcaseステートメントが無効になります。 無効化されたゴルーチンは、もはや乱数を生成しません。



翻訳者から
ここでは、nilをチェックしても問題はありません。そうでなければ、ゴルーチンは実行を完了しません。 最初のケースは実行されず、2番目のケースは意味を失ったため、selectでブロックされました。



コメントにエラーがあります-最後から2行目のcはnilと等しくありません。nilはローカル変数srcに対してのみ設定されています。 そして、誰も私たちのチャンネルを書いていないのでデッドロックが起こります



 package main import ( "fmt" "math/rand" ) func main() { c := make(chan int) d := make(chan bool) go func(src chan int) { for { select { case src <- rand.Intn(100): case <-d: src = nil } } }(c) //    . fmt.Printf("%d\n", <-c) fmt.Printf("%d\n", <-c) //    . d <- true //   ,    c  nil. fmt.Printf("%d\n", <-c) }
      
      





タイマー



タイムアウト



この例では、ゴルーチンは何らかの作業を開始します。 タイムアウトチャネルは、選択に時間がかかりすぎる場合に備えて設計されています。 この場合、ゴルーチンは30秒待機して終了します。 select'aの繰り返しごとにタイムアウトが再作成され、正常に完了したことが確認されます。 後続の各反復では、タイムアウトがリセットされます。



翻訳者から
コードに5秒、そして記事30に小さな誤字があります。


 package main import "time" func worker() { for { timeout := time.After(5 * time.Second) select { // ...  - case <-timeout: //      . return } } } func main() { go worker() }
      
      





ハートビート



この例では、ゴルーチンは何らかの作業を開始します。 ハートビートチャネルは、一定の間隔でcaseステートメントを実行するように設計されています。 ハートビートチャネルは各反復でリセットされないため、caseステートメントは常に時間通りに実行されます。



 package main import "time" func worker() { heartbeat := time.Tick(30 * time.Second) for { select { // ...  - case <-heartbeat: // ...  -   } } } func main() { go worker() }
      
      







ネットワークマルチプレクサ



この例は、単純なネットワークマルチプレクサーを示しています。 私たちのメインゴルチンでは、送信されたメッセージを処理するためのチャネルが作成され、ネットワーク接続が確立されます。 次に、100個のゴルーチンが起動され、行(メッセージのように振る舞う)が生成され、このチャネルを介して送信されます。 各メッセージは、無限ループのチャネルから読み取られ、ネットワーク接続に送信されます。



この例は開始されません(テストドメインに接続しようとしているため)が、単一のネットワーク接続にメッセージを送信する多くの非同期プロセスを簡単に開始できることを示しています。



 package main import "net" func worker(messages chan string) { for { var msg string // ...   messages <- msg } } func main() { messages := make(chan string) conn, _ := net.Dial("tcp", "example.com") for i := 0; i < 100; i++ { go worker(messages) } for { msg := <-messages conn.Write([]byte(msg)) } }
      
      





最初の答え



この例では、配列の各URLが個別のゴルーチンに渡されます。 各ゴルーチンは非同期に実行され、渡されたURLを要求します。 要求に対する各応答は最初のチャネルに送信されます。これにより、当然、最初に受信した応答が最初にチャネルに送られます。 その後、チャネルからこの応答を読み取り、それに応じて処理できます。



翻訳者から
例は再び機能しません(未使用の変数rを誓います)。 同様の例はyoutube.comです。 このビデオ全体を見ることをお勧めします。



 package main import "net/http" type response struct { resp *http.Response url string } func get(url string, r chan response) { if resp, err := http.Get(url); err == nil { r <- response{resp, url} } } func main() { first := make(chan response) for _, url := range []string{"http://code.jquery.com/jquery-1.9.1.min.js", "http://cdnjs.cloudflare.com/ajax/libs/jquery/1.9.1/jquery.min.js", "http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js", "http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.min.js"} { go get(url, first) } r := <-first // ...  - }
      
      





応答チャネル伝送



この例では、タスクをゴルーチンに転送するためにチャネルwが作成されます。 Gorutinはタスクを受け取り、それに含まれるURLにリクエストを行います。 タスクの一部として、respチャネルもゴルーチンに入ります。 要求が完了するとすぐに、応答が応答チャネルを介して送り返されます。 これにより、このゴルーチンはタスクを処理し、個々のタスクごとに設定された異なるチャネルを介して結果を送り返すことができます。



翻訳者から
簡単であれば、タスクごとに、ゴルーチンが結果を送信するための回答用の独自のチャネルを作成します。



 package main import "net/http" type work struct { url string resp chan *http.Response } func getter(w chan work) { for { do := <-w resp, _ := http.Get(do.url) do.resp <- resp } } func main() { w := make(chan work) go getter(w) resp := make(chan *http.Response) w <- work{"http://cdnjs.cloudflare.com/jquery/1.9.1/jquery.min.js", resp} r := <-resp // ...  - }
      
      





HTTPロードバランサー



この例では、前の例に基づいてロードバランサーを作成します。 それは標準入力から読み込まれたURLを処理し、各起動ごとに処理のためのgorutinを処理します。 各リクエストはロードバランサーを通過し、限られた数のワーカーに対してこれらのタスクをフィルタリングします。 これらのワーカーはリクエストを処理し、結果を単一の回答チャンネルに返します。



このようなロードバランサーを使用すると、膨大な数のリクエストを送信し、利用可能なすべてのリソースにリクエストを分散し、それらを整然と処理できます。



 package main import ( "fmt" "net/http" ) type job struct { url string resp chan *http.Response } type worker struct { jobs chan *job count int } func (w *worker) getter(done chan *worker) { for { j := <-w.jobs resp, _ := http.Get(j.url) j.resp <- resp done <- w } } func get(jobs chan *job, url string, answer chan string) { resp := make(chan *http.Response) jobs <- &job{url, resp} r := <-resp answer <- r.Request.URL.String() } func balancer(count int, depth int) chan *job { jobs := make(chan *job) done := make(chan *worker) workers := make([]*worker, count) for i := 0; i < count; i++ { workers[i] = &worker{make(chan *job, depth), 0} go workers[i].getter(done) } go func() { for { var free *worker min := depth for _, w := range workers { if w.count < min { free = w min = w.count } } var jobsource chan *job if free != nil { jobsource = jobs } select { case j := <-jobsource: free.jobs <- j free.count++ case w := <-done: w.count-- } } }() return jobs } func main() { jobs := balancer(10, 10) answer := make(chan string) for { var url string if _, err := fmt.Scanln(&url); err != nil { break } go get(jobs, url, answer) } for u := range answer { fmt.Printf("%s\n", u) } }
      
      





おわりに



Goは、私の観点からは問題のある言語ですが、学習して使用する準備ができている言語です。 このプレゼンテーションのアイデアは私に新しい概念をもたらし、その後、Goの優れた競争力サポートを活用する新しいプロジェクトを開始したいと考えました。 彼女はまた、Goのような言語が提供する標準ライブラリを読んで理解し、その言語の性質と設計上の決定をよりよく理解する必要があることを強調しました。



All Articles