섬유질이 필요한 이유


101

Fibers의 경우 전형적인 예가 있습니다 : 피보나치 수 생성

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 }

동일한 결과를 반환합니다.

그래서 섬유의 장점은 무엇입니까? 람다 및 기타 멋진 Ruby 기능으로는 할 수없는 Fibers로 어떤 종류의 작업을 작성할 수 있습니까?


4
오래된 피보나치 예제는 최악의 동기 부여 자일뿐입니다. ;-) O (1)에서 피보나치 수 를 계산 하는 데 사용할 수있는 공식도 있습니다 .
usr

17
문제는 알고리즘에 관한 것이 아니라 섬유 이해에 관한 것입니다. :)
fl00r

답변:


230

Fiber는 아마도 애플리케이션 레벨 코드에서 직접 사용하지 않을 것입니다. 그것들은 다른 추상화를 만드는 데 사용할 수있는 흐름 제어 기본 요소이며, 그런 다음 상위 수준 코드에서 사용할 수 있습니다.

아마도 Ruby에서 섬유를 가장 많이 사용 Enumerator하는 것은 Ruby 1.9의 핵심 Ruby 클래스 인 s 를 구현 하는 것입니다. 이것들은 매우 유용합니다.

Ruby 1.9에서는 블록 전달 하지 않고 코어 클래스에서 거의 모든 반복기 메서드를 호출 하면 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)>

이들은 EnumeratorEnumerable 객체이며, 그 each메소드는 블록으로 호출 된 경우 원래 반복기 메소드에 의해 생성되었을 요소를 생성합니다. 방금 준 예제에서 반환 된 Enumerator reverse_each에는 each3,2,1을 산출 하는 메서드가 있습니다. 반환 된 열거 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"디자인 패턴 책에 나와 있습니다). 위의 예는 열거자를 사용하여 내부 반복기를 외부 반복기로 바꿀 수 있음을 보여줍니다.

이것은 고유 한 열거자를 만드는 한 가지 방법입니다.

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직선 코드로,하지만 열거 그들에게 실행할 수 있습니다 한 번에 하나씩 . 에 대한 호출 사이 next에의 실행 an_iterator이 "고정"됩니다. 을 호출 할 때마다 next다음 yield문으로 계속 실행 된 다음 다시 "멈 춥니 다".

이것이 어떻게 구현되는지 짐작할 수 있습니까? Enumerator는에 대한 호출을 an_iterator파이버 로 래핑하고 파이버를 일시 중단 하는 블록을 전달합니다 . 따라서 an_iterator블록에 양보 할 때마다 실행중인 파이버가 일시 중단되고 메인 스레드에서 실행이 계속됩니다. 다음에를 호출하면 next제어가 광섬유로 전달 되고 블록이 반환 되며 an_iterator중단 된 지점에서 계속됩니다.

섬유없이 이것을하기 위해 필요한 것을 생각하는 것은 유익 할 것입니다. 내부 및 외부 반복자를 모두 제공하려는 모든 클래스에는 next. next에 대한 각 호출은 해당 상태를 확인하고 값을 반환하기 전에 업데이트해야합니다. Fiber를 사용하면 내부 반복기를 외부 반복기로 자동 변환 할 수 있습니다 .

이것은 파이버 퍼 세이와 관련이 없지만, 열거 자로 할 수있는 한 가지 더 언급하겠습니다 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]

블록없이 반복기를 호출하면 Enumerator가 반환되고 다른 Enumerable 메서드를 호출 할 수 있습니다.

Fiber로 돌아가서 takeEnumerable 의 방법 을 사용 했습니까?

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]

이것이 후드 아래에서 섬유를 사용하는지는 모르겠지만 가능합니다. 파이버는 무한 목록과 시리즈의 지연 평가를 구현하는 데 사용할 수 있습니다. 열거자를 사용하여 정의 된 일부 지연 메서드의 예를 들어 여기에 일부를 정의했습니다. https://github.com/alexdowad/showcase/blob/master/ruby-core/collections.rb

섬유를 사용하여 범용 코 루틴 시설을 구축 할 수도 있습니다. 아직 내 프로그램에서 코 루틴을 사용한 적이 없지만 알아두면 좋은 개념입니다.

나는 이것이 당신에게 가능성에 대한 아이디어를 제공하기를 바랍니다. 처음에 말했듯이 섬유는 저수준 흐름 제어 기본 요소입니다. 이를 통해 프로그램 내에서 여러 제어 흐름 "위치"(책 페이지의 다른 "책갈피"와 같은)를 유지하고 원하는대로 전환 할 수 있습니다. 임의의 코드가 파이버에서 실행될 수 있기 때문에 파이버에서 타사 코드를 호출 한 다음 "고정"하고 제어하는 ​​코드로 다시 호출 할 때 다른 작업을 계속할 수 있습니다.

다음과 같이 상상해보십시오. 많은 클라이언트에 서비스를 제공 할 서버 프로그램을 작성하고 있습니다. 클라이언트와의 완전한 상호 작용에는 일련의 단계를 거쳐야하지만 각 연결은 일시적이며 연결 사이의 각 클라이언트에 대한 상태를 기억해야합니다. (웹 프로그래밍처럼 들리나요?)

해당 상태를 명시 적으로 저장하고 클라이언트가 연결할 때마다 확인하는 대신 (다음 "단계"수행해야하는 작업을 확인하기 위해) 각 클라이언트에 대해 광섬유를 유지할 수 있습니다. 클라이언트를 식별 한 후 광섬유를 검색하고 다시 시작합니다. 그런 다음 각 연결이 끝날 때 광섬유를 일시 중단하고 다시 저장합니다. 이렇게하면 모든 단계를 포함하여 완전한 상호 작용을위한 모든 논리를 구현하는 직선 코드를 작성할 수 있습니다 (프로그램이 로컬에서 실행되도록 만든 경우 당연히 그랬던 것처럼).

나는 그러한 일이 (적어도 지금은) 실용적이지 않을 수있는 많은 이유가 있다고 확신하지만, 다시 한 번 몇 가지 가능성을 보여 드리려고합니다. 누가 알아; 일단 개념을 얻으면 아직 아무도 생각하지 않은 완전히 새로운 응용 프로그램이 나올 수 있습니다!


답변을 주셔서 감사합니다! 그렇다면 왜 chars클로저만으로 다른 열거자를 구현하지 않습니까?
fl00r

@ fl00r, 더 많은 정보를 추가하려고 생각하고 있지만이 답변이 이미 너무 긴지 모르겠습니다 ... 더 원하십니까?
Alex D

13
이 답변은 너무 좋아서 어딘가에 블로그 게시물로 작성해야합니다.
Jason Voegele

1
업데이트 : EnumerableRuby 2.0에 "lazy"메소드가 포함될 것 같습니다.
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비동기식 실행의 IO 스케줄링의 이점을 잃지 않고 비동기식 라이브러리 ( 이 경우)를 동기식 API처럼 보이는 방법에 대한 좋은 예를 가지고 있습니다. 여기에 링크가 있습니다.


감사합니다! 나는 문서를 읽었으므로 광섬유 내부에 많은 입구와 출구가있는이 모든 마법을 이해합니다. 하지만이 물건이 삶을 더 쉽게 만들어 줄지 모르겠습니다. 이 모든 이력서와 수익률을 따르는 것은 좋은 생각이 아니라고 생각합니다. 풀기 어려운 줄거리처럼 보입니다. 그래서 저는이 섬유 줄이 좋은 해결책이되는 경우가 있는지 이해하고 싶습니다. Eventmachine은 멋지지만 섬유를 이해하기 가장 좋은 곳은 아닙니다. 먼저이 모든 반응기 패턴을 이해해야하기 때문입니다. 그래서 저는 physical meaning더 간단한 예에서 섬유 를 이해할 수 있다고 믿습니다
fl00r
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.