adalah menulis secara bersamaan di stdout threadsafe?
kode di bawah ini tidak membuang data race
package main
import (
"fmt"
"os"
"strings"
)
func main() {
x := strings.Repeat(" ", 1024)
go func() {
for {
fmt.Fprintf(os.Stdout, x+"aa\n")
}
}()
go func() {
for {
fmt.Fprintf(os.Stdout, x+"bb\n")
}
}()
go func() {
for {
fmt.Fprintf(os.Stdout, x+"cc\n")
}
}()
go func() {
for {
fmt.Fprintf(os.Stdout, x+"dd\n")
}
}()
<-make(chan bool)
}
Saya mencoba beberapa panjang data, dengan varian https://play.golang.org/p/29Cnwqj5K30
Posting ini mengatakan itu bukan TS.
Email ini tidak benar-benar menjawab pertanyaan itu, atau saya tidak mengerti.
Dokumentasi paket os dan fmt tidak banyak menyebutkan tentang ini. Saya akui saya tidak menggali kode sumber dari kedua paket tersebut untuk menemukan penjelasan lebih lanjut, mereka tampak terlalu rumit bagi saya.
Apa saja rekomendasi dan referensinya ?
Jawaban
Saya tidak yakin itu akan memenuhi syarat sebagai jawaban pasti tetapi saya akan mencoba memberikan beberapa wawasan.
The F*
-functions dari fmt
paket hanya menyatakan mereka mengambil nilai dari tipe mengimplementasikan io.Writer
interface dan panggilan Write
di atasnya. Fungsinya sendiri aman untuk digunakan secara bersamaan - dalam arti tidak apa-apa untuk memanggil nomor apa pun fmt.Fwhaveter
secara bersamaan: paket itu sendiri disiapkan untuk itu, tetapi mendukung antarmuka di Go tidak menyatakan apa pun tentang jenis konkurensi yang sebenarnya.
Dengan kata lain, titik sebenarnya di mana konkurensi mungkin atau mungkin tidak diizinkan tergantung pada "penulis" yang fungsi fmt
penulisannya. (Satu juga harus diingat bahwa fmt.*Print*
fungsi diizinkan untuk dipanggil ke Write
tujuannya beberapa kali - berlawanan dengan yang disediakan oleh paket stok log
.)
Jadi, pada dasarnya kami memiliki dua kasus:
- Penerapan kustom dari
io.Writer
. - Implementasi stok itu, seperti
*os.File
atau pembungkus di sekitar soket yang dihasilkan oleh fungsinet
paket.
Kasus pertama adalah yang sederhana: apa pun yang dilakukan implementor.
Kasus kedua lebih sulit: seperti yang saya mengerti, pendirian perpustakaan standar Go tentang ini (meskipun tidak dinyatakan dengan jelas dalam dokumen) dalam pembungkus yang disediakan di sekitar "hal-hal" yang disediakan oleh OS — seperti deskriptor dan soket file — cukup "thin", dan karenanya semantik apa pun yang mereka implementasikan, diimplementasikan secara transitif oleh kode stdlib yang berjalan pada sistem tertentu.
Misalnya, POSIX mensyaratkan bahwa write(2)panggilan bersifat atomik satu sama lain ketika mereka beroperasi pada file biasa atau tautan simbolis. Ini berarti, karena setiap panggilan ke Write
hal-hal yang membungkus deskriptor atau soket file sebenarnya menghasilkan satu syscall "tulis" dari sistem tagret, Anda dapat berkonsultasi dengan dokumen OS target dan mendapatkan gambaran tentang apa yang akan terjadi.
Perhatikan bahwa POSIX hanya memberi tahu tentang objek sistem file, dan jika os.Stdout
dibuka ke terminal (atau pseudo-terminal) atau ke pipa atau apa pun yang mendukung write(2)
syscall, hasilnya akan bergantung pada subsistem yang relevan dan / atau driver implement — misalnya, data dari beberapa panggilan serentak mungkin diselingi, atau salah satu panggilan, atau keduanya, mungkin saja gagal oleh OS — tidak mungkin, tapi tetap saja.
Kembali ke Go, dari apa yang saya kumpulkan, fakta-fakta berikut berlaku tentang tipe Go stdlib yang membungkus deskriptor dan soket file:
- Mereka aman untuk digunakan secara bersamaan sendiri (maksud saya, pada level Go).
- Mereka "memetakan"
Write
danRead
memanggil 1-ke-1 ke objek yang mendasarinya — yaitu,Write
panggilan tidak pernah dibagi menjadi dua atau lebih syscall yang mendasarinya, danRead
panggilan tidak pernah mengembalikan data yang "dilem" dari hasil beberapa syscall yang mendasarinya. (Ngomong-ngomong, orang terkadang tersandung oleh perilaku tanpa embel-embel ini - misalnya, lihat ini atau ini sebagai contoh.)
Jadi pada dasarnya ketika kami mempertimbangkan ini dengan fakta fmt.*Print*
bebas untuk menelepon Write
beberapa kali per satu panggilan, contoh Anda yang digunakan os.Stdout
, akan:
- Jangan pernah menghasilkan data race - kecuali Anda telah menetapkan
os.Stdout
beberapa penerapan kustom pada variabel , - tapi - Data yang sebenarnya ditulis ke FD yang mendasarinya akan dicampur dalam urutan yang tidak dapat diprediksi yang mungkin bergantung pada banyak faktor termasuk versi dan pengaturan kernel OS, versi Go yang digunakan untuk membangun program, perangkat keras, dan beban pada sistem.
TL; DR
- Beberapa panggilan serentak untuk
fmt.Fprint*
menulis ke nilai "penulis" yang sama menunda konkurensinya ke implementasi (jenis) "penulis". - Tidak mungkin untuk memiliki data race dengan objek "file-like" yang disediakan oleh Go stdlib dalam penyiapan yang telah Anda sajikan dalam pertanyaan Anda.
- Masalah sebenarnya bukanlah pada data race pada level program Go tetapi dengan akses bersamaan ke sumber daya tunggal yang terjadi pada level OS. Dan di sana, kami tidak (biasanya) berbicara tentang balapan data karena OS komoditas Go mendukung mengekspos hal-hal yang dapat "ditulis" sebagai abstraksi, di mana balapan data nyata mungkin akan menunjukkan bug di kernel atau di driver (dan Detektor balapan Go tidak akan dapat mendeteksinya karena memori tersebut tidak akan dimiliki oleh waktu proses Go yang menjalankan proses).
Pada dasarnya, dalam kasus Anda, jika Anda perlu memastikan data yang dihasilkan oleh panggilan tertentu untuk fmt.Fprint*
keluar sebagai bagian yang berdekatan dengan penerima data aktual yang disediakan oleh OS, Anda perlu membuat serial panggilan ini karena fmt
paket tidak memberikan jaminan mengenai jumlah panggilan ke Write
"penulis" yang disediakan untuk fungsi yang diekspornya.
Serialisasi dapat berupa eksternal (eksplisit, yaitu "ambil kunci, panggil fmt.Fprint*
, lepaskan kunci") atau internal - dengan membungkus os.Stdout
dalam tipe khusus yang akan mengelola kunci, dan menggunakannya). Dan sementara kita melakukannya, log
paket melakukan hal itu, dan dapat langsung digunakan sebagai "pencatat" yang disediakannya, termasuk yang default, memungkinkan untuk menghalangi keluaran "tajuk log" (seperti stempel waktu dan nama file).