Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

การเปิดกล่องเครื่องมือการทำงานพร้อมกันของ Ruby

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

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

แต่ก่อนอื่นให้ย้อนกลับไปมองภาพรวมก่อน

ภาวะพร้อมกันกับความเท่าเทียม

คำเหล่านี้ใช้อย่างหลวม ๆ แต่มีความหมายที่แตกต่างกัน

  • การทำงานพร้อมกัน: ศิลปะของการทำงานหลายอย่างพร้อมกัน การสลับไปมาอย่างรวดเร็วอาจทำให้ผู้ใช้ดูเหมือนเกิดขึ้นพร้อมกัน
  • ขนาน: ทำงานหลายอย่างในเวลาเดียวกันอย่างแท้จริง แทนที่จะปรากฏพร้อมกัน แต่ปรากฏพร้อมกัน

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

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

ในทางกลับกัน Ruby ยังไม่รองรับ Parallelism

เหตุใดจึงไม่มีความเท่าเทียมในทับทิม

ในปัจจุบัน ไม่มีทางที่จะบรรลุความเท่าเทียมภายในกระบวนการ Ruby เดียวโดยใช้การนำ Ruby เริ่มต้นไปใช้ (โดยทั่วไปเรียกว่า MRI หรือ Cruby) Ruby VM บังคับใช้การล็อก (GVM หรือ Global VM Lock) ที่ป้องกันไม่ให้หลายเธรดเรียกใช้โค้ด Ruby พร้อมกัน ล็อกนี้มีไว้เพื่อปกป้องสถานะภายในของเครื่องเสมือนและเพื่อป้องกันสถานการณ์ที่อาจส่งผลให้ VM หยุดทำงาน นี่ไม่ใช่จุดที่ดีที่จะอยู่ แต่ความหวังทั้งหมดไม่สูญหาย:Ruby 3 กำลังจะมาเร็ว ๆ นี้และสัญญาว่าจะแก้ปัญหานี้โดยแนะนำแนวคิดที่มีชื่อรหัสว่า Guild (อธิบายไว้ในส่วนสุดท้ายของบทความนี้)

กระทู้

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

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

app =
  Proc.new do |env|
    sleep 0.05
    qs = env['QUERY_STRING']
    number = Integer(qs.match(/number=(\d+)/)[1])
    [
      '200',
      { 'Content-Type' => 'text/plain' },
      [number.even? ? 'even' : 'odd']
    ]
  end

run app

ในการรันเว็บแอปนี้ คุณจะต้องติดตั้ง rack gem จากนั้นรัน rackup config.ru .

เรายังต้องการพื้นที่เก็บข้อมูลจำลอง นี่คือคลาสที่จำลองฐานข้อมูลคีย์-ค่า:

class Datastore
  # ... accessors and initialization omitted ...
  def read(key)
    data[key]
  end

  def write(key, value)
    data[key] = value
  end
end

ตอนนี้ มาดูการใช้งานโซลูชันพร้อมกันของเรากัน เรามีวิธี run ซึ่งดึงข้อมูล 1,000 รายการพร้อมกันและจัดเก็บไว้ในที่เก็บข้อมูลของเรา

class ThreadPoweredIntegration
  # ... accessors and initialization ...
  def run
    threads = []
    (1..1000).each_slice(250) do |subset|
      threads << Thread.new do
        subset.each do |number|
          uri = 'https://localhost:9292/' \
            "even_or_odd?number=#{number}"
          status, body = AdHocHTTP.new(uri).blocking_get
          handle_response(status, body)
        rescue Errno::ETIMEDOUT
          retry # Try again if the server times out.
        end
      end
    end
    threads.each(&:join)
  end
  # ...
end

เราสร้างสี่เธรด แต่ละชุดประมวลผล 250 รายการ เราใช้กลยุทธ์นี้เพื่อไม่ให้ครอบงำ API ของบุคคลที่สามหรือระบบของเราเอง

โดยการมีการร้องขอพร้อมกันโดยใช้หลายเธรด การดำเนินการทั้งหมดจะใช้เวลาเพียงเศษเสี้ยวของเวลาที่จะนำไปใช้ตามลำดับ แม้ว่าแต่ละเธรดจะมีช่วงเวลาที่ไม่ทำงานในระหว่างขั้นตอนทั้งหมดที่จำเป็นในการสร้างและสื่อสารผ่านคำขอ HTTP แต่ Ruby VM จะอนุญาตให้เธรดอื่นเริ่มทำงาน นี่คือเหตุผลว่าทำไมการใช้งานนี้จึงเร็วกว่าการดำเนินการตามลำดับมาก

AdHocHTTP class เป็นไคลเอนต์ HTTP ที่ตรงไปตรงมาซึ่งนำมาใช้เป็นพิเศษสำหรับบทความนี้ เพื่อให้เราสามารถเน้นเฉพาะความแตกต่างระหว่างโค้ดที่ขับเคลื่อนโดยเธรดและโค้ดที่ขับเคลื่อนโดยไฟเบอร์ อยู่นอกเหนือขอบเขตของบทความนี้เพื่อหารือเกี่ยวกับการใช้งาน แต่คุณสามารถตรวจสอบได้ที่นี่ หากคุณสงสัย

สุดท้าย เราจัดการการตอบสนองของเซิร์ฟเวอร์เมื่อสิ้นสุดวงใน นี่คือวิธีการ handle_response ลักษณะ:

# ... inside the ThreadPoweredIntegration class ...

attr_reader :ds

def initialize
  @ds = Datastore.new(even: 0, odd: 0)
end

# ...

def handle_response(status, body)
  return if status != '200'
  key = body.to_sym
  curr_count = ds.read(key)
  ds.write(key, curr_count + 1)
end

วิธีนี้ดูดีใช่หรือไม่ มาลองใช้งานและดูว่ามีอะไรเกิดขึ้นที่ datastore ของเรา:

{ even: 497, odd: 489 }

มันค่อนข้างแปลกเพราะฉันแน่ใจว่าระหว่าง 1 ถึง 1,000 มี 500 เลขคู่และ 500 เลขคี่ ในส่วนถัดไป เรามาทำความเข้าใจว่าเกิดอะไรขึ้นและสำรวจวิธีแก้ไขจุดบกพร่องนี้โดยสังเขป

หัวข้อและข้อมูลการแข่งขัน:ปีศาจอยู่ในรายละเอียด

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

เนื่องจากเรากำลังดำเนินการกับทรัพยากรที่ใช้ร่วมกัน (ds วัตถุ datastore) เราจะต้องระมัดระวังเป็นพิเศษกับการดำเนินการที่ไม่ใช่ของอะตอม สังเกตว่าครั้งแรกที่เราอ่านจาก datastore และ -- ในคำสั่งที่สอง -- เราเขียนการนับที่เพิ่มขึ้นทีละ 1 ซึ่งเป็นปัญหาเนื่องจากเธรดของเราอาจหยุดทำงานหลังจากการอ่านแต่ก่อนการเขียน จากนั้น หากเธรดอื่นทำงานและเพิ่มมูลค่าของคีย์ที่เราสนใจ เราจะเขียนการนับที่ล้าสมัยเมื่อเธรดเดิมกลับมาทำงานต่อ

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

มีหลายวิธีในการแก้ไขการแข่งขันข้อมูล วิธีแก้ปัญหาง่ายๆ คือการใช้ mutex กลไกการซิงโครไนซ์นี้บังคับใช้การเข้าถึงแบบทีละส่วนไปยังส่วนที่กำหนดของรหัส นี่คือการใช้งานก่อนหน้าของเราที่แก้ไขโดยการใช้ mutex:

# ... inside ThreadPoweredIntegration class ...
def initialize
  # ...
  @semaphore = Mutex.new
end
# ...
def handle_response(status, body)
  return if status != '200'
  key = body.to_sym
  semaphore.synchronize do
    curr_count = ds.read(key)
    ds.write(key, curr_count + 1)
  end
end

หากคุณวางแผนที่จะใช้เธรดภายในแอปพลิเคชัน Rails โปรดอ่านคู่มืออย่างเป็นทางการ Threading and Code Execution in Rails เป็นสิ่งที่ต้องอ่าน การไม่ปฏิบัติตามหลักเกณฑ์เหล่านี้อาจทำให้เกิดผลที่ไม่พึงประสงค์ เช่น การเชื่อมต่อฐานข้อมูลรั่ว

หลังจากดำเนินการแก้ไขแล้ว เราได้ผลลัพธ์ที่คาดหวัง:

{ even: 500, odd: 500 }

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

ไฟเบอร์:เครื่องมือที่เพรียวบางสำหรับการทำงานพร้อมกัน

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

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

สถานการณ์เดียวกัน มีไฟเบอร์แล้ว

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

เราจะเริ่มต้นด้วยการดูที่ run วิธีการใช้งานไฟเบอร์ของเรา:

class FiberPoweredIntegration
  # ... accessors and initialization ...
  def run
    (1..1000).each_slice(250) do |subset|
      Fiber.new do
        subset.each do |number|
          uri = 'https://127.0.0.1:9292/' \
            "even_or_odd?number=#{number}"
          client = AdHocHTTP.new(uri)
          socket = client.init_non_blocking_get
          yield_if_waiting(client,
                           socket,
                           :connect_non_blocking_get)
          yield_if_waiting(client,
                           socket,
                           :write_non_blocking_get)
          status, body =
            yield_if_waiting(client,
                             socket,
                             :read_non_blocking_get)
          handle_response(status, body)
        ensure
          client&.close_non_blocking_get
        end
      end.resume
    end

    wait_all_requests
  end
  # ...
end

ขั้นแรก เราสร้างเส้นใยสำหรับแต่ละชุดย่อยของตัวเลขที่เราต้องการตรวจสอบว่าคู่หรือคี่

จากนั้นเราก็วนซ้ำตัวเลขเรียก yield_if_waiting . วิธีนี้มีหน้าที่ในการหยุดไฟเบอร์ปัจจุบันและอนุญาตให้ไฟเบอร์อื่นกลับมาทำงานต่อได้

สังเกตด้วยว่าหลังจากสร้างไฟเบอร์แล้ว เราเรียก resume . ทำให้ไฟเบอร์เริ่มทำงาน โดยโทร resume ทันทีหลังจากสร้าง เราเริ่มส่งคำขอ HTTP ก่อนที่ลูปหลักจะสิ้นสุดตั้งแต่ 1 ถึง 1,000

ในตอนท้ายของ run เมธอด มีการเรียก wait_all_requests . วิธีการนี้จะเลือกเส้นใยที่พร้อมใช้งานและยังรับประกันว่าเราได้ดำเนินการตามคำขอทั้งหมด เราจะมาดูในส่วนสุดท้ายของส่วนนี้

ทีนี้มาดู yield_if_waiting โดยละเอียด:

# ... inside FiberPoweredIntegration ...
def initialize
  @ds = Datastore.new(even: 0, odd: 0)
  @waiting = { wait_readable: {}, wait_writable: {} }
end
# ...
def yield_if_waiting(client, socket, operation)
  res_or_status = client.send(operation)
  is_waiting =
    [:wait_readable,
     :wait_writable].include?(res_or_status)
  return res_or_status unless is_waiting

  waiting[res_or_status][socket] = Fiber.current
  Fiber.yield
  waiting[res_or_status].delete(socket)
  yield_if_waiting(client, socket, operation)
rescue Errno::ETIMEDOUT
  retry # Try again if the server times out.
end

ก่อนอื่นเราพยายามดำเนินการ (เชื่อมต่อ อ่าน หรือเขียน) โดยใช้ไคลเอนต์ของเรา ผลลัพธ์หลักสองประการเป็นไปได้:

  • ความสำเร็จ: เมื่อสิ่งนั้นเกิดขึ้น เราก็กลับมา
  • เราสามารถได้รับสัญลักษณ์: นี่หมายความว่าเราต้องรอ

หนึ่ง "รอ" ได้อย่างไร

  1. เราสร้างจุดตรวจโดยเพิ่มซ็อกเก็ตของเราร่วมกับไฟเบอร์ปัจจุบันไปยังตัวแปรอินสแตนซ์ waiting (ซึ่งเป็น Hash )
  2. เราเก็บคู่นี้ไว้ในคอลเล็กชันที่มี IO ที่รออ่านหรือเขียน (เราจะเห็นว่าเหตุใดจึงสำคัญในอีกสักครู่) ทั้งนี้ขึ้นอยู่กับผลลัพธ์ที่เราได้รับจากลูกค้า
  3. เราหยุดการทำงานของไฟเบอร์ปัจจุบัน ปล่อยให้ไฟเบอร์อื่นทำงาน ไฟเบอร์ที่หยุดชั่วคราวจะได้รับโอกาสในการทำงานต่อเมื่อซ็อกเก็ตเครือข่ายที่เกี่ยวข้องพร้อม จากนั้น การดำเนินการ IO จะถูกลองอีกครั้ง (และครั้งนี้จะสำเร็จ)

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

ตอนนี้เราเข้าใจกลไกที่ใช้ในการดำเนินการเมื่อไฟเบอร์กำลังรอ IO แล้ว มาสำรวจบิตสุดท้ายที่จำเป็นเพื่อทำความเข้าใจการใช้งานที่ขับเคลื่อนด้วยไฟเบอร์นี้

def wait_all_requests
  while(waiting[:wait_readable].any? ||
        waiting[:wait_writable].any?)

    ready_to_read, ready_to_write =
      IO.select(waiting[:wait_readable].keys,
                waiting[:wait_writable].keys)

    ready_to_read.each do |socket|
      waiting[:wait_readable][socket].resume
    end

    ready_to_write.each do |socket|
      waiting[:wait_writable][socket].resume
    end
  end
end

แนวคิดหลักในที่นี้คือรอ (หรืออีกนัยหนึ่งคือวนซ้ำ) จนกว่าการดำเนินการ IO ที่รอดำเนินการทั้งหมดจะเสร็จสมบูรณ์

ในการทำเช่นนั้น เราใช้ IO.select . ยอมรับสองคอลเลกชันของวัตถุ IO ที่รอดำเนินการ:ชุดหนึ่งสำหรับการอ่านและอีกชุดสำหรับการเขียน ส่งคืนอ็อบเจ็กต์ IO ที่งานเสร็จ เนื่องจากเราเชื่อมโยงอ็อบเจ็กต์ IO เหล่านี้กับไฟเบอร์ที่รับผิดชอบในการรัน ไฟเบอร์เหล่านั้นกลับมาทำงานต่อได้ง่าย

เราทำซ้ำขั้นตอนเหล่านี้ต่อไปจนกว่าคำขอทั้งหมดจะถูกไล่ออกและเสร็จสิ้น

The Grand Finale:Comparable Performance, No Need for Locks

handle_responseของเรา เมธอดจะเหมือนกับที่ใช้ในโค้ดครั้งแรกโดยใช้เธรด (เวอร์ชันที่ไม่มี mutex) อย่างไรก็ตาม เนื่องจากเส้นใยทั้งหมดของเราทำงานอยู่ภายในเธรดเดียวกัน เราจึงไม่มีการแข่งกันของข้อมูล เมื่อเรารันโค้ด เราจะได้ผลลัพธ์ที่คาดหวัง:

{ even: 500, odd: 500 }

คุณอาจไม่ต้องการจัดการกับธุรกิจการเปลี่ยนไฟเบอร์ทุกครั้งที่คุณใช้ประโยชน์จาก async IO โชคดีที่อัญมณีบางชิ้นสรุปงานนี้และทำให้การใช้เส้นใยเป็นสิ่งที่นักพัฒนาไม่จำเป็นต้องนึกถึง ลองดูโครงการ async เป็นจุดเริ่มต้นที่ดี

ไฟเบอร์จะเปล่งประกายเมื่อต้องมีความสามารถในการปรับขนาดสูง

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

กิลด์ - การเขียนโปรแกรมคู่ขนานใน Ruby

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

สิ่งนี้อาจมีการเปลี่ยนแปลงใน Ruby 3 ด้วยการมาถึงของคุณสมบัติใหม่ที่เรียกว่า "Guilds" รายละเอียดยังคงไม่ชัดเจน แต่ในหัวข้อต่อไปนี้ เราจะมาดูกันว่าฟีเจอร์ระหว่างดำเนินการนี้จะช่วยให้ Ruby ทำงานพร้อมกันได้อย่างไร

กิลด์อาจทำงานอย่างไร

แหล่งที่มาของความเจ็บปวดที่สำคัญเมื่อใช้โซลูชันพร้อมกัน/คู่ขนานคือหน่วยความจำที่ใช้ร่วมกัน ในส่วนหัวข้อ เราได้เห็นแล้วว่าการสร้างสลิปและเขียนโค้ดที่ดูเหมือนไม่มีพิษมีภัยในแวบแรกนั้นง่ายเพียงใด แต่จริงๆ แล้วมีข้อบกพร่องที่ละเอียดอ่อน

Koichi Sasada - สมาชิก Ruby Core Team ที่เป็นผู้นำในการพัฒนาฟีเจอร์ใหม่ของ Guild - ทำงานหนักในการออกแบบโซลูชันที่จัดการกับอันตรายของการแบ่งปันหน่วยความจำระหว่างเธรดต่างๆ ในการนำเสนอของเขาที่งาน RubyConf ปี 2018 เขาอธิบายว่าเมื่อใช้กิลด์ คุณจะไม่สามารถแชร์วัตถุที่เปลี่ยนแปลงได้ แนวคิดหลักคือการป้องกันการแข่งขันข้อมูลโดยอนุญาตให้แชร์เฉพาะวัตถุที่ไม่เปลี่ยนรูประหว่างกิลด์ต่างๆ เท่านั้น

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

การใช้กิลด์เพื่อสำรวจสถานการณ์ทั่วไป

มีหลายสถานการณ์ที่คุณอาจต้องการเร่งความเร็วในการคำนวณด้วยการรันแบบขนาน สมมติว่าเราต้องคำนวณค่าเฉลี่ยและค่าเฉลี่ยของชุดข้อมูลเดียวกัน

ตัวอย่างด้านล่างแสดงให้เห็นว่าเราจะทำอย่างไรกับกิลด์ โปรดทราบว่ารหัสนี้ใช้ไม่ได้ในขณะนี้และอาจใช้งานไม่ได้ แม้จะปล่อยกิลด์แล้ว

# A frozen array of numeric values is an immutable object.
dataset = [88, 43, 37, 85, 84, 38, 13, 84, 17, 87].freeze
# The overhead of using guilds will probably be
# considerable, so it will only make sense to
# parallelize work when a dataset is large / when
# performing lots of operations.

g1 = Guild.new do
  mean = dataset.reduce(:+).fdiv(dataset.length)
  Guild.send_to(:mean, Guild.parent)
end

g2 = Guild.new do
  median = Median.calculate(dataset.sort)
  Guild.send_to(:median, Guild.parent)
end

results = {}
# Every Ruby program will be run inside a main guild;
# therefore, we can also receive messages in the main
# section of our program.
Guild.receive(:mean, :median) do |tag, result|
  results[tag] = result
end

สรุปผล

การทำงานพร้อมกันและความเท่าเทียมกันไม่ใช่จุดแข็งหลักของ Ruby แต่แม้แต่ในแผนกนี้ ภาษาก็มีเครื่องมือที่น่าจะดีพอที่จะจัดการกับกรณีการใช้งานส่วนใหญ่ Ruby 3 กำลังมา และดูเหมือนว่าสิ่งต่าง ๆ จะดีขึ้นมากเมื่อมีการแนะนำ Guild primitive ในความคิดของฉัน Ruby ยังคงเป็นตัวเลือกที่เหมาะสมมากในหลาย ๆ สถานการณ์ และเห็นได้ชัดว่าชุมชนของ Ruby ทำงานหนักเพื่อทำให้ภาษาดียิ่งขึ้นไปอีก มาฟังกันให้ดีๆ ว่ากำลังจะเกิดอะไรขึ้น!