Pendahuluan
Kondisi berpacu (race conditions) adalah salah satu dari kesalahan pemrograman yang berbahaya dan sukar ditangkap. Kesalahan ini biasanya menyebabkan kegagalan yang tidak menentu dan misterius, terkadang kegagalan ini muncul lama setelah kode berjalan. Walaupun mekanisme konkurensi Go membuat kita mudah menulis kode yang konkuren, tetapi ia tidak mencegah adanya kondisi berpacu. Perhatian, ketekunan, dan pengujian diperlukan. Dan perkakas yang tepat juga dapat membantu.
Kami dengan gembira memperkenalkan pendeteksi data race pada Go 1.1, sebuah perkakas baru untuk menemukan kondisi berpacu dalam kode Go. Sekarang ini tersedia pada sistem Linux, OS X, dan Windows dengan prosesor x86 64-bit.
Pendeteksi data race didasari oleh pustaka ThreadSanitizer dari C/C++, yang telah lama digunakan untuk mendeteksi banyak eror dalam basis kode internal Google dan Chromium. Teknologi ini diintegrasikan pada Go di bulan September 2012; sejak itu ia telah menangkap 42 kondisi6 berpacu dalam pustaka standar. Sekarang ia telah menjadi bagian dari proses pembangunan berkelanjutan, yang mana terus menangkap kondisi berpacu bila ia muncul.
Cara bekerja
Pendeteksi data race diintegrasikan dengan perkakas go.
Saat opsi baris perintah -race
di set, compiler membaca semua akses memori
dalam kode dan mencatat kapan dan bagaimana memori tersebut diakses, sementara
pustaka runtime membaca adanya akses yang tidak disinkronisasi ke variabel
yang berbagi.
Saat perilaku "berpacu" terdeteksi, sebuah peringatan dicetak.
(Bacalah
artikel berikut
untuk memahami lebih rinci tentang bagaimana algoritma bekerja).
Pendeteksi data race dapat mendeteksi kondisi berpacu hanya saat dipicu oleh
kode yang sedang berjalan, yang artinya sangatlah penting untuk menjalankan
program dengan opsi -race
telah dinyalakan sebelum digunakan di lingkungan
kerja yang sebenarnya.
Namun, program yang dibangun dengan -race
dapat menggunakan CPU dan memori
sepuluh kali lebih banyak, jadi tidak praktis untuk selalu menjalankan
pendeteksi data race.
Salah satu cara untuk mengatasi dilema ini yaitu dengan menjalankan beberapa
tes dengan pendeteksi data race dinyalakan.
Integrasi tes dan unit tes adalah kandidat yang bagus, secara mereka condong
menggunakan bagian kode secara konkuren.
Pendekatan lain yaitu dengan menjalankan program dengan pendeteksi data race
bersamaan dengan beberapa program yang sama pada beberapa server yang berbeda.
Menggunakan pendeteksi data race
Pendeteksi data race terintegrasi dengan perkakas Go.
Untuk membangun kode Anda dengan menyalakan pendeteksi data race, cukup
tambahkan opsi -race
pada baris perintah:
$ go test -race mypkg // pengujian paket $ go run -race mysrc.go // kompilasi dan menjalankan program $ go build -race mycmd // pembangunan program $ go install -race mypkg // pemasangan paket
Untuk mencoba sendiri pendeteksi data race, ambil dan jalankan contoh program berikut:
$ go get -race golang.org/x/blog/support/racy $ racy
Contoh-contoh
Berikut dua contoh masalah dunia nyata yang ditangkap oleh pendeteksi data race.
Contoh 1: Timer.Reset
Contoh pertama yaitu versi sederhana dari kesalahan nyata yang ditemukan
oleh pendeteksi data race.
Program ini menggunakan sebuah time.Timer
untuk mencetak sebuah pesan
setelah durasi waktu acak antara 0 sampai 1 detik.
Hal ini terjadi berulang kali selama 5 detik.
Program ini menggunakan time.AfterFunc
untuk membuat sebuah Timer
untuk
pesan yang pertama dan kemudian menggunakan method Reset
untuk menjadwalkan
pesan selanjutnya, supaya dapat menggunakan ulang variabel Timer
yang sudah
ada.
11 func main() { 12 start := time.Now() 13 var t *time.Timer 14 t = time.AfterFunc(randomDuration(), func() { 15 fmt.Println(time.Now().Sub(start)) 16 t.Reset(randomDuration()) 17 }) 18 time.Sleep(5 * time.Second) 19 } 20 21 func randomDuration() time.Duration { 22 return time.Duration(rand.Int63n(1e9)) 23 }
Kode tersebut tampak masuk akal, namun pada kondisi tertentu ia akan gagal:
panic: runtime error: invalid memory address or nil pointer dereference [signal 0xb code=0x1 addr=0x8 pc=0x41e38a] goroutine 4 [running]: time.stopTimer(0x8, 0x12fe6b35d9472d96) src/pkg/runtime/ztime_linux_amd64.c:35 +0x25 time.(*Timer).Reset(0x0, 0x4e5904f, 0x1) src/pkg/time/sleep.go:81 +0x42 main.funcĀ·001() race.go:14 +0xe3 created by time.goFunc src/pkg/time/sleep.go:122 +0x48
Apa yang terjadi? Menjalankan program dengan menyalakan pendeteksi data race akan tampak lebih jelas:
================== WARNING: DATA RACE Read by goroutine 5: main.funcĀ·001() race.go:16 +0x169 Previous write by goroutine 1: main.main() race.go:14 +0x174 Goroutine 5 (running) created at: time.goFunc() src/pkg/time/sleep.go:122 +0x56 timerproc() src/pkg/runtime/ztime_linux_amd64.c:181 +0x189 ==================
Pendeteksi data race memperlihatkan masalahnya: pembacaan dan penulisan
tanpa sinkronisasi pada variabel t
dari goroutine yang berbeda.
Jika durasi timer awal sangat kecil, fungsi timer bisa dipanggil sebelum
main
goroutine telah menyimpan nilai ke t
sehingga pemanggilan t.Reset
dilakukan pada t
yang nil
.
Untuk memperbaiki kondisi berpacu ini kita mengubah kode untuk membaca dan
menulis variabel t
hanya dari main
goroutine:
11 func main() { 12 start := time.Now() 13 reset := make(chan bool) 14 var t *time.Timer 15 t = time.AfterFunc(randomDuration(), func() { 16 fmt.Println(time.Now().Sub(start)) 17 reset <- true 18 }) 19 for time.Since(start) < 5*time.Second { 20 <-reset 21 t.Reset(randomDuration()) 22 } 23 }
Di sini, main
goroutine sajalah yang bertanggung jawab men-set dan me-reset
Timer t
dan kanal reset
yang baru mengkomunikasikan kebutuhan untuk
mereset timer dengan cara yang aman.
Pendekatan lain yang lebih sederhana dan kurang efisien yaitu dengan menghindari menggunakan timer yang sama.
Contoh 2: ioutil.Discard
Contoh kedua lebih halus.
Paket ioutil
memiliki objek
Discard
yang mengimplementasikan
io.Writer
,
yang meniadakan semua data yang ditulis ke dalam objek tersebut.
Seperti /dev/null
: sebuah tempat mengirim data yang Anda bisa baca tapi
tidak ingin disimpan.
Objek Discard
ini biasanya digunakan oleh
io.Copy
untuk mengosongkan pembaca, seperti ini:
io.Copy(ioutil.Discard, reader)
Pada bulan Juli 2011, time Go menyadari bahwa menggunakan Discard
dengan
cara ini tidak efisien: fungsi Copy
mengalokasikan penyangga sebesar 32 kB
setiap kali dipanggil, namun saat digunakan dengan Discard
penyangga
tersebut tidak dipakai secara kita hanya akan melempar data yang dibaca saja.
Kita memikirkan bahwa penggunaan idiomatis dari Copy
dan Discard
ini
seharusnya tidak terlalu membebankan.
Perbaikannya cukup sederhana.
Jika Writer
mengimplementasi method ReadFrom
, sebuah pemanggilan Copy
seperti berikut:
io.Copy(writer, reader)
didelegasikan ke pemanggilan yang lebih efisien:
writer.ReadFrom(reader)
Kita
menambahkan method ReadFrom
ke tipe Discard
, yang memiliki penyangga internal yang dibagi dengan semua
penggunanya.
Kita tahu bahwa secara teori ini adalah kondisi berpacu, namun secara semua
penulisan ke penyangga seharusnya langsung dibuang kami berpikir masalah
kondisi berpacu di sini tidak begitu penting.
Saat pendeteksi data race diimplementasikan ia langsung menandakan kode tersebut sebagai "berpacu". Sekali lagi, kita menyadari bahwa kode tersebut bermasalah, namun memutuskan bahwa kondisi berpacu tersebut tidak "nyata". Untuk menghindari kondisi "positif salah" ini pada saat pembangunan, kita mengimplementasikan versi yang tidak berpacu yang dinyalakan hanya saat pendeteksi data race berjalan.
Akan tetapi beberapa bulan kemudian
Brad
menemui sebuah bug yang
janggal dan menyebalkan.
Setelah beberapa hari melakukan debug, dia menemukan kondisi berpacu yang
nyata yang disebabkan oleh ioutil.Discard
.
Berikut kode yang diketahui berpacu dalam io/ioutil
, yang mana Discard
adalah devNull
yang berbagi sebuah penyangga tunggal dengan semua
penggunanya.
var blackHole [4096]byte // shared buffer func (devNull) ReadFrom(r io.Reader) (n int64, err error) { readSize := 0 for { readSize, err = r.Read(blackHole[:]) n += int64(readSize) if err != nil { if err == io.EOF { return n, nil } return } } }
Program Brad memiliki sebuah tipe trackDigestReader
, yang membungkus sebuah
io.Reader
dan mencatat hash dari apa yang ia baca.
type trackDigestReader struct { r io.Reader h hash.Hash } func (t trackDigestReader) Read(p []byte) (n int, err error) { n, err = t.r.Read(p) t.h.Write(p[:n]) return }
Sebagai contoh, ia bisa digunakan untuk menghitung hash SHA-1 dari sebuah berkas saat membacanya:
tdr := trackDigestReader{r: file, h: sha1.New()} io.Copy(writer, tdr) fmt.Printf("File hash: %x", tdr.h.Sum(nil))
Pada kasus-kasus tertentu data terkadang tidak perlu ditulis—tetapi hash
masih diperlukan—maka Discard
digunakan:
io.Copy(ioutil.Discard, tdr)
Namun pada kasus ini penyangga blackHole
bukan hanya lubang hitam; ia adalah
tempat untuk menyimpan data antara pembacaan dari sumber io.Reader
dan
penulisan ke hash.Hash
.
Saat beberapa goroutine mulai melakukan hash secara bersamaan, setiap
goroutine akan berbagi penyangga blackHole
yang sama, kondisi berpacu mulai
timbul dengan mengkorupsi data antara pembacaan dan penulisan.
Tidak ada eror atau panic yang terjadi, namun hash yang dihasilkan selalu
salah.
func (t trackDigestReader) Read(p []byte) (n int, err error) { // penyangga p adalah blackHole n, err = t.r.Read(p) // p bisa dikorupsi oleh goroutine yang lain, // baik oleh Read di atas atau oleh Write di bawah. t.h.Write(p[:n]) return }
Bug ini akhirnya
diperbaiki
dengan memberikan penyangga yang unik untuk setiap penggunaan
ioutil.Discard
, mengeliminasi kondisi berpacu pada penyangga yang berbagi.
Kesimpulan
Pendeteksi data race adalah perkakas yang tangguh untuk memeriksa ketepatan dari program yang konkuren. Ia tidak akan menimbulkan kondisi positif-salah, jadi perhatikan baik-baik peringatan yang dikeluarkan oleh pendeteksi ini. Namun ia hanya akan bekerja baik seperti halnya tes-tes Anda; Anda harus memastikan mereka benar-benar menggunakan properti konkuren dari kode Anda supaya pendeteksi data race dapat melakukan kerjanya dengan baik.
Apa yang Anda tunggu lagi? Jalankan "go test -race" pada kode Anda hari ini!