SYM's Knowledge Index & Creation Records

これは Blog ではない. Tech Knowledge Stack and Index である. by SYM@孤軍奮闘する IT Engineer.

Knowledge Stack & Index (全記事一覧)

本ページは投稿記事一覧です。 (自動更新)

※ 旧ページ:SYM's Knowledge Index 徐々に移行予定

自作ツール(Githubリンク) により本ブログへの投稿/更新はほぼ自動化

golang 並列処理

golang 並列処理

Goの魅力

  • 他の言語は後から並列処理の機構を組み込むと大手術になることがあるが、Goは容易
  • 高水準のパフォーマンスが出るコードを少ない手間で実現できるところ
  • I/Oコストが高い領域はGoとの相性が良い

Goの業務アプリケーションで並列処理の適用を検討すべき場面は、1リクエスト/バッチタスクの内部を高速化したい時。(例:1リクエスト中で複数データストアから情報取得し、結果を複数箇所に格納が必要な時)

Gorutine

  • 個々のgoroutineは識別不可
  • 優先度や親子関係はない
  • 外部から終了させられない
  • 終了検知には別の仕組みが必要(channel?)
  • かなり少ない量のメモリしか要求せず、起動は高速
    • 起動コストはゼロではない

goroutineの乱用は避ける

  • 並列処理は複雑さと高める
  • goroutineを駆使したコードは意図が伝わりにくい
  • 基本的には標準/準標準パッケージ機能の利用を検討
func main() {
    go output("goroutine")

    go func(msg string) {
        fmt.Println(msg)
    }("immediate execution")
}

Channel

  • 同時実行するgoroutineを接続するパイプ(複数のgoroutineから送受信しても安全が保障されたキュー)
  • Channelは同期の手段
    • Channelはgoroutineをブロックする
    • 送信goroutineと受信goroutineが揃うまでブロック(バッファなしの場合)
    • 送信側のバッファ一杯になると受信側が取りに来るまで or バッファが空ならブロック(バッファありの場合)

※ブロック=待ち受け

func main() {
    msgs := make(chan string, 3)
    msgs <- "main"
    go func() {
        msgs <- "func1"
    }()
    go func() {
        msgs <- "func2"
    }()

    msg1 := <-msgs  // channelから値を読み込むまでメインgoroutineストップ
    msg2 := <-msgs
    msg3 := <-msgs
    fmt.Println(msg1, msg2, msg3)  // main func2 func1
}
  • 一方向チャネル型
    • チャネルの向きを指定できる。向き:送信/受信
func send(recvCh chan<- string, msg string) {
    recvCh <- msg
}

func receive(sendCh <-chan string, recvCh chan<- string) {
    msg := <-sendCh
    recvCh <- msg
}

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    // send(ch1, "sending")  deadlock..
    // receive(ch1, ch2)
    go send(ch1, "sending")
    go receive(ch1, ch2)
    fmt.Println(<-ch2)
}
  • キャパシティが一杯のチャネルに書き込もうとするゴルーチンは、チャネルの空きが出るまで待機する
  • 空のチャネルから読み込もうとするゴルーチンは、チャネルに要素が入ってくるまで待機する
func main() {
    // send slow
    ch1 := make(chan string, 2)
    go func() {
        for i := 0; i < 6; i++ {
            send := "send" + strconv.Itoa(i)
            ch1 <- send
            time.Sleep(1 * time.Second)
        }
    }()
    go func() {
        for j := 0; j < 3; j++ {
            fmt.Println("sub:", j, <-ch1)
        }
    }()
    for j := 0; j < 3; j++ {
        fmt.Println("main:", j, <-ch1)
    }
    time.Sleep(3 * time.Second)
}
//main: 0 send0
//sub: 0 send1
//main: 1 send2
//sub: 1 send3
//main: 2 send4
//sub: 2 send5

Select

複数チャネルの待ち受けかつチャネル毎の制御ができる。

  • 複数goroutineの待ち受け可
  • 先に終わったものから捌く
func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    // ch3 := make(chan string)
    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "one"
    }()
    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "two"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println("received1", msg1)
        case msg2 := <-ch2:
            fmt.Println("received2", msg2)
        // default: 
        // fmt.Println("none")
      // default句を入れると待ち受けが起こらず全てnoneが出力
        // default句は何も送受信がなかった時の処理
        // goroutine起動のタイムラグによるすり抜け
        }
    }
}
// received1 one
// received2 two

timeout

func main() {

    ch1 := make(chan string, 1)
    go func() {
        time.Sleep(2 * time.Second)
        ch1 <- "sending 1"
    }()

    select {
    case res := <-ch1:
        fmt.Println(res)
    case <-time.After(1 * time.Second):
        fmt.Println("timeout 1")
    } // timeout

    ch2 := make(chan string, 1)
    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "sending 2"
    }()
    select {
    case res := <-ch2:
        fmt.Println(res)
    case <-time.After(3 * time.Second):
        fmt.Println("timeout 2")
    } // success
}

Non-Blocking Channel Operation

  • バッファなしchannelのため最初からブロック
  • <-readCh : Channelから値読み込みを永遠に待ち続ける = 値書き込みがなければ永続待機 = deadlock発生
func main() {
    // var writeCh chan<- string
    var readCh <-chan string
    ch := make(chan string)
    readCh = ch

    go func() {
        // writeCh <- "Writing..."
    }()

    fmt.Println(<-readCh)
}
// fatal error: all goroutines are asleep - deadlock!

上記ケースを制御できるのが default

func main() {
    messages := make(chan string)
    signals := make(chan bool)

    select {
    case msg := <-messages:
        fmt.Println("received message", msg)
    default:
        fmt.Println("no message received")
    }

    msg := "hi"
    select {
    case messages <- msg:
        fmt.Println("sent message", msg)
    default:
        fmt.Println("no message sent")
    }

    select {
    case msg := <-messages:
        fmt.Println("received message", msg)
    case sig := <-signals:
        fmt.Println("received signal", sig)
    default:
        fmt.Println("no activity")
    }
}

close

(チャネル受信待ちなど) ブロック中のgoroutineを解放

func main() {
    jobs := make(chan int, 5)
    done := make(chan bool)

    go func() {
        for {
            j, more := <-jobs
            if more {
                fmt.Println("received job", j)
            } else {
                fmt.Println("received all jobs")
                done <- true
                return
            }
        }
    }()

    for j := 1; j <= 3; j++ {
        jobs <- j
        fmt.Println("sent job", j)
    }
    close(jobs) // ループも解除
    time.Sleep(time.Second)
    fmt.Println("sent all jobs")

    <-done
}

クローズすることで、rangeを用いてchannelから取り出すことができる

※クローズしなければ受信待ちによるブロックでdeadlock

func main() {
    queue := make(chan string, 2)
    queue <- "one"
    queue <- "two"
    close(queue) // 削除するとdeadlock

    for elem := range queue {
        fmt.Println(elem)
    }
}

worker

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Println("worker", id, "started  job", j)
        time.Sleep(time.Second)
        fmt.Println("worker", id, "finished job", j)
        results <- j * 2
    }
}

func main() {
    const jobNum = 5
    jobs := make(chan int, numJobs)  // ★バッファ有無で挙動が変わる
    results := make(chan int, numJobs)  // バッファ指定なくすとdeadlock

    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }
    // go func() {
    for j := 1; j <= jobNum; j++ {
        jobs <- j
    }
    // }
    fmt.Println("sleep...")
    time.Sleep(2 * time.Second)
    fmt.Println("sleep end")
    close(jobs)

    for a := 1; a <= jobNum; a++ {
        fmt.Println(<-results)
    }
}
// ★ バッファ指定なしの場合
// worker 3 started  job 1
// worker 1 started  job 2
// worker 2 started  job 3
// worker 1 finished job 2
// worker 1 started  job 4
// worker 2 finished job 3
// worker 2 started  job 5
// worker 3 finished job 1
// sleep...
// worker 1 finished job 4
// worker 2 finished job 5
// sleep end
// 4
// 6
// 2
// 8
// 10

// ★ バッファなしでも jobs <- j のループを別goroutineにすると以下
// sleep...
// worker 1 started  job 1
// worker 2 started  job 2
// worker 3 started  job 3
// worker 3 finished job 3
// worker 3 started  job 4
// worker 2 finished job 2
// worker 2 started  job 5
// worker 1 finished job 1
// sleep end
// 6
// 4
// 2
// worker 3 finished job 4
// 8
// worker 2 finished job 5
// 10

// ★ バッファ指定ありの場合
// sleep...
// worker 1 started  job 1
// worker 2 started  job 2
// worker 3 started  job 3
// worker 3 finished job 3
// worker 3 started  job 4
// worker 1 finished job 1
// worker 1 started  job 5
// worker 2 finished job 2
// worker 1 finished job 5
// worker 3 finished job 4
// sleep end
// 6
// 2
// 4
// worker 1 finished job 5
// 10
// worker 2 finished job 4
// 8
  • バッファ指定ありの場合は、待ち受け? バッファが空だからブロック、送信されたものをworkerが受け取るまでのタイムラグ?の間にmainのsleep実行か?
  • バッファ指定なしの場合は、即実行? 送信側(main)と受信側のgoroutine揃うためブロックされず、workerが受け取ったら即実行
    • 送信側もgoroutineにすると、起動のタイムラグで先にmainのsleep実行
  • results をバッファ指定なしにした場合は、3回送信時点で受信側がそれ以上取り出そうとしdeadlock

fan-out/fan-in

  • ファンアウト:並列処理の起点となる1つのロジックから分岐。この分岐
  • ファンイン:分岐の待ち合わせ

sync.WaitGroup

  • ファンアウト、ファンインの仕組みを提供
  • 複数のgoroutineを管理

メソッド:

  • Add:タスク数登録
  • Done:タスク完了
  • Wait:タスク完了の待機
func worker(id int) {
    fmt.Printf("Worker %d start\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d end\n", id)
}

func main() {
    var wg sync.WaitGroup
    for i := 1; i <= 5; i++ {
        wg.Add(1)
        i := i
        go func() {
            defer wg.Done()
            worker(i)
        }()
    }
    wg.Wait()
}

errorgroup.Group

func main() {
    // eg, ctx := errgroup.WithContext(ctx)
    var eg errgroup.Group
    for i := 1; i <= 5; i++ {
        id := i
        eg.Go(func() error {
            worker(id)
            return nil
        })
    }
    err := eg.Wait()
    if err != nil {
        fmt.Println("error: ", err)
    }
}

refs

Goのgoroutine, channelをちょっと攻略!

Go by Example

GoroutineとChannel

Goでの並列処理を徹底解剖!

[Go言語]Channelを使い倒そうぜ!

Go Concurrency Patterns: Pipelines and cancellation

Go Concurrency Patterns

Addvanced Go Concurrency Patterns

Refactor Diary 1 (Java: APIレスポンス解析)

Refactor Diary 1 (Java: APIレスポンス解析)

例:ユーザロールチェック

ユーザ情報はAPIにより以下のようなものが取れるものとする

// API Response Data Model
public class User {
    private String userName;
    private List<RoleEnum> roles;
    // 他にも複数のフィールドがあるものとする
    // Getter,Setter等省略
}

リファクタ前

public class UserRoleValidator {

    private final Logger logger = LoggerFactory.getLogger(UserRoleValidator.class);
    private final ApiClient apiClient;  // OkHttpClient ラッパークラスとする
    private static final List<RoleEnum> REQUIRED_ANY_ROLES = Lists.of(RoleEnum.Admin, RoleEnum.Manage);

    UserRoleValidator(ApiClient apiClient) {
        this.apiClient = apiClient;
    }

    // APi実行~レスポンスボディ変換~ロールチェックまで全部入り
    public void execute() {
        OkHttp.Response response = apiClient.getUser(userName);
        if (!response.isSuccessful()) {
            LOGGER.error("Response Status Code: " + response.code());
            if (Objects.nonNull(response.body())) {
                LOGGER.error("Error Response Contents: " + response.body().string());
            }
            LOGGER.error("message");
            throw new IOException("message");
        }
        User user = new Gson().fromJson(response.body().string(), User.class)
        List<Role> roles = Objects.isNull(user.getRoles()) ? List.of() : user.getRoles();
        if (roles.isEmpty() || 
                REQUIRED_ANY_ROLES.stream().anyMatch(r -> roles.contains(r))) {
            LOGGER.error("message");
            throw new InsufficientRoleException("message");
        }
    }
}

何が問題か

  • (レスポンスに対して共通で行うような) 共通処理が入り込み、流用できない
  • ロールチェックのパターン網羅するテストを書くにあたり、全テストで apiClient をmockし、返すresponse body data を指定する必要がある等、テスト自体が見にくくなる

リファクタ後

public class UserRoleValidator {

    private static final Logger LOGGER = LoggerFactory.getLogger(UserRoleValidator.class);
    private final ApiClient apiClient; 

    UserRoleValidator(ApiClient apiClient) {
        this.apiClient = apiClient;
    }

    public void execute(String userName) {
        var apiResponse = new DataStoreApiResponse(apiClient.getUser(userName));
        if (apiResponse.isSuccessful()) {
            LOGGER.error("message");
            throw new IOException("message");
        }
        validateUserRole(apiResponse.deserialize());
    }

    public void validateUserRole(User user) {
        if (RequiredRoles.isSatisfy(user.getRoles())) {
            LOGGER.error("message");
            throw new InsufficientRoleException("message");
        }
    }
}

// API Response Data Model
public class User {
    private String userName;
    private List<RoleEnum> roles;
    // Getter,Setter等省略
}

// レスポンスに対する共通処理の集約
class DataStoreApiResponse {
    
    private static final Logger LOGGER = LoggerFactory.getLogger(DataStoreApiResponse.class);
    private final isSuccessful;
    
    DataStoreApiResponse(OkHttp.Response response) {
        if (!response.isSuccessful()) {
            LOGGER.error("Response Status Code: " + response.code());
            if (Objects.nonNull(response.body())) {
                LOGGER.error("Error Response Contents: " + response.body().string());
            }
            isSuccessful = false;
            return;
        }
        isSuccessful = true;
    }

    boolean isSuccessfull() {
        return isSuccessful;
    }

    int getStatusCode() {
        return response.code();
    }

    <T> T deserialize(Class<T> cls) {
        return new Gson().fromJson(response.body().string(), cls)
    }
}

// 知識の確立
public class RequiredRoles {

    private static final List<RoleEnum> REQUIRED_ANY_ROLES = Lists.of(RoleEnum.Admin, RoleEnum.Manage);

    boolean isSatisfy(User user) {
        List<Role> roles = Objects.isNull(user.getRoles()) ? List.of() : user.getRoles();
        if (roles.isEmpty()) {
            return false;
        }
        return REQUIRED_ANY_ROLES.stream().anyMatch(r -> roles.contains(r));
    }
}

先にどういうところでどういうテストが必要になるかを考えて、テストしやすい部品を作る

  • 不良を作りこみやすいような重要な箇所(上記例ならロールチェック処理)を見極め
  • そこに対して、容易にテストができるよう部品に分ける

それにより、複数の重要な知識が1か所に混在し、結果として見通しが悪くなるようなことが防げる。テストも見やすくなり、どういったケースがあり得るのか等テストから読み取りが容易になる

VSCode Markdown

VSCode Markdown

フォーマット

VS CodeのMarkdownフォーマットについて

VSCodeでMarkdownの自動フォーマット&整形ルールを自由に設定

https://github.com/remarkjs/remark/blob/main/packages/remark-stringify/readme.md#options

スライド作成(Marp)

【VS Code + Marp】Markdownから爆速・自由自在なデザインで、プレゼンスライドを作る

Docker メモ

Docker メモ

基本コマンド

dockerコマンド

docker build <image> -f <Dockerfile Path> -t <name>

docker run <image> # image build, container build & run
docker run -d -t -v <mount> --rm --name <container> <image> <args>

docker ps

docker exec <container> <command>
docker exec -it product_web_1 bash

docker stop <container>

docker-composeコマンド

docker-compose build  # image build

docker-compose up  # container build & run
docker-compose up --build  # imageをbuildしてrun
docker-compose up -d  # detachedモード(バックグラウンド実行)

docker-compose ps

docker-compose exec <service> <command>
docker-compose exec web bash  # コンテナに入る

docker-compose down  # stopしてrm

停止しているコンテナ削除

docker system prune

ENTRYPOINT & CMD

ENTRYPOINTを使うことで、Dockerコンテナをコマンドのようにすることができる

  • ENTRYPOINT:コンテナ実行時に必ず実行するコマンド
  • CMD:コンテナ実行時にデフォルトで実行するコマンド。上書き可

両方指定の場合は、ENTRYPOINTは固定部、CMDは変更可部分(引数で上書き)となる

詳細: (Docker) CMDとENTRYPOINTの「役割」と「違い」を解説

ベストプラクティス

ベストプラクティス

  • 1つのコンテナには1つのアプリケーション
  • Docker ImageのLayer数は最小限にする
  • Layerを作るのは、RUN、COPY、ADDの3つ
  • コマンドは && で繋げるべし
  • バックスラッシュ()で改行する
  • Dockerfileを作る時は、キャッシュをうまく活用する
  • CMDは Layerを作らない
  • build context (ワークディレクトリのようなもの)に余計なファイルは置かない
  • COPY / ADD の使い分け
    • COPY:単純にファイルやフォルダをコピーする場合に使用
    • ADD:tarの圧縮ファイルをコピーして回答したい時に使用

随時、有益な物を追加

実践 Docker - ソフトウェアエンジニアの「Docker よくわからない」を終わりにする本

プロダクトマネジメント Link Stack

プロダクトマネジメント Link Stack

随時、有益そうな情報を積んでおく

プロダクトマネージャーはプロダクトの「ミニCEO」なのか?

プロダクトマネージャーの最低限の3つの業務【業務フォーマット付き】