bagaimana cara mengetahui apa yang BUKAN thread-safe di ruby?

93

mulai dari Rails 4 , semuanya harus berjalan di lingkungan berulir secara default. Artinya adalah semua kode yang kita tulis DAN SEMUA permata yang kita gunakan harus adathreadsafe

jadi, saya punya beberapa pertanyaan tentang ini:

  1. apa yang TIDAK aman untuk benang di ruby ​​/ rails? Vs Apa thread-safe di ruby ​​/ rails?
  2. Apakah ada daftar permata yang adalah dikenal thread atau sebaliknya?
  3. apakah ada daftar pola umum kode yang BUKAN contoh threadsafe @result ||= some_method?
  4. Apakah struktur data dalam ruby ​​lang core seperti Hashdll threadsafe?
  5. Pada MRI, di mana terdapat GVL/GIL yang berarti hanya 1 thread ruby ​​yang dapat berjalan dalam satu waktu kecuali IO, apakah perubahan threadsafe memengaruhi kita?
CuriousMind
sumber
2
Apakah Anda yakin bahwa semua kode dan semua permata HARUS menjadi threadsafe? Apa yang dikatakan oleh catatan rilis adalah bahwa Rails sendiri akan menjadi threadsafe, bukan semua hal lain yang digunakan dengannya HARUS
bertakhtropi pada
Pengujian multi-threaded akan menjadi risiko threadsafe terburuk. Jika Anda harus mengubah nilai variabel lingkungan di sekitar kasus pengujian, Anda langsung tidak aman untuk thread. Bagaimana Anda mengatasinya? Dan ya, semua permata harus aman untuk benang.
Lukas Oberhuber

Jawaban:

110

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 += 1tidak aman untuk thread, karena ini adalah operasi ganda. @n = 1adalah 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 + btidak apa-apa, a = bjuga baik, dan a.foo(b)tidak masalah, jika metode fooini 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; endadalah 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 stuffpengakses. 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_stuffberfungsi dengan baik saat pertama kali digunakan, tetapi mengembalikan sesuatu yang lain untuk kedua kalinya. Mengapa? The load_thingsMetode terjadi untuk berpikir itu memiliki hash pilihan berlalu untuk itu, dan melakukan color = options.delete(:color). Sekarang STANDARD_OPTIONSkonstanta 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.

Theo
sumber
Jawaban yang bagus. Mempertimbangkan bahwa aplikasi rel tipikal adalah multi-proses (seperti yang Anda jelaskan, banyak pengguna berbeda mengakses aplikasi yang sama), saya bertanya-tanya apa risiko marjinal utas ke model konkurensi ... Dengan kata lain, seberapa jauh lebih "berbahaya" apakah itu berjalan dalam mode ulir jika Anda sudah berurusan dengan beberapa konkurensi melalui proses?
jahe
2
@Theo Terima kasih banyak. Hal konstan itu adalah bom besar. Ini bahkan tidak aman untuk proses. Jika konstanta diubah dalam satu permintaan, itu akan menyebabkan permintaan selanjutnya untuk melihat konstanta yang diubah bahkan dalam satu utas. Konstanta Ruby aneh
rubish
5
Lakukan STANDARD_OPTIONS = {...}.freezeuntuk meningkatkan mutasi dangkal
glebm
Jawaban yang sangat bagus
Cheyne
3
"Jika Anda menulis kode seperti @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }[...], nilai variabel bersama sesudahnya tidak deterministik." - Tahukah Anda jika ini berbeda di antara versi Ruby? Misalnya, menjalankan kode Anda pada 1.8 memberikan nilai yang berbeda @n, tetapi pada 1.9 dan yang lebih baru tampaknya secara konsisten memberikan nilai yang @nsama dengan 300.
user200783
10

Selain jawaban Theo, saya akan menambahkan beberapa area masalah yang harus diperhatikan di Rails secara khusus, jika Anda beralih ke config.threadsafe!

  • Variabel kelas :

    @@i_exist_across_threads

  • ENV :

    ENV['DONT_CHANGE_ME']

  • Benang :

    Thread.start

crizCraig.dll
sumber
9

mulai dari Rails 4, semuanya harus berjalan di lingkungan berulir secara default

Ini tidak 100% benar. Thread-safe Rails hanya aktif secara default. Jika Anda menerapkan pada server aplikasi multi-proses seperti Penumpang (komunitas) atau Unicorn, tidak akan ada perbedaan sama sekali. Perubahan ini hanya menyangkut Anda, jika Anda menerapkan pada lingkungan multi-thread seperti Puma atau Passenger Enterprise> 4.0

Di masa lalu, jika Anda ingin menerapkan pada server aplikasi multi-utas, Anda harus mengaktifkan config.threadsafe , yang sekarang menjadi default, karena semua yang dilakukannya tidak memiliki efek atau juga diterapkan ke aplikasi Rails yang berjalan dalam satu proses ( Prooflink ).

Tetapi jika Anda memang menginginkan semua manfaat streaming Rails 4 dan hal-hal waktu nyata lainnya dari penerapan multi-threaded maka mungkin Anda akan menemukan artikel ini menarik. Sedihnya @Theo, untuk aplikasi Rails, Anda sebenarnya hanya perlu menghilangkan mutasi status statis selama permintaan. Meskipun ini adalah praktik sederhana untuk diikuti, sayangnya Anda tidak dapat memastikannya untuk setiap permata yang Anda temukan. Sejauh yang saya ingat Charles Oliver Nutter dari proyek JRuby punya beberapa tip tentang itu di podcast ini .

Dan jika Anda ingin menulis pemrograman Ruby konkuren murni, di mana Anda memerlukan beberapa struktur data yang diakses oleh lebih dari satu utas, Anda mungkin akan menemukan permata thread_safe berguna.

dre-hh
sumber