ทำไมเราถึงต้องการเส้นใย


101

สำหรับ Fibers เรามีตัวอย่างคลาสสิก: การสร้างตัวเลข Fibonacci

fib = Fiber.new do  
  x, y = 0, 1 
  loop do  
    Fiber.yield y 
    x,y = y,x+y 
  end 
end

ทำไมเราถึงต้องการเส้นใยที่นี่? ฉันสามารถเขียนสิ่งนี้ใหม่ได้ด้วย Proc เดียวกัน (จริงๆแล้วการปิด)

def clsr
  x, y = 0, 1
  Proc.new do
    x, y = y, x + y
    x
  end
end

ดังนั้น

10.times { puts fib.resume }

และ

prc = clsr 
10.times { puts prc.call }

จะส่งคืนผลลัพธ์เดียวกัน

ข้อดีของเส้นใยคืออะไร สิ่งที่ฉันสามารถเขียนด้วยเส้นใยที่ฉันไม่สามารถทำได้ด้วย lambdas และคุณสมบัติอื่น ๆ ของ Ruby?


4
ฟีโบนักชีตัวอย่างเก่าเป็นเพียงแรงผลักดันที่เลวร้ายที่สุด ;-) มีแม้กระทั่งสูตรที่คุณสามารถใช้ในการคำนวณคือใด ๆจำนวน fibonacci ใน O (1)
usr

17
ปัญหาไม่ได้เกี่ยวกับอัลกอริทึม แต่เกี่ยวกับการทำความเข้าใจเส้นใย :)
fl00r

คำตอบ:


230

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

การใช้เส้นใยอันดับ 1 ใน Ruby คือการใช้Enumerators ซึ่งเป็นคลาส Ruby หลักใน Ruby 1.9 สิ่งเหล่านี้มีประโยชน์อย่างเหลือเชื่อ

ใน Ruby 1.9 หากคุณเรียกใช้เมธอด iterator เกือบทุกชนิดในคลาสหลักโดยไม่ผ่านบล็อกมันจะส่งคืนEnumeratorไฟล์.

irb(main):001:0> [1,2,3].reverse_each
=> #<Enumerator: [1, 2, 3]:reverse_each>
irb(main):002:0> "abc".chars
=> #<Enumerator: "abc":chars>
irb(main):003:0> 1.upto(10)
=> #<Enumerator: 1:upto(10)>

Enumeratorวัตถุเหล่านี้เป็นวัตถุที่นับได้และeachวิธีการของพวกเขาให้องค์ประกอบที่จะได้รับผลจากวิธีการวนซ้ำดั้งเดิมหากถูกเรียกด้วยบล็อก ในตัวอย่างที่ฉันเพิ่งให้ตัวแจงนับที่ส่งคืนโดยreverse_eachมีeachวิธีการที่ให้ผล 3,2,1 Enumerator ส่งคืนโดยcharsให้ "c", "b", "a" (และอื่น ๆ ) แต่ไม่เหมือนกับวิธีการวนซ้ำแบบเดิม Enumerator ยังสามารถส่งคืนองค์ประกอบทีละรายการหากคุณเรียกnextมันซ้ำ ๆ :

irb(main):001:0> e = "abc".chars
=> #<Enumerator: "abc":chars>
irb(main):002:0> e.next
=> "a"
irb(main):003:0> e.next
=> "b"
irb(main):004:0> e.next
=> "c"

คุณอาจเคยได้ยิน "ตัวทำซ้ำภายใน" และ "ตัวทำซ้ำภายนอก" (คำอธิบายที่ดีของทั้งสองอย่างมีอยู่ในหนังสือรูปแบบการออกแบบ "Gang of Four") ตัวอย่างข้างต้นแสดงให้เห็นว่า Enumerators สามารถใช้เพื่อเปลี่ยน iterator ภายในให้เป็น external

นี่เป็นวิธีหนึ่งในการสร้างตัวนับของคุณเอง:

class SomeClass
  def an_iterator
    # note the 'return enum_for...' pattern; it's very useful
    # enum_for is an Object method
    # so even for iterators which don't return an Enumerator when called
    #   with no block, you can easily get one by calling 'enum_for'
    return enum_for(:an_iterator) if not block_given?
    yield 1
    yield 2
    yield 3
  end
end

มาลองดูกัน:

e = SomeClass.new.an_iterator
e.next  # => 1
e.next  # => 2
e.next  # => 3

เดี๋ยวก่อน ... มีอะไรแปลก ๆ ไหม? คุณเขียนyieldคำสั่งan_iteratorเป็นรหัสเส้นตรง แต่ Enumerator สามารถเรียกใช้ทีละรายการได้ ระหว่างการโทรไปยังnextการดำเนินการan_iteratorคือ "หยุด" ทุกครั้งที่คุณโทรnextมันจะยังคงทำงานต่อไปยังข้อความต่อไปนี้yieldจากนั้น "ค้าง" อีกครั้ง

คุณสามารถคาดเดาวิธีการใช้งานได้หรือไม่? แจงนับ wraps การเรียกร้องให้an_iteratorเส้นใยและผ่านบล็อกที่ระงับเส้นใย ดังนั้นทุกครั้งที่ส่งan_iteratorผลให้กับบล็อกไฟเบอร์ที่ทำงานอยู่จะถูกระงับและการดำเนินการจะดำเนินต่อไปบนเธรดหลัก ครั้งต่อไปที่คุณโทรระบบnextจะส่งผ่านการควบคุมไปยังไฟเบอร์บล็อกจะส่งกลับและan_iteratorดำเนินการต่อจากจุดที่ค้างไว้

มันจะเป็นคำแนะนำที่จะคิดว่าจะต้องทำอย่างไรหากไม่มีเส้นใย ทุกระดับที่ต้องการที่จะให้ทั้งสอง iterators nextภายในและภายนอกจะต้องมีรหัสอย่างชัดเจนในการติดตามของรัฐระหว่างการโทรไปยัง การเรียกแต่ละครั้งจะต้องตรวจสอบสถานะนั้นและอัปเดตก่อนส่งคืนค่า ด้วยเส้นใยเราสามารถแปลงตัววนซ้ำภายในเป็นเครื่องภายนอกได้โดยอัตโนมัติ

สิ่งนี้ไม่เกี่ยวข้องกับเส้นใย persay แต่ขอพูดถึงอีกสิ่งหนึ่งที่คุณสามารถทำได้กับ Enumerators: ช่วยให้คุณสามารถใช้วิธีการ Enumerable ลำดับที่สูงกว่ากับตัวทำซ้ำอื่น ๆ นอกเหนือeachจาก คิดเกี่ยวกับมันได้ตามปกติทุกวิธี Enumerable รวมทั้งmap, select, include?, inject, และอื่น ๆทั้งหมดที่eachทำงานในองค์ประกอบที่ให้ผลโดย แต่ถ้าออบเจ็กต์มีตัวทำซ้ำอื่นที่ไม่ใช่eachล่ะ?

irb(main):001:0> "Hello".chars.select { |c| c =~ /[A-Z]/ }
=> ["H"]
irb(main):002:0> "Hello".bytes.sort
=> [72, 101, 108, 108, 111]

การเรียกตัววนซ้ำโดยไม่มีบล็อกจะส่งกลับตัวนับจากนั้นคุณสามารถเรียกใช้เมธอด Enumerable อื่น ๆ ได้

กลับไปที่เส้นใยคุณใช้takeวิธีจาก Enumerable หรือไม่?

class InfiniteSeries
  include Enumerable
  def each
    i = 0
    loop { yield(i += 1) }
  end
end

ถ้ามีอะไรเรียกeachวิธีนั้นดูเหมือนว่ามันไม่ควรกลับมาใช่มั้ย? ลองดู:

InfiniteSeries.new.take(10) # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

ฉันไม่รู้ว่าสิ่งนี้ใช้เส้นใยใต้ฝากระโปรงหรือเปล่า แต่ทำได้ เส้นใยสามารถใช้เพื่อใช้รายการที่ไม่มีที่สิ้นสุดและการประเมินแบบเกียจคร้านของชุดข้อมูล สำหรับตัวอย่างของวิธีขี้เกียจที่กำหนดด้วย Enumerators ฉันได้กำหนดไว้ที่นี่: https://github.com/alexdowad/showcase/blob/master/ruby-core/collections.rb

คุณยังสามารถสร้างศูนย์โครูทีนเอนกประสงค์โดยใช้เส้นใย ฉันยังไม่เคยใช้โครูทีนในโปรแกรมใด ๆ เลย แต่เป็นแนวคิดที่ดีที่ควรทราบ

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

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

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

ฉันแน่ใจว่ามีหลายสาเหตุที่สิ่งนั้นอาจไม่สามารถใช้งานได้จริง (อย่างน้อยก็ในตอนนี้) แต่อีกครั้งฉันแค่พยายามแสดงให้คุณเห็นความเป็นไปได้บางอย่าง ใครจะรู้; เมื่อคุณได้แนวคิดแล้วคุณอาจพบกับแอปพลิเคชั่นใหม่ทั้งหมดที่ยังไม่มีใครคิด!


ขอบคุณคำตอบของคุณ! เหตุใดพวกเขาจึงไม่นำไปใช้charsหรือตัวนับอื่น ๆ เพียงแค่ปิด?
fl00r

@ fl00r ฉันกำลังคิดที่จะเพิ่มข้อมูลให้มากกว่านี้ แต่ฉันไม่รู้ว่าคำตอบนี้ยาวเกินไปไหม ... คุณต้องการมากกว่านี้ไหม
Alex D

13
คำตอบนี้ดีมากจนควรเขียนเป็นบล็อกโพสต์ที่ไหนสักแห่ง methinks
Jason Voegele

1
อัปเดต: ดูเหมือนว่าEnumerableจะรวมเมธอด "lazy" ไว้ใน Ruby 2.0
Alex D

2
takeไม่ต้องใช้เส้นใย แต่ให้takeหยุดพักระหว่างผลผลิตที่ n เมื่อใช้ภายในบล็อกให้breakส่งกลับการควบคุมไปยังเฟรมที่กำหนดบล็อก a = [] ; InfiniteSeries.new.each { |x| a << x ; break if a.length == 10 } ; a
Matthew

22

ซึ่งแตกต่างจากการปิดซึ่งมีจุดเข้าและออกที่กำหนดไว้เส้นใยสามารถรักษาสถานะและผลตอบแทน (ผลผลิต) ได้หลายครั้ง:

f = Fiber.new do
  puts 'some code'
  param = Fiber.yield 'return' # sent parameter, received parameter
  puts "received param: #{param}"
  Fiber.yield #nothing sent, nothing received 
  puts 'etc'
end

puts f.resume
f.resume 'param'
f.resume

พิมพ์สิ่งนี้:

some code
return
received param: param
etc

การนำตรรกะนี้ไปใช้กับคุณสมบัติทับทิมอื่น ๆ จะอ่านได้น้อยลง

ด้วยคุณสมบัตินี้การใช้เส้นใยที่ดีคือการจัดตารางการทำงานร่วมกันด้วยตนเอง (เป็นการเปลี่ยนเธรด) Ilya Grigorik มีตัวอย่างที่ดีในการเปลี่ยนไลบรารีอะซิงโครนัส ( eventmachineในกรณีนี้) ให้เป็น API แบบซิงโครนัสโดยไม่สูญเสียข้อดีของการตั้งเวลา IO ของการดำเนินการแบบอะซิงโครนัส นี่คือการเชื่อมโยง


ขอบคุณ! ฉันอ่านเอกสารดังนั้นฉันจึงเข้าใจเวทมนตร์ทั้งหมดนี้โดยมีหลายรายการและออกจากเส้นใย แต่ฉันไม่แน่ใจว่าสิ่งนี้ทำให้ชีวิตง่ายขึ้น ฉันไม่คิดว่าการพยายามทำตามประวัติย่อและผลตอบแทนทั้งหมดนี้เป็นความคิดที่ดี ดูเหมือนกุญแจไขที่ไขยาก ดังนั้นฉันต้องการทำความเข้าใจว่ามีบางกรณีไหมที่เส้นใยนี้เป็นทางออกที่ดี Eventmachine นั้นเจ๋ง แต่ไม่ใช่สถานที่ที่ดีที่สุดในการทำความเข้าใจเส้นใยเพราะก่อนอื่นคุณควรเข้าใจรูปแบบเครื่องปฏิกรณ์ทั้งหมดนี้ ดังนั้นฉันจึงเชื่อมั่นฉันสามารถเข้าใจเส้นใยphysical meaningในตัวอย่างง่าย ๆ
fl00r
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.