การทำงานพร้อมกันของ Ruby และ Parallelism: บทช่วยสอนเชิงปฏิบัติ
เผยแพร่แล้ว: 2022-03-11มาเริ่มกันด้วยการขจัดความสับสนระหว่างนักพัฒนา Ruby ที่ธรรมดาเกินไป กล่าวคือ: การทำงานพร้อมกันและความเท่าเทียมกัน ไม่ใช่ สิ่งเดียวกัน (เช่น พร้อมกัน != ขนาน)
โดยเฉพาะอย่างยิ่ง การทำงาน พร้อมกัน ของ Ruby คือการที่งานสองงานสามารถเริ่มต้น เรียกใช้ และเสร็จสิ้นได้ในช่วงเวลาที่ ทับซ้อนกัน ไม่จำเป็นต้องหมายความว่าทั้งคู่จะทำงานพร้อมกันในทันที (เช่น หลายเธรดบนเครื่องแกนเดียว) ในทางตรงกันข้าม ความ ขนาน คือเมื่องานสองงานทำงาน พร้อมกัน อย่างแท้จริง (เช่น หลายเธรดบนตัวประมวลผลแบบมัลติคอร์)
ประเด็นสำคัญที่นี่คือเธรดและ/หรือกระบวนการที่เกิดขึ้นพร้อมกัน ไม่จำเป็น ต้องทำงานแบบคู่ขนานกัน
บทช่วยสอนนี้ให้การปฏิบัติจริง (แทนที่จะเป็นเชิงทฤษฎี) สำหรับเทคนิคและวิธีการต่างๆ ที่พร้อมใช้งานสำหรับการทำงานพร้อมกันและความเท่าเทียมกันใน Ruby
สำหรับตัวอย่าง Ruby ในชีวิตจริงเพิ่มเติม โปรดดูบทความเกี่ยวกับ Ruby Interpreters and Runtimes ของเรา
กรณีทดสอบของเรา
สำหรับกรณีทดสอบอย่างง่าย ฉันจะสร้างคลาส Mailer
และเพิ่มฟังก์ชัน Fibonacci (แทนที่จะเป็นวิธี sleep()
) เพื่อให้คำขอแต่ละรายการใช้ CPU มากขึ้น ดังนี้:
class Mailer def self.deliver(&block) mail = MailBuilder.new(&block).mail mail.send_mail end Mail = Struct.new(:from, :to, :subject, :body) do def send_mail fib(30) puts "Email from: #{from}" puts "Email to : #{to}" puts "Subject : #{subject}" puts "Body : #{body}" end def fib(n) n < 2 ? n : fib(n-1) + fib(n-2) end end class MailBuilder def initialize(&block) @mail = Mail.new instance_eval(&block) end attr_reader :mail %w(from to subject body).each do |m| define_method(m) do |val| @mail.send("#{m}=", val) end end end end
จากนั้นเราสามารถเรียกใช้คลาส Mailer
ดังต่อไปนี้เพื่อส่งจดหมาย:
Mailer.deliver do from "[email protected]" to "[email protected]" subject "Threading and Forking" body "Some content" end
(หมายเหตุ: ซอร์สโค้ดสำหรับกรณีทดสอบนี้มีให้ที่นี่บน github)
ในการสร้างเส้นฐานเพื่อวัตถุประสงค์ในการเปรียบเทียบ ให้เริ่มต้นด้วยการทำการเปรียบเทียบแบบง่ายๆ โดยเรียกใช้จดหมาย 100 ครั้ง:
puts Benchmark.measure{ 100.times do |i| Mailer.deliver do from "eki_#{i}@eqbalq.com" to "jill_#{i}@example.com" subject "Threading and Forking (#{i})" body "Some content" end end }
สิ่งนี้ให้ผลลัพธ์ต่อไปนี้บนโปรเซสเซอร์ Quad-core ที่มี MRI Ruby 2.0.0p353:
15.250000 0.020000 15.270000 ( 15.304447)
หลายกระบวนการกับมัลติเธรด
ไม่มีคำตอบ "ขนาดเดียวที่เหมาะกับทุกคน" เมื่อต้องตัดสินใจว่าจะใช้หลายกระบวนการหรือกับแอปพลิเคชัน Ruby ของคุณแบบมัลติเธรด ตารางด้านล่างสรุปปัจจัยสำคัญบางประการที่ต้องพิจารณา
กระบวนการ | กระทู้ |
---|---|
ใช้หน่วยความจำมากขึ้น | ใช้หน่วยความจำน้อยลง |
ถ้าพ่อแม่ตายก่อนที่ลูกจะจากไป เด็กอาจกลายเป็นซอมบี้ได้ | เธรดทั้งหมดตายเมื่อกระบวนการตาย (ไม่มีโอกาสเป็นซอมบี้) |
มีราคาแพงกว่าสำหรับกระบวนการที่แยกกันเพื่อสลับบริบทเนื่องจากระบบปฏิบัติการจำเป็นต้องบันทึกและโหลดใหม่ทั้งหมด | เธรดมีค่าใช้จ่ายน้อยกว่ามากเนื่องจากใช้พื้นที่ที่อยู่และหน่วยความจำร่วมกัน |
กระบวนการที่แยกกันจะได้รับพื้นที่หน่วยความจำเสมือนใหม่ (การแยกกระบวนการ) | เธรดใช้หน่วยความจำร่วมกัน จึงต้องควบคุมและจัดการกับปัญหาหน่วยความจำที่เกิดขึ้นพร้อมกัน |
ต้องใช้การสื่อสารระหว่างกระบวนการ | สามารถ "สื่อสาร" ผ่านคิวและหน่วยความจำที่ใช้ร่วมกันได้ |
ช้ากว่าในการสร้างและทำลาย | สร้างและทำลายเร็วขึ้น |
ง่ายต่อการเขียนโค้ดและดีบัก | โค้ดและดีบักอาจซับซ้อนกว่าอย่างเห็นได้ชัด |
ตัวอย่างของโซลูชัน Ruby ที่ใช้หลายกระบวนการ:
- Resque: ไลบรารี Ruby ที่ Redis สำรองไว้สำหรับสร้างงานพื้นหลัง วางไว้บนหลายคิว และประมวลผลในภายหลัง
- Unicorn: เซิร์ฟเวอร์ HTTP สำหรับแอปพลิเคชัน Rack ที่ออกแบบมาเพื่อให้บริการลูกค้าที่รวดเร็วในการเชื่อมต่อที่มีเวลาแฝงต่ำ แบนด์วิดท์สูง และใช้ประโยชน์จากคุณสมบัติต่างๆ ในเคอร์เนลที่เหมือน Unix/Unix
ตัวอย่างของโซลูชัน Ruby ที่ใช้มัลติเธรด:
- Sidekiq: กรอบการประมวลผลพื้นหลังที่มีคุณสมบัติครบถ้วนสำหรับ Ruby มีจุดมุ่งหมายเพื่อให้ง่ายต่อการรวมเข้ากับแอปพลิเคชัน Rails ที่ทันสมัยและประสิทธิภาพที่สูงกว่าโซลูชันอื่นที่มีอยู่
- Puma: เว็บเซิร์ฟเวอร์ Ruby ที่สร้างขึ้นเพื่อการทำงานพร้อมกัน
- บาง: เว็บเซิร์ฟเวอร์ Ruby ที่รวดเร็วและเรียบง่าย
หลายกระบวนการ
ก่อนที่เราจะพิจารณาตัวเลือกมัลติเธรดของ Ruby เรามาสำรวจเส้นทางที่ง่ายกว่าในการวางไข่หลายกระบวนการ
ใน Ruby การเรียกระบบ fork()
ถูกใช้เพื่อสร้าง "สำเนา" ของกระบวนการปัจจุบัน กระบวนการใหม่นี้มีการจัดกำหนดการที่ระดับระบบปฏิบัติการ ดังนั้นจึงสามารถทำงานควบคู่ไปกับกระบวนการดั้งเดิม เช่นเดียวกับกระบวนการอิสระอื่นๆ ( หมายเหตุ: fork()
เป็นการเรียกระบบ POSIX ดังนั้นจึงไม่สามารถใช้งานได้หากคุณใช้งาน Ruby บนแพลตฟอร์ม Windows)
ตกลง เรามาเรียกใช้กรณีทดสอบของเรากัน แต่คราวนี้ใช้ fork()
เพื่อใช้งานหลายกระบวนการ:
puts Benchmark.measure{ 100.times do |i| fork do Mailer.deliver do from "eki_#{i}@eqbalq.com" to "jill_#{i}@example.com" subject "Threading and Forking (#{i})" body "Some content" end end end Process.waitall }
( Process.waitall
รอให้กระบวนการย่อย ทั้งหมด ออกและส่งคืนอาร์เรย์ของสถานะกระบวนการ)
รหัสนี้ให้ผลลัพธ์ต่อไปนี้ (อีกครั้งบนโปรเซสเซอร์ Quad-core ที่มี MRI Ruby 2.0.0p353):
0.000000 0.030000 27.000000 ( 3.788106)
ไม่โทรมเกินไป! เราทำให้ Mailer เร็วขึ้นประมาณ 5 เท่าโดยเพียงแค่แก้ไขโค้ดสองสามบรรทัด (เช่น ใช้ fork()
)
อย่าตื่นเต้นจนเกินไป แม้ว่ามันอาจจะน่าดึงดูดใจให้ใช้การฟอร์กเนื่องจากเป็นวิธีแก้ปัญหาที่ง่ายสำหรับการทำงานพร้อมกันของ Ruby แต่ก็มีข้อเสียเปรียบที่สำคัญคือปริมาณหน่วยความจำที่จะใช้ การ Forking ค่อนข้างแพง โดยเฉพาะอย่างยิ่งถ้า Copy-on-Write (CoW) ไม่ได้ใช้งานโดยล่าม Ruby ที่คุณใช้อยู่ หากแอปของคุณใช้หน่วยความจำ 20MB เช่น การฟอร์ก 100 ครั้งอาจใช้หน่วยความจำมากถึง 2GB!
นอกจากนี้ แม้ว่า multithreading จะมีความซับซ้อนของตัวเองเช่นกัน แต่ก็มีความสลับซับซ้อนหลายอย่างที่ต้องพิจารณาเมื่อใช้ fork()
เช่น ตัวอธิบายไฟล์ที่ใช้ร่วมกันและเซมาฟอร์ (ระหว่างกระบวนการแยกส่วนหลักและย่อย) ความจำเป็นในการสื่อสารผ่านไพพ์ และอื่นๆ
ทับทิมมัลติเธรด
ตกลง ตอนนี้เรามาลองทำโปรแกรมเดียวกันให้เร็วขึ้นโดยใช้เทคนิค Ruby multithreading แทน
เธรดจำนวนมากภายในกระบวนการเดียวมีค่าใช้จ่ายน้อยกว่าจำนวนกระบวนการที่เกี่ยวข้องอย่างมาก เนื่องจากใช้พื้นที่ที่อยู่และหน่วยความจำร่วมกัน
ให้คำนึงถึงกรณีทดสอบของเราอีกครั้ง แต่คราวนี้ใช้คลาส Thread
ของ Ruby:
threads = [] puts Benchmark.measure{ 100.times do |i| threads << Thread.new do Mailer.deliver do from "eki_#{i}@eqbalq.com" to "jill_#{i}@example.com" subject "Threading and Forking (#{i})" body "Some content" end end end threads.map(&:join) }
รหัสนี้ให้ผลลัพธ์ต่อไปนี้ (อีกครั้งบนโปรเซสเซอร์ Quad-core ที่มี MRI Ruby 2.0.0p353):
13.710000 0.040000 13.750000 ( 13.740204)
คนเกียจคร้าน ที่ไม่น่าประทับใจอย่างแน่นอน! เกิดอะไรขึ้น? เหตุใดจึงให้ผลลัพธ์เกือบเท่าๆ กับที่เราได้รับเมื่อเรารันโค้ดแบบซิงโครนัส
คำตอบซึ่งเป็นความหายนะของการมีอยู่ของโปรแกรมเมอร์ Ruby หลายคนคือ Global Interpreter Lock (GIL) ต้องขอบคุณ GIL ทำให้ Cruby (การนำ MRI ไปใช้) ไม่รองรับการทำเธรดจริงๆ
Global Interpreter Lock เป็นกลไกที่ใช้ในล่ามภาษาคอมพิวเตอร์เพื่อซิงโครไนซ์การทำงานของเธรดเพื่อให้ทำงานได้ครั้งละหนึ่งเธรดเท่านั้น ล่ามที่ใช้ GIL จะ อนุญาตให้ทำงานครั้งละหนึ่งเธรดและ หนึ่งเธรดเท่านั้น แม้ว่าจะรันบนโปรเซสเซอร์แบบมัลติคอร์ก็ตาม Ruby MRI และ CPython เป็นสองตัวอย่างที่พบบ่อยที่สุดของล่ามยอดนิยมที่มี GIL
กลับไปที่ปัญหาของเรา เราจะใช้ประโยชน์จากมัลติเธรดใน Ruby เพื่อปรับปรุงประสิทธิภาพในแง่ของ GIL ได้อย่างไร
ใน MRI (Cruby) คำตอบที่โชคร้ายคือโดยพื้นฐานแล้วคุณติดอยู่และมีน้อยมากที่มัลติเธรดสามารถทำเพื่อคุณได้

การทำงานพร้อมกันของ Ruby ที่ไม่มีระบบขนานยังคงมีประโยชน์มาก แม้ว่า สำหรับงานที่มี IO มาก (เช่น งานที่ต้องรอบนเครือข่ายบ่อยๆ) ดังนั้นเธรด ยัง คงมีประโยชน์ใน MRI สำหรับงานที่ต้องใช้ IO มาก มีเหตุผลที่ว่าทำไมเธรดจึงถูกคิดค้นและใช้งานก่อนที่เซิร์ฟเวอร์มัลติคอร์จะเป็นเรื่องปกติ
อย่างไรก็ตาม หากคุณมีตัวเลือกในการใช้เวอร์ชันอื่นที่ไม่ใช่ Cruby คุณสามารถใช้ Ruby แบบอื่นได้ เช่น JRuby หรือ Rubinius เนื่องจากไม่มี GIL และรองรับ Ruby threading แบบขนานจริง
เพื่อพิสูจน์ประเด็นนี้ นี่คือผลลัพธ์ที่เราได้รับเมื่อเรารันโค้ดในเวอร์ชันเธรดที่เหมือนกันทุกประการเหมือนเมื่อก่อน แต่คราวนี้รันบน JRuby (แทนที่จะเป็น Cruby):
43.240000 0.140000 43.380000 ( 5.655000)
ตอนนี้เรากำลังพูดถึง!
แต่…
กระทู้ไม่ฟรี
ประสิทธิภาพที่ได้รับการปรับปรุงด้วยหลายเธรดอาจทำให้หลายคนเชื่อว่าเราสามารถเพิ่มเธรดต่อไปได้ – โดยทั่วไปอย่างไม่สิ้นสุด – เพื่อให้โค้ดของเราทำงานเร็วขึ้นและเร็วขึ้น นั่นคงจะดีถ้ามันเป็นความจริง แต่ความจริงก็คือว่าเธรดนั้นไม่ฟรี ดังนั้น ไม่ช้าก็เร็ว ทรัพยากรของคุณจะหมด
สมมติว่าเราต้องการเรียกใช้จดหมายตัวอย่างของเรา ไม่ใช่ 100 ครั้ง แต่ 10,000 ครั้ง มาดูกันว่าเกิดอะไรขึ้น:
threads = [] puts Benchmark.measure{ 10_000.times do |i| threads << Thread.new do Mailer.deliver do from "eki_#{i}@eqbalq.com" to "jill_#{i}@example.com" subject "Threading and Forking (#{i})" body "Some content" end end end threads.map(&:join) }
บูม! ฉันได้รับข้อผิดพลาดกับ OS X 10.8 หลังจากวางไข่ประมาณ 2,000 เธรด:
can't create Thread: Resource temporarily unavailable (ThreadError)
ตามที่คาดไว้ ไม่ช้าก็เร็วเราเริ่มฟาดฟันหรือหมดทรัพยากรทั้งหมด ดังนั้นความสามารถในการปรับขนาดของวิธีนี้จึงมีจำกัดอย่างชัดเจน
การรวมเธรด
โชคดีที่มีวิธีที่ดีกว่า กล่าวคือการรวมเธรด
เธรดพูลเป็นกลุ่มของเธรดที่นำกลับมาใช้ใหม่ได้ล่วงหน้าซึ่งพร้อมใช้งานสำหรับการทำงานตามความจำเป็น พูลเธรดมีประโยชน์อย่างยิ่งเมื่อมีงานสั้นจำนวนมากที่ต้องทำมากกว่างานยาวจำนวนเล็กน้อย ซึ่งจะช่วยป้องกันไม่ให้ต้องเสียค่าใช้จ่ายในการสร้างเธรดหลายครั้ง
พารามิเตอร์คอนฟิกูเรชันคีย์สำหรับเธรดพูลโดยทั่วไปคือจำนวนของเธรดในพูล เธรดเหล่านี้สามารถสร้างอินสแตนซ์ทั้งหมดได้ในคราวเดียว (เช่น เมื่อสร้างพูล) หรือแบบขี้เกียจ (เช่น ตามความจำเป็นจนกว่าจะมีการสร้างเธรดสูงสุดในพูล)
เมื่อพูลได้รับมอบหมายงานให้ดำเนินการ พูลจะมอบหมายงานให้กับหนึ่งในเธรดที่ไม่ได้ใช้งานในปัจจุบัน หากไม่มีเธรดที่ไม่ได้ใช้งาน (และจำนวนเธรดสูงสุดถูกสร้างขึ้นแล้ว) เธรดจะรอให้เธรดทำงานจนเสร็จและไม่ได้ใช้งาน จากนั้นจึงมอบหมายงานให้กับเธรดนั้น
กลับมาที่ตัวอย่างของเรา เราจะเริ่มโดยใช้ Queue
(เนื่องจากเป็นประเภทข้อมูลที่ปลอดภัยสำหรับเธรด) และใช้กลุ่มเธรดที่ใช้งานง่าย:
ต้องการ "./lib/mailer" ต้องการ "เกณฑ์มาตรฐาน" ต้องใช้ "เธรด"
POOL_SIZE = 10 jobs = Queue.new 10_0000.times{|i| jobs.push i} workers = (POOL_SIZE).times.map do Thread.new do begin while x = jobs.pop(true) Mailer.deliver do from "eki_#{x}@eqbalq.com" to "jill_#{x}@example.com" subject "Threading and Forking (#{x})" body "Some content" end end rescue ThreadError end end end workers.map(&:join)
ในโค้ดด้านบนนี้ เราเริ่มต้นด้วยการสร้างคิว jobs
สำหรับงานที่ต้องดำเนินการ เราใช้ Queue
เพื่อจุดประสงค์นี้เนื่องจากเป็นเธรดที่ปลอดภัย (ดังนั้น หากมีหลายเธรดเข้าถึงได้พร้อมกัน ก็จะรักษาความสอดคล้องกัน) ซึ่งหลีกเลี่ยงความจำเป็นในการใช้งานที่ซับซ้อนมากขึ้นซึ่งต้องใช้ mutex
จากนั้นเราผลัก ID ของผู้ส่งเมลไปที่คิวงาน และสร้างกลุ่มของเธรดผู้ปฏิบัติงาน 10 รายการ
ภายในแต่ละเธรดของผู้ปฏิบัติงาน เราแสดงรายการจากคิวงาน
ดังนั้น วงจรชีวิตของเธรดผู้ปฏิบัติงานคือการรออย่างต่อเนื่องเพื่อให้งานถูกใส่ลงในคิวงานและดำเนินการตามนั้น
ข่าวดีก็คือมันใช้งานได้และขยายได้โดยไม่มีปัญหาใดๆ น่าเสียดายที่สิ่งนี้ค่อนข้างซับซ้อนแม้ในบทช่วยสอนง่ายๆ ของเรา
เซลลูลอยด์
ต้องขอบคุณระบบนิเวศของ Ruby Gem ความซับซ้อนส่วนใหญ่ของการทำ multithreading นั้นถูกห่อหุ้มไว้อย่างเรียบร้อยใน Ruby Gems ที่ใช้งานง่ายจำนวนหนึ่งที่แกะออกจากกล่อง
ตัวอย่างที่ดีคือเซลลูลอยด์ หนึ่งในอัญมณีทับทิมที่ฉันชอบ เฟรมเวิร์กเซลลูลอยด์เป็นวิธีที่ง่ายและสะอาดในการปรับใช้ระบบที่ทำงานพร้อมกันตามนักแสดงใน Ruby เซลลูลอยด์ช่วยให้ผู้คนสร้างโปรแกรมที่เกิดขึ้นพร้อมกันจากอ็อบเจ็กต์ที่เกิดขึ้นพร้อมกันได้อย่างง่ายดายพอๆ กับที่พวกเขาสร้างโปรแกรมแบบต่อเนื่องจากอ็อบเจกต์ที่เรียงตามลำดับ
ในบริบทของการสนทนาของเราในโพสต์นี้ ฉันกำลังเน้นไปที่คุณลักษณะ Pools โดยเฉพาะ แต่ให้ช่วยเหลือตัวเองและลองดูในรายละเอียดเพิ่มเติม เมื่อใช้ Celluloid คุณจะสามารถสร้างโปรแกรม Ruby แบบมัลติเธรดได้โดยไม่ต้องกังวลเกี่ยวกับปัญหาร้ายแรง เช่น การชะงักงัน และคุณจะพบว่าการใช้คุณสมบัติที่ซับซ้อนอื่นๆ เช่น Futures และ Promises นั้นไม่ใช่เรื่องยาก
ต่อไปนี้คือความเรียบง่ายของโปรแกรมส่งเมลเวอร์ชันมัลติเธรดของเราที่ใช้ Celluloid:
require "./lib/mailer" require "benchmark" require "celluloid" class MailWorker include Celluloid def send_email(id) Mailer.deliver do from "eki_#{id}@eqbalq.com" to "jill_#{id}@example.com" subject "Threading and Forking (#{id})" body "Some content" end end end mailer_pool = MailWorker.pool(size: 10) 10_000.times do |i| mailer_pool.async.send_email(i) end
สะอาด ง่าย ปรับขนาดได้ และทนทาน คุณจะขออะไรอีก
งานเบื้องหลัง
แน่นอน ทางเลือกอื่นที่เป็นไปได้ ขึ้นอยู่กับความต้องการและข้อจำกัดในการปฏิบัติงานของคุณคือการจ้างงานพื้นหลัง Ruby Gems จำนวนหนึ่งมีอยู่เพื่อรองรับการประมวลผลเบื้องหลัง (เช่น บันทึกงานในคิวและประมวลผลในภายหลังโดยไม่ปิดกั้นเธรดปัจจุบัน) ตัวอย่างที่โดดเด่น ได้แก่ Sidekiq, Resque, Delayed Job และ Beanstalkd
สำหรับโพสต์นี้ ฉันจะใช้ Sidekiq และ Redis (แคชและที่เก็บคีย์-ค่าของโอเพ่นซอร์ส)
ขั้นแรก ให้ติดตั้ง Redis และเรียกใช้ในเครื่อง:
brew install redis redis-server /usr/local/etc/redis.conf
เมื่ออินสแตนซ์ Redis ในพื้นที่ทำงาน มาดูเวอร์ชันของโปรแกรมส่งเมลตัวอย่าง ( mail_worker.rb
) โดยใช้ Sidekiq:
require_relative "../lib/mailer" require "sidekiq" class MailWorker include Sidekiq::Worker def perform(id) Mailer.deliver do from "eki_#{id}@eqbalq.com" to "jill_#{id}@example.com" subject "Threading and Forking (#{id})" body "Some content" end end end
เราสามารถทริกเกอร์ Sidekiq ด้วยไฟล์ mail_worker.rb
:
sidekiq -r ./mail_worker.rb
และจาก IRB:
⇒ irb >> require_relative "mail_worker" => true >> 100.times{|i| MailWorker.perform_async(i)} 2014-12-20T02:42:30Z 46549 TID-ouh10w8gw INFO: Sidekiq client with redis options {} => 100
เรียบง่ายสุดๆ และสามารถปรับขนาดได้อย่างง่ายดายโดยเพียงแค่เปลี่ยนจำนวนพนักงาน
อีกทางเลือกหนึ่งคือใช้ Sucker Punch หนึ่งในไลบรารีประมวลผล RoR แบบอะซิงโครนัสที่ฉันโปรดปราน การใช้งาน Sucker Punch จะคล้ายกันมาก เราแค่ต้องรวม SuckerPunch::Job
แทน Sidekiq::Worker
และ MailWorker.new.async.perform()
แทนที่จะเป็น MailWorker.perform_async()
บทสรุป
การทำงานพร้อมกันสูงไม่เพียงทำได้ใน Ruby เท่านั้น แต่ยังทำได้ง่ายกว่าที่คุณคิด
แนวทางหนึ่งที่ใช้ได้คือการแยกกระบวนการที่ทำงานอยู่เพื่อเพิ่มพลังการประมวลผล อีกเทคนิคหนึ่งคือการใช้ประโยชน์จากมัลติเธรด แม้ว่าเธรดจะเบากว่ากระบวนการ ซึ่งต้องการโอเวอร์เฮดน้อยกว่า คุณยังสามารถใช้ทรัพยากรจนหมดได้หากคุณเริ่มเธรดจำนวนมากเกินไปพร้อมๆ กัน ในบางจุด คุณอาจพบว่าจำเป็นต้องใช้เธรดพูล โชคดีที่ความซับซ้อนหลายอย่างของมัลติเธรดทำได้ง่ายขึ้นโดยใช้ประโยชน์จากอัญมณีที่มีอยู่จำนวนหนึ่ง เช่น เซลลูลอยด์และโมเดลนักแสดง
อีกวิธีหนึ่งในการจัดการกระบวนการที่ใช้เวลานานคือการใช้การประมวลผลพื้นหลัง มีไลบรารีและบริการมากมายที่ช่วยให้คุณสามารถใช้งานพื้นหลังในแอปพลิเคชันของคุณได้ เครื่องมือยอดนิยมบางตัวรวมถึงเฟรมเวิร์กงานที่ได้รับการสนับสนุนจากฐานข้อมูลและคิวข้อความ
การฟอร์ก เกลียว และการประมวลผลเบื้องหลังล้วนเป็นทางเลือกที่ทำงานได้ การตัดสินใจว่าจะใช้อันใดขึ้นอยู่กับลักษณะของแอปพลิเคชันของคุณ สภาพแวดล้อมการทำงานของคุณ และข้อกำหนด หวังว่าบทช่วยสอนนี้จะให้ข้อมูลเบื้องต้นที่เป็นประโยชน์เกี่ยวกับตัวเลือกต่างๆ ที่มีอยู่