Tak satu pun dari struktur data inti yang aman untuk thread. Satu-satunya yang saya tahu tentang paket Ruby adalah implementasi antrian di perpustakaan standar ( require 'thread'; q = Queue.new
).
GIL MRI tidak menyelamatkan kita dari masalah keamanan thread. Ini hanya memastikan bahwa dua thread tidak dapat menjalankan kode Ruby pada saat yang bersamaan , yaitu pada dua CPU yang berbeda pada waktu yang sama. Untaian masih dapat dijeda dan dilanjutkan kapan saja di kode Anda. Jika Anda menulis kode seperti @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }
misalnya mutasi variabel bersama dari beberapa utas, nilai variabel bersama sesudahnya tidak deterministik. GIL kurang lebih merupakan simulasi dari sistem inti tunggal, tidak mengubah masalah mendasar dalam menulis program bersamaan yang benar.
Meskipun MRI telah menjadi single-threaded seperti Node.js, Anda masih harus memikirkan tentang konkurensi. Contoh dengan variabel incremented akan berfungsi dengan baik, tetapi Anda masih bisa mendapatkan kondisi balapan di mana hal-hal terjadi dalam urutan non-deterministik dan satu callback mengganggu hasil yang lain. Sistem asinkron berulir tunggal lebih mudah untuk dipikirkan, tetapi tidak bebas dari masalah konkurensi. Bayangkan aplikasi dengan banyak pengguna: jika dua pengguna menekan edit pada kiriman Stack Overflow pada waktu yang kurang lebih bersamaan, luangkan waktu untuk mengedit kiriman lalu tekan simpan, yang perubahannya akan dilihat oleh pengguna ketiga nanti ketika mereka membaca posting yang sama?
Di Ruby, seperti pada kebanyakan runtime serentak lainnya, apa pun yang lebih dari satu operasi tidak aman untuk thread. @n += 1
tidak aman untuk thread, karena ini adalah operasi ganda. @n = 1
adalah thread safe karena ini adalah satu operasi (ada banyak operasi di balik terpal, dan saya mungkin akan mendapat masalah jika saya mencoba menjelaskan mengapa "thread safe" secara mendetail, tetapi pada akhirnya Anda tidak akan mendapatkan hasil yang tidak konsisten dari tugas ). @n ||= 1
, bukan dan tidak ada operasi + tugas singkatan lainnya juga. Satu kesalahan yang sering saya buat adalah menulis return unless @started; @started = true
, yang sama sekali tidak aman untuk thread.
Saya tidak mengetahui daftar otoritatif dari pernyataan aman thread dan non-thread safe untuk Ruby, tetapi ada aturan praktis yang sederhana: jika sebuah ekspresi hanya melakukan satu operasi (bebas efek samping), itu mungkin thread safe. Sebagai contoh: a + b
tidak apa-apa, a = b
juga baik, dan a.foo(b)
tidak masalah, jika metode foo
ini bebas efek samping (karena hampir semua hal di Ruby adalah panggilan metode, bahkan penugasan dalam banyak kasus, ini juga berlaku untuk contoh lainnya). Efek samping dalam konteks ini berarti hal-hal yang mengubah keadaan. def foo(x); @x = x; end
adalah tidak efek samping bebas.
Salah satu hal tersulit dalam menulis kode aman thread di Ruby adalah semua struktur data inti, termasuk array, hash, dan string, dapat berubah. Sangat mudah untuk secara tidak sengaja membocorkan bagian dari negara Anda, dan ketika bagian itu bisa berubah, hal-hal bisa benar-benar kacau. Perhatikan kode berikut:
class Thing
attr_reader :stuff
def initialize(initial_stuff)
@stuff = initial_stuff
@state_lock = Mutex.new
end
def add(item)
@state_lock.synchronize do
@stuff << item
end
end
end
Sebuah instance dari kelas ini dapat dibagikan di antara utas dan mereka dapat dengan aman menambahkan sesuatu ke dalamnya, tetapi ada bug konkurensi (ini bukan satu-satunya): status internal objek bocor melalui stuff
pengakses. Selain bermasalah dari perspektif enkapsulasi, itu juga membuka kaleng worm konkurensi. Mungkin seseorang mengambil larik itu dan meneruskannya ke tempat lain, dan kode itu pada gilirannya menganggapnya sekarang memiliki larik itu dan dapat melakukan apa pun yang diinginkan dengannya.
Contoh Ruby klasik lainnya adalah ini:
STANDARD_OPTIONS = {:color => 'red', :count => 10}
def find_stuff
@some_service.load_things('stuff', STANDARD_OPTIONS)
end
find_stuff
berfungsi dengan baik saat pertama kali digunakan, tetapi mengembalikan sesuatu yang lain untuk kedua kalinya. Mengapa? The load_things
Metode terjadi untuk berpikir itu memiliki hash pilihan berlalu untuk itu, dan melakukan color = options.delete(:color)
. Sekarang STANDARD_OPTIONS
konstanta tidak memiliki nilai yang sama lagi. Konstanta hanya konstan dalam apa yang mereka referensikan, mereka tidak menjamin konstannya struktur data yang dirujuknya. Coba pikirkan apa yang akan terjadi jika kode ini dijalankan secara bersamaan.
Jika Anda menghindari keadaan yang bisa berubah bersama (misalnya variabel dalam objek yang diakses oleh beberapa utas, struktur data seperti hash dan array yang diakses oleh banyak utas) keamanan utas tidak terlalu sulit. Cobalah untuk meminimalkan bagian aplikasi Anda yang diakses secara bersamaan, dan fokuskan upaya Anda di sana. IIRC, dalam aplikasi Rails, objek kontroler baru dibuat untuk setiap permintaan, sehingga hanya akan digunakan oleh satu utas, dan hal yang sama berlaku untuk objek model apa pun yang Anda buat dari kontroler itu. Namun, Rails juga mendorong penggunaan variabel global ( User.find(...)
menggunakan variabel globalUser
, Anda mungkin menganggapnya hanya sebagai kelas, dan ini adalah kelas, tetapi juga merupakan namespace untuk variabel global), beberapa di antaranya aman karena hanya dapat dibaca, tetapi terkadang Anda menyimpan sesuatu dalam variabel global ini karena itu nyaman. Berhati-hatilah saat Anda menggunakan apa pun yang dapat diakses secara global.
Sudah mungkin untuk menjalankan Rails di lingkungan berulir cukup lama sekarang, jadi tanpa menjadi ahli Rails, saya masih akan mengatakan bahwa Anda tidak perlu khawatir tentang keamanan utas ketika datang ke Rails itu sendiri. Anda masih dapat membuat aplikasi Rails yang tidak aman untuk thread dengan melakukan beberapa hal yang saya sebutkan di atas. Ketika datang permata lain berasumsi bahwa mereka tidak thread safe kecuali mereka mengatakannya, dan jika mereka mengatakan bahwa mereka berasumsi bahwa mereka tidak, dan melihat-lihat kode mereka (tetapi hanya karena Anda melihat bahwa mereka melakukan hal-hal seperti@n ||= 1
tidak berarti bahwa mereka tidak thread safe, itu adalah hal yang benar-benar sah untuk dilakukan dalam konteks yang benar - Anda harus mencari hal-hal seperti keadaan yang dapat berubah dalam variabel global, bagaimana ia menangani objek yang dapat berubah yang diteruskan ke metodenya, dan terutama bagaimana itu menangani hash opsi).
Terakhir, menjadi thread unsafe adalah properti transitif. Apa pun yang menggunakan sesuatu yang tidak aman untuk benang itu sendiri tidak aman untuk benang.