จะรู้ได้อย่างไรว่าอะไรไม่ปลอดภัยในทับทิม?


93

เริ่มต้นจาก Rails 4ทุกอย่างจะต้องทำงานในสภาพแวดล้อมแบบเธรดโดยค่าเริ่มต้น ความหมายนี้คือโค้ดทั้งหมดที่เราเขียนและ อัญมณีทั้งหมดที่เราใช้จะต้องเป็นthreadsafe

ดังนั้นฉันมีคำถามสองสามข้อเกี่ยวกับเรื่องนี้:

  1. อะไรที่ไม่ปลอดภัยต่อเกลียวในทับทิม / ราง? Vsอะไรคือความปลอดภัยของเกลียวในทับทิม / ราง?
  2. มีรายการของอัญมณีที่เป็นที่รู้จักกันเป็นด้ายหรือในทางกลับกัน?
  3. มีรายการรูปแบบทั่วไปของรหัสที่ไม่ใช่ตัวอย่างที่ปลอดภัย@result ||= some_methodหรือไม่
  4. โครงสร้างข้อมูลใน Ruby lang core เช่นHashetc threadsafe หรือไม่?
  5. ใน MRI ซึ่งมีGVL/GILซึ่งหมายความว่าเธรดทับทิมเพียง 1 เส้นเท่านั้นที่สามารถทำงานได้ในแต่ละครั้งยกเว้นIOเธรดที่ปลอดภัยมีผลต่อเราหรือไม่?

2
คุณแน่ใจหรือไม่ว่ารหัสทั้งหมดและอัญมณีทั้งหมดจะต้องปลอดภัย สิ่งที่บันทึกประจำรุ่นกล่าวคือ Rails จะเป็นเธรดที่ปลอดภัยไม่ใช่ว่าทุกอย่างที่ใช้กับมันจะเป็น
enthrops

การทดสอบแบบหลายเธรดจะเป็นความเสี่ยงที่ปลอดภัยที่สุดสำหรับเธรด เมื่อคุณต้องเปลี่ยนค่าของตัวแปรสภาพแวดล้อมรอบ ๆ กรณีทดสอบของคุณคุณจะไม่ปลอดภัยในทันที คุณจะแก้ไขปัญหานั้นอย่างไร? และใช่อัญมณีทั้งหมดจะต้องปลอดภัย
Lukas Oberhuber

คำตอบ:


110

โครงสร้างข้อมูลหลักไม่มีเธรดที่ปลอดภัย สิ่งเดียวที่ฉันรู้จักที่มาพร้อมกับ Ruby คือการนำคิวไปใช้ในไลบรารีมาตรฐาน ( require 'thread'; q = Queue.new)

GIL ของ MRI ไม่ได้ช่วยเราจากปัญหาด้านความปลอดภัยของด้าย ตรวจสอบให้แน่ใจว่าเธรดสองเธรดไม่สามารถรันโค้ด Ruby พร้อมกันได้นั่นคือบน CPU สองตัวในเวลาเดียวกัน เธรดยังคงสามารถหยุดชั่วคราวและเปิดต่อได้ทุกเมื่อในโค้ดของคุณ หากคุณเขียนโค้ด@n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }เช่นการกลายพันธุ์ตัวแปรที่ใช้ร่วมกันจากหลายเธรดค่าของตัวแปรที่ใช้ร่วมกันหลังจากนั้นจะไม่ถูกกำหนด GIL เป็นแบบจำลองของระบบแกนเดียวไม่มากก็น้อยจะไม่เปลี่ยนประเด็นพื้นฐานของการเขียนโปรแกรมพร้อมกันที่ถูกต้อง

แม้ว่า MRI จะเป็นเธรดเดียวเช่น Node.js คุณก็ยังต้องคิดถึงการทำงานพร้อมกัน ตัวอย่างที่มีตัวแปรที่เพิ่มขึ้นจะใช้งานได้ดี แต่คุณยังสามารถรับเงื่อนไขการแข่งขันที่สิ่งต่าง ๆ เกิดขึ้นในลำดับที่ไม่ได้กำหนดและหนึ่งกลุ่มการเรียกกลับเป็นผลลัพธ์ของอีกตัวแปรหนึ่ง ระบบอะซิงโครนัสแบบเธรดเดี่ยวนั้นง่ายกว่าในการให้เหตุผล แต่ก็ไม่ปลอดจากปัญหาการทำงานพร้อมกัน ลองนึกถึงแอปพลิเคชันที่มีผู้ใช้หลายคน: หากผู้ใช้สองคนกดแก้ไขโพสต์ Stack Overflow ในเวลาเดียวกันไม่มากก็น้อยให้ใช้เวลาแก้ไขโพสต์แล้วกดบันทึกซึ่งผู้ใช้คนที่สามจะเห็นการเปลี่ยนแปลงในภายหลังเมื่อ อ่านโพสต์เดียวกันไหม

ใน Ruby เช่นเดียวกับการทำงานพร้อมกันอื่น ๆ ส่วนใหญ่สิ่งที่มากกว่าหนึ่งการดำเนินการจะไม่ปลอดภัยต่อเธรด @n += 1เธรดไม่ปลอดภัยเนื่องจากเป็นการดำเนินการหลายอย่าง @n = 1เธรดปลอดภัยเนื่องจากเป็นการดำเนินการเดียว (เป็นการดำเนินการจำนวนมากภายใต้ประทุนและฉันอาจประสบปัญหาหากพยายามอธิบายว่าทำไมเธรดจึง "ปลอดภัย" โดยละเอียด แต่สุดท้ายคุณจะไม่ได้รับผลลัพธ์ที่ไม่สอดคล้องกันจากการมอบหมายงาน ). @n ||= 1ไม่ใช่และไม่มีการดำเนินการชวเลขอื่น ๆ + การมอบหมาย ความผิดพลาดอย่างหนึ่งที่ฉันเคยทำหลายครั้งคือการเขียนreturn unless @started; @started = trueซึ่งไม่ปลอดภัยเลย

ฉันไม่ทราบรายการข้อความที่เชื่อถือได้ของเธรดที่ปลอดภัยและไม่ใช่เธรดที่ปลอดภัยสำหรับ Ruby แต่มีกฎง่ายๆคือหากนิพจน์ทำการดำเนินการเพียงอย่างเดียว (ไม่มีผลข้างเคียง) แสดงว่าเธรดปลอดภัย ตัวอย่างเช่นa + bมีการตกลงa = bเป็นยัง ok และa.foo(b)จะ ok ถ้าวิธีการที่fooเป็นผลข้างเคียงฟรี (ตั้งแต่เพียงเกี่ยวกับอะไรในรูบีเป็นวิธีการเรียกแม้งานในหลาย ๆ กรณีนี้ไปสำหรับตัวอย่างอื่น ๆ ด้วย) ผลข้างเคียงในบริบทนี้หมายถึงสิ่งที่เปลี่ยนสถานะ def foo(x); @x = x; endคือไม่ได้ผลข้างเคียงฟรี

สิ่งที่ยากที่สุดอย่างหนึ่งเกี่ยวกับการเขียนโค้ดที่ปลอดภัยของเธรดใน Ruby คือโครงสร้างข้อมูลหลักทั้งหมดรวมถึงอาร์เรย์แฮชและสตริงนั้นไม่แน่นอน เป็นเรื่องง่ายมากที่จะทำให้ชิ้นส่วนของสถานะของคุณรั่วไหลโดยไม่ตั้งใจและเมื่อชิ้นส่วนนั้นเป็นสิ่งที่ไม่แน่นอนอาจทำให้เสียหายได้ พิจารณารหัสต่อไปนี้:

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

อินสแตนซ์ของคลาสนี้สามารถใช้ร่วมกันระหว่างหัวข้อและพวกเขาได้อย่างปลอดภัยสามารถเพิ่มสิ่งที่มัน แต่มีข้อผิดพลาดการทำงานพร้อมกัน (มันไม่ได้เป็นเพียงคนเดียว): รัฐภายในของการรั่วไหลของวัตถุที่ผ่านstuffการเข้าถึง นอกจากจะมีปัญหาจากมุมมองของการห่อหุ้มแล้วมันยังเปิดโอกาสของเวิร์มที่เกิดขึ้นพร้อมกัน อาจมีใครบางคนใช้อาร์เรย์นั้นและส่งต่อไปยังที่อื่นและรหัสนั้นก็คิดว่าตอนนี้เป็นเจ้าของอาร์เรย์นั้นและสามารถทำอะไรก็ได้ที่ต้องการ

อีกตัวอย่างคลาสสิกของ Ruby คือ:

STANDARD_OPTIONS = {:color => 'red', :count => 10}

def find_stuff
  @some_service.load_things('stuff', STANDARD_OPTIONS)
end

find_stuffใช้งานได้ดีในครั้งแรกที่ใช้ แต่ส่งคืนอย่างอื่นในครั้งที่สอง ทำไม? วิธีการที่เกิดขึ้นจะคิดว่ามันเป็นเจ้าของกัญชาตัวเลือกผ่านไปและไม่load_things color = options.delete(:color)ตอนนี้STANDARD_OPTIONSค่าคงที่ไม่มีค่าเหมือนเดิมอีกต่อไป ค่าคงที่เป็นค่าคงที่ในสิ่งที่อ้างอิงเท่านั้นไม่รับประกันความคงที่ของโครงสร้างข้อมูลที่อ้างถึง แค่คิดว่าจะเกิดอะไรขึ้นถ้าโค้ดนี้ทำงานพร้อมกัน

หากคุณหลีกเลี่ยงสภาวะที่ไม่สามารถใช้ร่วมกันได้ (เช่นตัวแปรอินสแตนซ์ในออบเจ็กต์ที่เข้าถึงโดยหลายเธรดโครงสร้างข้อมูลเช่นแฮชและอาร์เรย์ที่เข้าถึงโดยเธรดหลายเธรด) ความปลอดภัยของเธรดนั้นไม่ยากนัก พยายามย่อส่วนของแอปพลิเคชันของคุณที่เข้าถึงพร้อมกันและมุ่งเน้นความพยายามของคุณที่นั่น IIRC ในแอ็พพลิเคชัน Rails อ็อบเจ็กต์คอนโทรลเลอร์ใหม่จะถูกสร้างขึ้นสำหรับทุกคำร้องขอดังนั้นมันจะถูกใช้โดยเธรดเดียวเท่านั้นและสิ่งเดียวกันนี้จะใช้กับอ็อบเจ็กต์โมเดลใด ๆ ที่คุณสร้างจากคอนโทรลเลอร์นั้น อย่างไรก็ตาม Rails ยังสนับสนุนให้ใช้ตัวแปรส่วนกลาง ( User.find(...)ใช้ตัวแปร globalUserคุณอาจคิดว่ามันเป็นเพียงคลาสและเป็นคลาส แต่ก็เป็นเนมสเปซสำหรับตัวแปรส่วนกลางเช่นกัน) บางส่วนมีความปลอดภัยเนื่องจากเป็นแบบอ่านอย่างเดียว แต่บางครั้งคุณบันทึกสิ่งต่างๆไว้ในตัวแปรส่วนกลางเหล่านี้เนื่องจาก สะดวก ระมัดระวังอย่างยิ่งเมื่อคุณใช้สิ่งที่สามารถเข้าถึงได้ทั่วโลก

เป็นไปได้ที่จะเรียกใช้ Rails ในสภาพแวดล้อมแบบเธรดมาระยะหนึ่งแล้วดังนั้นหากไม่เป็นผู้เชี่ยวชาญด้าน Rails ฉันก็ยังคงพูดได้ว่าคุณไม่ต้องกังวลเกี่ยวกับความปลอดภัยของเธรดเมื่อพูดถึง Rails เอง คุณยังคงสามารถสร้างแอปพลิเคชัน Rails ที่ไม่ปลอดภัยสำหรับเธรดได้โดยทำบางสิ่งที่ฉันกล่าวถึงข้างต้น เมื่อพูดถึงอัญมณีอื่น ๆ จะคิดว่าพวกเขาไม่ปลอดภัยเว้นแต่พวกเขาจะบอกว่าพวกเขาเป็นและถ้าพวกเขาบอกว่าพวกเขาคิดว่าพวกเขาไม่ใช่และมองผ่านรหัสของพวกเขา (แต่เพียงเพราะคุณเห็นว่าพวกเขาไปในสิ่งต่างๆเช่น@n ||= 1 ไม่ได้หมายความว่าพวกมันไม่ปลอดภัยเธรดนั่นเป็นสิ่งที่ถูกต้องตามกฎหมายอย่างสมบูรณ์ที่จะทำในบริบทที่ถูกต้อง - คุณควรมองหาสิ่งต่างๆเช่นสถานะที่ไม่แน่นอนในตัวแปรส่วนกลางแทนวิธีจัดการกับวัตถุที่เปลี่ยนแปลงไม่ได้ที่ส่งผ่านไปยังวิธีการของมันและโดยเฉพาะอย่างยิ่งวิธีการนั้น จัดการแฮชตัวเลือก)

สุดท้ายการไม่ปลอดภัยของเธรดเป็นคุณสมบัติสกรรมกริยา สิ่งใดก็ตามที่ใช้สิ่งที่ไม่ปลอดภัยต่อเธรดคือตัวมันเองไม่ปลอดภัยต่อเธรด


คำตอบที่ดี เมื่อพิจารณาว่าแอปพลิเคชันรางทั่วไปเป็นแบบหลายกระบวนการ (เช่นเดียวกับที่คุณอธิบายผู้ใช้หลายคนที่เข้าถึงแอปเดียวกัน) ฉันสงสัยว่าความเสี่ยงเล็กน้อยของเธรดต่อรูปแบบการทำงานพร้อมกันคืออะไร ... กล่าวอีกนัยหนึ่ง "อันตราย" มากขึ้นเพียงใด มันจะทำงานในโหมดเธรดหรือไม่หากคุณกำลังจัดการกับการทำงานพร้อมกันบางอย่างผ่านกระบวนการ?
Gingerlime

2
@ พวกขอบคุณตัน สิ่งที่คงที่เป็นระเบิดลูกใหญ่ มันไม่ปลอดภัยแม้แต่กระบวนการ หากค่าคงที่ได้รับการเปลี่ยนแปลงในหนึ่งคำขอจะทำให้คำขอในภายหลังเห็นค่าคงที่ที่เปลี่ยนแปลงแม้ในเธรดเดียว ค่าคงที่ของทับทิมนั้นแปลก
ถู

5
ทำSTANDARD_OPTIONS = {...}.freezeเพื่อเพิ่มการกลายพันธุ์ที่ตื้น
glebm

คำตอบที่ยอดเยี่ยมจริงๆ
Cheyne

3
"ถ้าคุณเขียนโค้ดเช่น@n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }[... ] ค่าของตัวแปรที่แชร์หลังจากนั้นจะไม่ถูกกำหนด" - คุณรู้หรือไม่ว่าสิ่งนี้แตกต่างระหว่าง Ruby รุ่นใด? ตัวอย่างเช่นการรันโค้ดของคุณบน 1.8 จะให้ค่าที่แตกต่างกัน@nแต่ใน 1.9 และหลังจากนั้นดูเหมือนว่าจะให้@nเท่ากับ 300 อย่างต่อเนื่อง
user200783

10

นอกจากคำตอบของธีโอแล้วฉันจะเพิ่มพื้นที่ปัญหาสองสามส่วนเพื่อค้นหาใน Rails โดยเฉพาะหากคุณเปลี่ยนไปใช้ config.threadsafe!

  • ตัวแปรคลาส :

    @@i_exist_across_threads

  • ENV :

    ENV['DONT_CHANGE_ME']

  • หัวข้อ :

    Thread.start


9

เริ่มจาก Rails 4 ทุกอย่างจะต้องทำงานในสภาพแวดล้อมเธรดโดยค่าเริ่มต้น

สิ่งนี้ไม่ถูกต้อง 100% Thread-safe Rails จะเปิดโดยค่าเริ่มต้น หากคุณปรับใช้บนเซิร์ฟเวอร์แอปแบบหลายกระบวนการเช่น Passenger (community) หรือ Unicorn จะไม่มีความแตกต่างเลย การเปลี่ยนแปลงนี้เกี่ยวข้องกับคุณเท่านั้นหากคุณปรับใช้ในสภาพแวดล้อมแบบมัลติเธรดเช่น Puma หรือ Passenger Enterprise> 4.0

ในอดีตหากคุณต้องการปรับใช้บนเซิร์ฟเวอร์แอปแบบมัลติเธรดคุณต้องเปิดconfig.threadsafeซึ่งเป็นค่าเริ่มต้นในขณะนี้เนื่องจากสิ่งที่ทำไม่มีเอฟเฟกต์หรือใช้กับแอป Rails ที่ทำงานในกระบวนการเดียว ( Prooflink )

แต่ถ้าคุณทำต้องการให้ทุกทางรถไฟ 4 สตรีมมิ่งประโยชน์และสิ่งที่เวลาจริงอื่น ๆ ของการใช้งานแบบมัลติเธรดแล้วบางทีคุณอาจจะพบนี้น่าสนใจบทความ ในฐานะที่เป็น @ ที่น่าเศร้าสำหรับแอป Rails คุณเพียงแค่ต้องละเว้นสถานะคงที่กลายพันธุ์ในระหว่างการร้องขอ แม้ว่านี่จะเป็นแนวทางปฏิบัติง่ายๆ แต่น่าเสียดายที่คุณไม่สามารถมั่นใจได้ในทุกอัญมณีที่คุณพบ เท่าที่ฉันจำ Charles Oliver Nutter จากโครงการ JRuby มีเคล็ดลับเกี่ยวกับเรื่องนี้ในพอดคาสต์นี้

และหากคุณต้องการเขียนโปรแกรม Ruby พร้อมกันแบบบริสุทธิ์ซึ่งคุณจะต้องมีโครงสร้างข้อมูลบางอย่างที่เข้าถึงได้โดยมากกว่าหนึ่งเธรดคุณอาจพบว่าอัญมณีthread_safeมีประโยชน์

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.