いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-229 (マップはスレッドセーフではない)

概要

少し前に以下のメモをアップしました。

devlights.hatenablog.com

上記ではスライスでしたが、マップも同様です。

こちらはスライスとは違って、ちょっとでも非同期アクセスすると即落ちるのであまり問題になることは無いと思います。

マップに関しては 公式のFAQ でも言及されています。

https://go.dev/doc/faq#atomic_maps

以下、一応メモです。

サンプル

// マップ操作 はスレッドセーフでは無いというのを示すサンプルです。
// 本サンプルはデータ競合が発生しています。
//
// Go本家のFAQにもmap操作はatomicでは無いですよと記載がある。
//   - https://go.dev/doc/faq#atomic_maps
//
// REFERENCES:
//   - https://go.dev/doc/faq#atomic_maps
//   - https://stackoverflow.com/questions/44152988/append-not-thread-safe
//   - https://stackoverflow.com/questions/49879322/can-i-concurrently-write-different-slice-elements
package main

import (
    "flag"
    "fmt"
    "strconv"
    "sync"
)

func main() {
    var (
        verbose = flag.Bool("verbose", false, "verbose output")
    )
    flag.Parse()

    var (
        src = make(map[string]bool)
        dst = make(map[string]bool)
    )

    for i := 0; i < 5; i++ {
        src[strconv.Itoa(i)] = true
    }

    wg := sync.WaitGroup{}
    for k, v := range src {
        wg.Add(1)
        go func(k string, v bool) {
            defer wg.Done()

            dst[k] = v
        }(k, v)
    }

    wg.Wait()

    fmt.Printf("src-len=%d\tdst-len=%d\n", len(src), len(dst))

    if *verbose {
        fmt.Println("=========== SRC ===========")
        for _, v := range src {
            fmt.Println(v)
        }
        fmt.Println("=========== DST ===========")
        for _, v := range dst {
            fmt.Println(v)
        }
    }
}

Taskfile.yml

version: '3'

tasks:
  fmt:
    cmds:
      - go fmt ./...
  vet:
    cmds:
      - go vet ./...
  run:
    cmds:
      - cmd: for i in {1..10} ; do go run race/main.go; done
        silent: true
  run-notrace:
    cmds:
      - cmd: for i in {1..10} ; do go run -race notrace/main.go; done
        silent: true
  run-with-raceoption:
    cmds:
      - cmd: go run -race race/main.go
        ignore_error: true

実行すると以下のようになります。

gitpod /workspace/try-golang (master) $ task -d examples/singleapp/map_is_not_threadsafe/ run
fatal error: concurrent map writes

goroutine 6 [running]:
runtime.throw({0x4a3255?, 0x0?})
        /home/gitpod/go/src/runtime/panic.go:992 +0x71 fp=0xc00005a700 sp=0xc00005a6d0 pc=0x430a71
runtime.mapassign_faststr(0x0?, 0x0?, {0x4a6178, 0x1})
        /home/gitpod/go/src/runtime/map_faststr.go:212 +0x39c fp=0xc00005a768 sp=0xc00005a700 pc=0x410c3c
main.main.func1({0x4a6178?, 0x0?}, 0x1)
        /workspace/try-golang/examples/singleapp/map_is_not_threadsafe/race/main.go:41 +0x78 fp=0xc00005a7b8 sp=0xc00005a768 pc=0x4878b8
main.main.func2()
        /workspace/try-golang/examples/singleapp/map_is_not_threadsafe/race/main.go:42 +0x32 fp=0xc00005a7e0 sp=0xc00005a7b8 pc=0x487812
runtime.goexit()
        /home/gitpod/go/src/runtime/asm_amd64.s:1571 +0x1 fp=0xc00005a7e8 sp=0xc00005a7e0 pc=0x45bb01
created by main.main
        /workspace/try-golang/examples/singleapp/map_is_not_threadsafe/race/main.go:38 +0x1be

goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc000108270?)
        /home/gitpod/go/src/runtime/sema.go:56 +0x25
sync.(*WaitGroup).Wait(0xc000104e78?)
        /home/gitpod/go/src/sync/waitgroup.go:136 +0x52
main.main()
        /workspace/try-golang/examples/singleapp/map_is_not_threadsafe/race/main.go:45 +0x2d4
exit status 2
task: Failed to run task "run": exit status 1

Mutexでクリティカルセクションを作っているサンプル

// マップ操作 はスレッドセーフでは無いというのを示すサンプルです。
// 本サンプルはデータ競合が発生しません。
//
// REFERENCES:
//   - https://stackoverflow.com/questions/44152988/append-not-thread-safe
//   - https://stackoverflow.com/questions/49879322/can-i-concurrently-write-different-slice-elements
package main

import (
    "flag"
    "fmt"
    "strconv"
    "sync"
)

func main() {
    var (
        verbose = flag.Bool("verbose", false, "verbose output")
    )
    flag.Parse()

    var (
        mu sync.Mutex
        src = make(map[string]bool)
        dst = make(map[string]bool)
    )

    for i := 0; i < 5; i++ {
        src[strconv.Itoa(i)] = true
    }

    wg := sync.WaitGroup{}
    for k, v := range src {
        wg.Add(1)
        go func(k string, v bool) {
            defer wg.Done()

            mu.Lock()
            dst[k] = v
            mu.Unlock()
        }(k, v)
    }

    wg.Wait()

    fmt.Printf("src-len=%d\tdst-len=%d\n", len(src), len(dst))

    if *verbose {
        fmt.Println("=========== SRC ===========")
        for _, v := range src {
            fmt.Println(v)
        }
        fmt.Println("=========== DST ===========")
        for _, v := range dst {
            fmt.Println(v)
        }
    }
}

実行すると以下のようになります。

gitpod /workspace/try-golang (master) $ task -d examples/singleapp/map_is_not_threadsafe/ run-notrace
src-len=5       dst-len=5
src-len=5       dst-len=5
src-len=5       dst-len=5
src-len=5       dst-len=5
src-len=5       dst-len=5
src-len=5       dst-len=5
src-len=5       dst-len=5
src-len=5       dst-len=5
src-len=5       dst-len=5
src-len=5       dst-len=5

まあ、Goで非同期処理する場合は チャネル っていうとても素晴らしい機構があるのでそれを使うべしってことですね。

参考情報


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。