Pendahuluan
Data race adalah tipe kesalahan yang paling umum dan sukar untuk di-debug dalam sistem konkuren. Data race terjadi saat dua goroutine mengakses variabel yang sama secara bersamaan dengan salah satu metoda akses adalah tulis. Lihat Model memori pada Go untuk lebih rinci.
Berikut contoh data race yang bisa membuat program crash dan korupsi pada memori:
func main() { c := make(chan bool) m := make(map[string]string) go func() { m["1"] = "a" // Akses konflik yang pertama. c <- true }() m["2"] = "b" // Akses konflik yang kedua. <-c for k, v := range m { fmt.Println(k, v) } }
Penggunaan
Untuk membantu mendiagnosis kesalahan seperti ini, Go memiliki pendeteksi
data race.
Untuk menggunakan pendeteksi tersebut, tambahkan opsi -race
pada perintah
go
:
$ go test -race mypkg // saat menguji paket $ go run -race mysrc.go // saat menjalankan berkas sumber $ go build -race mycmd // saat membangun program $ go install -race mypkg // saat memasang paket
Format laporan
Saat pendeteksi data race menemukan sebuah data race dalam program, ia akan mencetak sebuah laporan. Laporan tersebut berisi stack trace dari akses-akses yang konflik, dan juga kumpulan baris yang melaporkan di goroutine mana akses tersebut terjadi. Berikut contohnya:
WARNING: DATA RACE Read by goroutine 185: net.(*pollServer).AddFD() src/net/fd_unix.go:89 +0x398 net.(*pollServer).WaitWrite() src/net/fd_unix.go:247 +0x45 net.(*netFD).Write() src/net/fd_unix.go:540 +0x4d4 net.(*conn).Write() src/net/net.go:129 +0x101 net.func·060() src/net/timeout_test.go:603 +0xaf Previous write by goroutine 184: net.setWriteDeadline() src/net/sockopt_posix.go:135 +0xdf net.setDeadline() src/net/sockopt_posix.go:144 +0x9c net.(*conn).SetDeadline() src/net/net.go:161 +0xe3 net.func·061() src/net/timeout_test.go:616 +0x3ed Goroutine 185 (running) created at: net.func·061() src/net/timeout_test.go:609 +0x288 Goroutine 184 (running) created at: net.TestProlongTimeout() src/net/timeout_test.go:618 +0x298 testing.tRunner() src/testing/testing.go:301 +0xe8
Opsi-opsi
Variabel lingkungan GORACE
dapat digunakan untuk menset opsi pendeteksi
data race.
Formatnya yaitu:
GORACE="opsi1=nilai1 opsi2=nilai2"
Opsi-opsinya adalah sebagai berikut:
-
log_path
(nilai bakustderr
): Pendeteksi data race menulis laporan ke berkas bernamalog_path.pid
. Nama-nama khusus sepertistdout
danstderr
menyebabkan laporan ditulis ke standar keluaran dan standar eror. -
exitcode
(nilai baku66
): Nilai status program saat berhenti setelah mendeteksi adanya data race. -
strip_path_prefix
(nilai baku ""): Hapus string prefiks dari semua berkas laporan, untuk membuat laporan lebih singkat. -
history_size
(nilai baku 1): Riwayat akses memori per-goroutine yaitu32K * 2**history_size
elemen. Meningkatkan nilai ini dapat menghindari eror "failed to restore the stack", dengan biaya bertambahnya penggunaan memori. -
halt_on_error
(nilai baku 0): Mengontrol apakah program berhenti setelah melaporkan data race yang pertama atau tidak. -
atexit_sleep_ms
(nilai baku 1000): Lamanyamain
goroutine untuk tidur sebentar sebelum program berhenti, dalam milidetik.
Contoh:
$ GORACE="log_path=/tmp/race/report strip_path_prefix=/my/go/sources/" go test -race
Artinya, laporan tentang adanya data race pada hasil pengujian, jika ada,
akan ditulis ke dalam berkas /tmp/race/report
dan setiap keluaran dari
laporan akan menghapus prefix "/my/go/sources/".
Mengindahkan pengujian
Saat opsi -race
diberikan saat pembangunan, perintah go
menambahan
tag race
pada build
.
Anda dapat menggunakan tag ini untuk mengindahkan beberapa kode dan pengujian
dari pendeteksi data race.
Berikut contohnya:
// +build !race package foo // The test contains a data race. See issue 123. func TestFoo(t *testing.T) { // ... } // The test fails under the race detector due to timeouts. func TestBar(t *testing.T) { // ... } // The test takes too long under the race detector. func TestBaz(t *testing.T) { // ... }
Cara penggunaan
Sebagai langkah awal, jalankan tes Anda menggunakan pendeteksi data race
(go test -race
).
Pendeteksi data race hanya dapat mencari data race saat program
dijalankan, ia tidak bisa menemukan data race bila kode tidak dieksekusi.
Jika tes-tes Anda tidak komplit, Anda mungkin bisa menemukan data race pada
program Anda dengan membangun program dengan tambahan opsi -race
dan
menjalankan program tersebut pada beban kerja yang sesungguhnya.
Contoh Data race yang umum
Berikut beberapa contoh data race yang umum terjadi. Semua contoh ini dapat dideteksi dengan pendeteksi data race.
Data race pada pengulangan
func main() { var wg sync.WaitGroup wg.Add(5) for i := 0; i < 5; i++ { go func() { fmt.Println(i) // Bukan 'i' yang Anda harapkan. wg.Done() }() } wg.Wait() }
Variabel i
di dalam fungsi adalah variabel yang sama digunakan oleh
pengulangan, sehingga pembacaan pada goroutine berpacu dengan pengulangan,
akibatnya pembacaan dalam goroutine berpacu dengan penambahan pada pengulangan.
(Program tersebut bisa jadi mencetak 55555
, bukan 01234
).
Program tersebut dapat diperbaiki dengan membuat salinan dari variabel i
:
func main() { var wg sync.WaitGroup wg.Add(5) for i := 0; i < 5; i++ { go func(j int) { fmt.Println(j) // Variabel `j` adalah salinan lokal dari `i`. wg.Done() }(i) // Kirim salinan dari variabel `i` ke dalam fungsi. } wg.Wait() }
Berbagi variabel tanpa sengaja
// ParallelWrite menulis data ke file1 dan file2, mengembalikan satu atau // lebih eror. func ParallelWrite(data []byte) chan error { res := make(chan error, 2) f1, err := os.Create("file1") if err != nil { res <- err } else { go func() { // Variabel err ini dibagi dengan goroutine main, sehingga // penulisan err di sini berpacu dengan err di bawah. _, err = f1.Write(data) res <- err f1.Close() }() } f2, err := os.Create("file2") // Konflik penulisan err kedua. if err != nil { res <- err } else { go func() { _, err = f2.Write(data) res <- err f2.Close() }() } return res }
Cara memperbaiki yaitu dengan menggunakan variabel yang baru dalam goroutine
(perhatikan penggunaan :=
):
... _, err := f1.Write(data) ... _, err := f2.Write(data) ...
Variabel global yang tidak dilindungi
Jika kode berikut dipanggil dari beberapa goroutine, ia akan menyebabkan data
race pada variabel map service
.
Pembacaan dan penulisan secara bersamaan dari variabel map yang sama tidak
aman:
var service map[string]net.Addr func RegisterService(name string, addr net.Addr) { service[name] = addr } func LookupService(name string) net.Addr { return service[name] }
Untuk membuat kode lebih aman, lindungi akses dengan sebuah mutex
:
var ( service map[string]net.Addr serviceMu sync.Mutex ) func RegisterService(name string, addr net.Addr) { serviceMu.Lock() defer serviceMu.Unlock() service[name] = addr } func LookupService(name string) net.Addr { serviceMu.Lock() defer serviceMu.Unlock() return service[name] }
Variabel primitif yang tidak dilindungi
Data race juga dapat terjadi pada variabel-variabel bertipe primitif
(bool
, int
, int64
, dan lainnya), seperti pada contoh berikut:
type Watchdog struct{ last int64 } func (w *Watchdog) KeepAlive() { w.last = time.Now().UnixNano() // Konflik akses yang pertama. } func (w *Watchdog) Start() { go func() { for { time.Sleep(time.Second) // Second conflicting access. if w.last < time.Now().Add(-10*time.Second).UnixNano() { fmt.Println("No keepalives for 10 seconds. Dying.") os.Exit(1) } } }() }
Bahkan data race yang tampak "polos" seperti di atas dapat menyebabkan masalah yang sukar-di-debug yang disebabkan oleh akses memori yang tidak atomik, interferensi akibat optimisasi compiler, atau masalah pengurutan saat mengakses memori prosesor.
Cara paling umum untuk memperbaiki data race seperti ini yaitu dengan
menggunakan sebuah kanal (channel) atau mutex
.
Supaya bebas-penguncian, bisa menggunakan paket
sync/atomic
.
type Watchdog struct{ last int64 } func (w *Watchdog) KeepAlive() { atomic.StoreInt64(&w.last, time.Now().UnixNano()) } func (w *Watchdog) Start() { go func() { for { time.Sleep(time.Second) if atomic.LoadInt64(&w.last) < time.Now().Add(-10*time.Second).UnixNano() { fmt.Println("No keepalives for 10 seconds. Dying.") os.Exit(1) } } }() }
Operasi kirim-dan-tutup yang tidak sinkron
Seperti yang didemokan oleh contoh berikut, operasi kirim dan tutup yang tidak disinkronkan pada kanal yang sama bisa menimbulkan kondisi data race:
c := make(chan struct{}) // atau kanal dengan-penyangga. // Pendeteksi data race tidak bisa menemukan hubungan terjadi-sebelum untuk // operasi kirim-dan-tutup seperti di bawah ini. Dua operasi berikut tidak // disinkronkan dan terjadi secara konkuren. go func() { c <- struct{}{} }() close(c)
Menurut Memori model pada Go, pengiriman ke kanal terjadi sebelum penerimaan dari kanal selesai. Untuk sinkronisasi operasi kirim-dan-tutup, gunakan operasi penerimaan untuk menjamin bahwa pengiriman selesai sebelum operasi tutup dilakukan:
c := make(chan struct{}) // atau kanal dengan penyangga. go func() { c <- struct{}{} }() <-c close(c)
Dukungan sistem
Pendeteksi data race berjalan pada linux/amd64
, linux/ppc64le
,
linux/arm64
, freebsd/amd64
, netbsd/amd64
, darwin/amd64
,
darwin/arm64
, dan windows/amd64
.
Beban runtime
Biaya dari penggunaan pendeteksi data race beragam pada program, namun pada umumnya penggunaan memori bisa meningkat 5-10x dan waktu eksekusi bertambah 2-20x.
Pendeteksi data race saat ini mengalokasikan 8 byte tambahan per perintah
defer
dan recover
.
Alokasi tambahan ini
tidak berkurang sampai goroutine selesai.
Hal ini berarti jika Anda memiliki goroutine yang berjalan lama yang secara
periodik memanggil defer
dan recover
, penggunaan memori program Anda bisa
bertambah tanpa batas.
Alokasi memori ini tidak akan muncul pada keluaran dari runtime.ReadMemStats
atau runtime/pprof
.