2017/12/21

Recent entries from same category

  1. Go 言語プログラミングエッセンスという本を書きました。
  2. errors.Join が入った。
  3. unsafe.StringData、unsafe.String、unsafe.SliceData が入った。
  4. Re: Go言語で画像ファイルか確認してみる
  5. net/url に JoinPath が入った。

Go は簡単に軽量スレッドが起動できるのがウリなのだけど、その使い方が難しいと思われているきらいがある。

Goへの誤解について - GolangRdyJp

よくGoで誤解されるポイントについて個人的な見解を書いておきます。 今回の記事は Goアドベントカレンダー2017 その3 の20日目の記事です。 使ってないパッケージがコンパイルエラーって面倒じゃね...

http://golang.rdy.jp/2017/12/20/go-fact/

慣れていない間は、処理を並行化する際に「どうやったら並行化できるんだ」が分からない事があるのだと思う。

Big Sky :: golang の channel を使ったテクニックあれこれ

golang の channel は他の言語に見ない独特のパラダイムを開発者に提供します。 単純にスレッド間でメッセージングをするだけでもC言語で書けばそこそこの量になったり、慣れていない人であればど...

https://mattn.kaoriya.net/software/lang/go/20160706165757.htm

これに書いたワーカー等は、ある程度 Go に慣れていないと簡単には書けないかもしれない。ただ、実はそういった人達でも簡単に並行化できて、しかもリソースに制限を掛ける事もできる。例えば以下のコードを見て欲しい。

package main

import (
    "fmt"
    "time"
)

func doSomething(u string) {
    fmt.Println(u)
    time.Sleep(2 * time.Second)
}

func main() {
    urls := []string{
        "http://www.example.com",
        "http://www.example.net",
        "http://www.example.net/foo",
        "http://www.example.net/bar",
        "http://www.example.net/baz",
    }

    for _, u := range urls {
        doSomething(u)
    }
}

スライスを処理する単純なコードで、シーケンシャルに処理しているので遅い。教科書的に並行化するのであれば以下の様になる。

package main

import (
    "fmt"
    "sync"
    "time"
)

func doSomething(u string) {
    fmt.Println(u)
    time.Sleep(2 * time.Second)
}

func main() {
    urls := []string{
        "http://www.example.com",
        "http://www.example.net",
        "http://www.example.net/foo",
        "http://www.example.net/bar",
        "http://www.example.net/baz",
    }

    var wg sync.WaitGroup
    for _, u := range urls {
        wg.Add(1)
        go func() {
            defer wg.Done()
            doSomething(u)
        }()
    }
    wg.Wait()
}

ここで上記リンク先で示したワーカーとの違いを考えてみて欲しい。ワーカーの方は起動個数が決まっているのでリソース制限が出来ている。負荷を掛けない為に goroutine の起動個数を絞っている。一見、このコードからワーカーへの移行は難しい様に見えるが、実はこのコードから簡単にリソース制限できる。

package main

import (
    "fmt"
    "sync"
    "time"
)

func doSomething(u string) {
    fmt.Println(u)
    time.Sleep(2 * time.Second)
}

func main() {
    urls := []string{
        "http://www.example.com",
        "http://www.example.net",
        "http://www.example.net/foo",
        "http://www.example.net/bar",
        "http://www.example.net/baz",
    }

    limit := make(chan struct{}, 3)

    var wg sync.WaitGroup
    for _, u := range urls {
        wg.Add(1)
        go func() {
            limit <- struct{}{}
            defer wg.Done()
            doSomething(u)
            <-limit
        }()
        time.Sleep(200 * time.Millisecond)
    }
    wg.Wait()
}

分かりやすく200msのウエイトを入れてある。

limit というバッファ数を付けた channel を作って、処理の前で追加、処理の後で取り出すというコードで挟むだけ。limit は3個のバッファなのでこの doSomething は同時に3個までしか実行されない。注意する点としては goroutine は urls の個数分起動する事。その処理に注力したいが対象の処理(ダウンロードするファイルサーバなど)に負荷を掛けたくない場合に、既存のシーケンシャルな処理を簡単に並行化する際のテクニックである。

追記: タイトルが goroutine の実行個数と言いながら goroutine 自体はいっぱい起動するのはこれ如何にという指摘を頂きましたが、ごもっともでタイトルバグですね。ただ GOMAXPROCS 分しかアクティブな goroutine は回されないので、実用的ではあります。

Posted at by