edge
เพิ่มเติมที่ rubyonrails.org: เพิ่มเติมเกี่ยวกับ Ruby on Rails

การใช้เธรดและการประมวลผลโค้ดใน Rails

หลังจากอ่านเอกสารนี้คุณจะทราบ:

1 การประมวลผลโดยอัตโนมัติ

Rails อนุญาตให้ดำเนินการต่างๆ ที่เกิดขึ้นพร้อมกันโดยอัตโนมัติ

เมื่อใช้เว็บเซิร์ฟเวอร์แบบเธรด เช่น Puma เริ่มต้น จะมีการบริการคำขอ HTTP หลายรายการพร้อมกัน โดยแต่ละคำขอจะได้รับตัวควบคุมของตัวเอง

เมื่อใช้ตัวอักษร Active Job ที่ใช้เธรด รวมถึงตัวเลือก Async ที่มีอยู่ในตัว จะประมวลผลงานหลายรายการพร้อมกันเช่นกัน ช่อง Action Cable ก็จัดการด้วยวิธีเดียวกัน

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

ส่วนที่เหลือของเอกสารนี้อธิบายกลไกที่ Rails ใช้ในการทำให้ "สามารถเพิ่มรายละเอียดเพิ่มเติม" และวิธีการใช้ส่วนขยายและแอปพลิเคชันที่มีความต้องการพิเศษ

2 ผู้ปฏิบัติงาน

ผู้ปฏิบัติงานของ Rails แยกโค้ดแอปพลิเคชันออกจากโค้ดเฟรมเวิร์ก: ทุกครั้งที่เฟรมเวิร์กเรียกใช้โค้ดที่คุณเขียนในแอปพลิเคชันของคุณ โค้ดนั้นจะถูกห่อหุ้มด้วยผู้ปฏิบัติงาน

ผู้ปฏิบัติงานประกอบด้วยการเรียกใช้งานสองครั้ง: to_run และ to_complete คือ การเรียกใช้งาน Run ก่อนโค้ดแอปพลิเคชัน และการเรียกใช้งาน Complete หลังโค้ด

2.1 การเรียกใช้งานเริ่มต้น

ในแอปพลิเคชัน Rails เริ่มต้น ผู้ปฏิบัติงานจะถูกใช้ในการ:

  • ติดตามเธรดที่อยู่ในตำแหน่งที่ปลอดภัยสำหรับการโหลดและโหลดใหม่
  • เปิดใช้งานและปิดใช้งานแคชการค้นหา Active Record
  • คืนการเชื่อมต่อ Active Record ที่ได้รับไปยังพูล
  • จำกัดอายุการแคชภายใน

ก่อน Rails 5.0 บางส่วนจัดการโดยคลาส Rack middleware ที่แยกออกเป็นตัวโดยสาร (เช่น ActiveRecord::ConnectionAdapters::ConnectionManagement) หรือห่อหุ้มโค้ดโดยตรงด้วยเมธอดเช่น ActiveRecord::Base.connection_pool.with_connection ผู้ปฏิบัติงานจะแทนที่เหล่านี้ด้วยอินเทอร์เฟซที่เป็นรูปแบบที่เป็นทางการเดียว

2.2 การห่อหุ้มโค้ดแอปพลิเคชัน

หากคุณกำลังเขียนไลบรารีหรือคอมโพเนนต์ที่จะเรียกใช้โค้ดแอปพลิเคชัน คุณควรห่อหุ้มด้วยการเรียกใช้ผู้ปฏิบัติงาน:

Rails.application.executor.wrap do
  # เรียกใช้โค้ดแอปพลิเคชันที่นี่
end

เคล็ดลับ: หากคุณเรียกใช้โค้ดแอปพลิเคชันซ้ำๆ จากกระบวนการที่ทำงานอย่างต่อเนื่อง คุณอาจต้องห่อหุ้มโดยใช้ Reloader แทน

แต่ละเธรดควรห่อหุ้มก่อนที่จะเรียกใช้โค้ดแอปพลิเคชัน ดังนั้นหากแอปพลิเคชันของคุณมอบหมายงานให้กับเธรดอื่นๆ เช่นผ่าน Thread.new หรือคุณสมบัติของ Concurrent Ruby ที่ใช้พูลเธรด คุณควรห่อหุ้มบล็อกทันที: ruby Thread.new do Rails.application.executor.wrap do # โค้ดของคุณที่นี่ end end

หมายเหตุ: Concurrent Ruby ใช้ ThreadPoolExecutor ซึ่งมีการกำหนดค่า executor บางครั้ง แต่ไม่เกี่ยวข้องกัน

Executor เป็นเส้นทางที่ปลอดภัยในการเข้าถึง; หากมีการทำงานอยู่บนเธรดปัจจุบันแล้ว wrap จะไม่มีผลกระทบ

หากไม่สามารถใส่โค้ดของแอปพลิเคชันในบล็อกได้ (ตัวอย่างเช่น Rack API ทำให้เกิดปัญหา) คุณยังสามารถใช้คู่ run! / complete! ได้:

Thread.new do
  execution_context = Rails.application.executor.run!
  # โค้ดของคุณที่นี่
ensure
  execution_context.complete! if execution_context
end

2.3 การประสานความสามารถ

Executor จะใส่เธรดปัจจุบันในโหมด running ใน Load Interlock การดำเนินการนี้จะบล็อกชั่วคราวหากเธรดอื่นกำลังโหลดค่าคงที่หรือยกเลิกการโหลดเพื่อโหลดใหม่แอปพลิเคชัน

3 Reloader

เช่นเดียวกับ Executor Reloader ยังครอบคลุมโค้ดของแอปพลิเคชัน หาก Executor ยังไม่ทำงานอยู่บนเธรดปัจจุบัน Reloader จะเรียกใช้ Executor ให้คุณเพียงครั้งเดียว ดังนั้นคุณเพียงแค่เรียกใช้เพียงอันเดียวนี้ นี่ยังรับรองว่าทุกอย่างที่ Reloader ทำ รวมถึงการเรียกใช้ callback ทั้งหมด เกิดขึ้นใน Executor

Rails.application.reloader.wrap do
  # เรียกใช้โค้ดของแอปพลิเคชันที่นี่
end

Reloader เหมาะสำหรับกรณีที่กรอบการทำงานระดับเฟรมเวิร์กที่ใช้เวลานานเรียกใช้โค้ดของแอปพลิเคชันซ้ำๆ เช่นเซิร์ฟเวอร์เว็บหรือคิวงาน รูปแบบนี้ Rails จะครอบคลุมการร้องขอเว็บและ Active Job workers โดยอัตโนมัติ ดังนั้นคุณจะใช้ Reloader นายจะน้อยมาก เสมอพิจารณาว่า Executor เหมาะสำหรับกรณีใช้ของคุณ

3.1 การเรียกใช้ Callbacks

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

Reloader ยังมีการให้ callback to_run และ to_complete ที่จะถูกเรียกใช้ พวกเขาจะถูกเรียกใช้ในจุดเดียวกันกับ Executor แต่เฉพาะเมื่อการดำเนินการปัจจุบันเริ่มต้นโหลดแอปพลิเคชันใหม่ หากไม่พบการโหลดใหม่ Reloader จะเรียกใช้บล็อกที่ครอบคลุมโดยไม่มี callback อื่น

3.2 การยกเลิกคลาส

ส่วนสำคัญที่สุดของกระบวนการโหลดใหม่คือการยกเลิกคลาส ที่ทำให้คลาสที่โหลดอัตโนมัติถูกลบออกและพร้อมที่จะโหลดใหม่ นี้จะเกิดขึ้นทันทีก่อน Run หรือ Complete callback ขึ้นอยู่กับการตั้งค่า reload_classes_only_on_change บางครั้งจำเป็นต้องทำการโหลดใหม่เพิ่มเติมก่อนหรือหลังการ Unload คลาส ดังนั้น Reloader ยังมีการให้บริการ before_class_unload และ after_class_unload callbacks

3.3 การทำงานแบบพร้อมกัน

เฉพาะกระบวนการ "ระดับบน" ที่ใช้เวลานานเท่านั้นที่ควรเรียกใช้ Reloader เพราะหากมีการตรวจสอบว่าต้องโหลดใหม่ Reloader จะบล็อกจนกว่าเธรดอื่น ๆ ทั้งหมดจะเสร็จสิ้นการเรียกใช้ Executor

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

4 พฤติกรรมของเฟรมเวิร์ก

คอมโพเนนต์ของเฟรมเวิร์กใช้เครื่องมือเหล่านี้ในการจัดการความพร้อมทางการแข่งขันของตนเองด้วย

ActionDispatch::Executor และ ActionDispatch::Reloader เป็น Rack middlewares ที่ห่อหุ้มคำขอด้วย Executor หรือ Reloader ที่กำหนดไว้ พวกเขาถูกเพิ่มอัตโนมัติในสแต็กแอปพลิเคชันเริ่มต้น Reloader จะตรวจสอบให้แน่ใจว่าคำขอ HTTP ที่มาถึงจะได้รับการบริการด้วยสำเนาของแอปพลิเคชันที่โหลดใหม่ล่าสุดหากมีการเปลี่ยนแปลงโค้ด

Active Job ยังห่อหุ้มการดำเนินการงานของตนด้วย Reloader เพื่อโหลดโค้ดล่าสุดเพื่อดำเนินการงานแต่ละงานเมื่อออกจากคิว

Action Cable ใช้ Executor แทน: เนื่องจากการเชื่อมต่อ Cable เชื่อมโยงกับตัวอย่างของคลาสที่เฉพาะเจาะจง ไม่สามารถโหลดใหม่ได้สำหรับทุกข้อความ WebSocket ที่มาถึง เพียงแค่ตัวจัดการข้อความถูกห่อหุ้มเท่านั้น; การเชื่อมต่อ Cable ที่ใช้เวลานานไม่ส่งผลให้เกิดการโหลดใหม่ที่ถูกเรียกใช้โดยคำขอหรืองานเข้าใหม่แทน Action Cable ใช้ before_class_unload callback ของ Reloader เพื่อตัดการเชื่อมต่อทั้งหมด และเมื่อไคลเอ็นต์เชื่อมต่ออัตโนมัติเข้ามา จะเป็นการสื่อสารกับรุ่นใหม่ของโค้ด

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

4.1 การกำหนดค่า

Reloader จะตรวจสอบการเปลี่ยนแปลงของไฟล์เท่านั้นเมื่อ config.enable_reloading เป็น true และ config.reload_classes_only_on_change เป็น true นี่คือค่าเริ่มต้นในสภาพแวดล้อม development

เมื่อ config.enable_reloading เป็น false (ใน production ตามค่าเริ่มต้น) Reloader จะเป็นเพียงผ่านทางไปยัง Executor

Executor เสมอมีงานสำคัญที่ต้องทำ เช่นการจัดการการเชื่อมต่อฐานข้อมูล เมื่อ config.enable_reloading เป็น false และ config.eager_load เป็น true (ค่าเริ่มต้นของ production) จะไม่มีการโหลดใหม่เกิดขึ้นดังนั้นไม่จำเป็นต้องใช้ Load Interlock กับการตั้งค่าเริ่มต้นในสภาพแวดล้อม development Executor จะใช้ Load Interlock เพื่อให้แน่ใจว่าค่าคงที่ถูกโหลดเฉพาะเมื่อปลอดภัย

5 การล็อคโหลด

การล็อคโหลดช่วยให้การโหลดอัตโนมัติและโหลดใหม่สามารถเปิดใช้งานในสภาวะการทำงานแบบหลายเธรดได้

เมื่อเธรดหนึ่งกำลังดำเนินการโหลดอัตโนมัติโดยประเมินคำจำกัดความของคลาสจากไฟล์ที่เหมาะสม สิ่งสำคัญคือไม่มีเธรดอื่นที่พบการอ้างอิงถึงค่าคงที่ที่กำหนดไว้บางส่วน

อย่างเช่นเดียวกัน การโหลด/โหลดใหม่เป็นปลอดภัยเท่านั้นเมื่อไม่มีรหัสแอปพลิเคชันที่กำลังดำเนินการอยู่ในระหว่างการดำเนินการ: หลังจากการโหลดใหม่ ค่าคงที่ User ตัวอย่างเช่นอาจชี้ไปที่คลาสที่แตกต่างกัน โดยไม่มีกฎนี้ การโหลดในเวลาที่ไม่เหมาะสมอาจหมายความว่า User.new.class == User หรือแม้แต่ User == User อาจเป็นเท็จ

ข้อจำกัดทั้งสองนี้ถูกแก้ไขโดยการล็อคโหลด มันติดตามว่าเธรดใดกำลังทำงานรหัสแอปพลิเคชัน โหลดคลาส หรือยกเลิกการโหลดค่าคงที่โดยอัตโนมัติ

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

5.1 permit_concurrent_loads

Executor จะเรียกใช้ running lock โดยอัตโนมัติตลอดระยะเวลาของบล็อก และ autoload รู้ว่าเมื่อจะอัพเกรดเป็น load lock และสลับกลับเป็น running อีกครั้งหลังจากนั้น

การดำเนินการที่บล็อก Executor (ซึ่งรวมถึงรหัสแอปพลิเคชันทั้งหมด) ที่บล็อกการทำงานอาจเก็บรักษา running lock อย่างไม่จำเป็น หากเธรดอื่นพบค่าคงที่ที่ต้องโหลดอัตโนมัตินี้อาจทำให้เกิดการติดกัน

ตัวอย่างเช่น ถ้า User ยังไม่ได้โหลด ต่อไปนี้จะเกิดการติดกัน:

Rails.application.executor.wrap do
  th = Thread.new do
    Rails.application.executor.wrap do
      User # เธรดภายในรอที่นี่ มันไม่สามารถโหลด
           # User ในขณะที่เธรดอื่นกำลังทำงาน
    end
  end

  th.join # เธรดภายนอกรอที่นี่ ถือ 'running' lock
end

เพื่อป้องกันการติดกันนี้ เธรดภายนอกสามารถ permit_concurrent_loads ได้ โดยการเรียกใช้เมธอดนี้ เธรดรับรองว่าจะไม่อ้างอิงค่าคงที่ที่โหลดอัตโนมัติใด ๆ ภายในบล็อกที่ให้ไว้ วิธีที่ปลอดภัยที่สุดในการปฏิบัติตามสัญญานั้นคือให้ใช้ใกล้ที่สุดกับการเรียกใช้ที่บล็อก:

Rails.application.executor.wrap do
  th = Thread.new do
    Rails.application.executor.wrap do
      User # เธรดภายในสามารถรับ 'load' lock
           # โหลด User และดำเนินการต่อได้
    end
  end

  ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
    th.join # เธรดภายนอกรอที่นี่ แต่ไม่มีล็อค
  end
end

ตัวอย่างอื่น ๆ โดยใช้ Concurrent Ruby:

Rails.application.executor.wrap do
  futures = 3.times.collect do |i|
    Concurrent::Promises.future do
      Rails.application.executor.wrap do
        # ทำงานที่นี่
      end
    end
  end

  values = ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
    futures.collect(&:value)
  end
end

5.2 ActionDispatch::DebugLocks

หากแอปพลิเคชันของคุณติดล็อกและคุณคิดว่า Load Interlock อาจเกี่ยวข้อง คุณสามารถเพิ่ม ActionDispatch::DebugLocks middleware ไปยัง config/application.rb ชั่วคราวได้:

config.middleware.insert_before Rack::Sendfile,
                                  ActionDispatch::DebugLocks

หากคุณรีสตาร์ทแอปพลิเคชันและเรียกใช้เงื่อนไขการติดล็อกอีกครั้ง /rails/locks จะแสดงสรุปของเธรดทั้งหมดที่รู้จักในปัจจุบันโดย interlock ว่ามีการถือหรือรอล็อกระดับใดและแสดง backtrace ปัจจุบันของเธรด

โดยทั่วไปแล้ว การติดล็อกจะเกิดจาก interlock ที่ขัดแย้งกับล็อกภายนอกหรือการเรียกใช้ I/O ที่บล็อก หลังจากคุณค้นพบแล้วคุณสามารถใช้ permit_concurrent_loads ครอบห่อได้

ข้อเสนอแนะ

คุณสามารถช่วยปรับปรุงคุณภาพของคู่มือนี้ได้

กรุณาช่วยเพิ่มเติมหากพบข้อผิดพลาดหรือข้อผิดพลาดทางความจริง เพื่อเริ่มต้นคุณสามารถอ่านส่วน การสนับสนุนเอกสาร ของเราได้

คุณอาจพบเนื้อหาที่ไม่สมบูรณ์หรือเนื้อหาที่ไม่ได้อัปเดต กรุณาเพิ่มเอกสารที่ขาดหายไปสำหรับเนื้อหาหลัก โปรดตรวจสอบ Edge Guides ก่อนเพื่อตรวจสอบ ว่าปัญหาได้รับการแก้ไขหรือไม่ในสาขาหลัก ตรวจสอบ คู่มือแนวทาง Ruby on Rails เพื่อดูรูปแบบและกฎเกณฑ์

หากคุณพบข้อผิดพลาดแต่ไม่สามารถแก้ไขได้เอง กรุณา เปิดปัญหา.

และสุดท้าย การสนทนาใด ๆ เกี่ยวกับ Ruby on Rails เอกสารยินดีต้อนรับที่สุดใน เว็บบอร์ดอย่างเป็นทางการของ Ruby on Rails.