Computer >> 컴퓨터 >  >> 프로그램 작성 >> Ruby

Ruby의 섬유 및 열거자 - 블록을 뒤집어서

Ruby에는 반복을 수행하는 다양한 방법(루프, 블록 및 열거자)이 있습니다. 대부분의 Ruby 프로그래머는 최소한 루프와 블록에 익숙하지만 EnumeratorFiber 종종 어둠 속에 머문다. 이번 Ruby Magic 에디션에서는 게스트 작성자 Julik이 Enumerable을 조명합니다. 및 Fiber enumerable을 제어하고 블록을 뒤집어서 흐름을 제어하는 ​​방법을 설명합니다.

일시 중단 블록 및 연쇄 반복

Enumerator를 반환하는 방법을 설명한 Ruby Magic의 이전 버전에서 Enumerator에 대해 논의했습니다. 자신의 #each에서 방법과 그 용도. Enumerator의 더욱 광범위한 사용 사례 및 Fiber 그것은 그들이 비행 중 "블록을 일시 중단"할 수 있다는 것입니다. #each에 주어진 블록뿐만 아니라 또는 #each에 대한 전체 호출 , 하지만 어떤 블록이든!

이것은 블록을 사용하는 대신 순차적 호출을 기대하는 호출자에 대한 브리지로 블록을 사용하여 작동하는 메서드에 대한 shim을 구현하는 데 사용할 수 있는 매우 강력한 구성입니다. 예를 들어 데이터베이스 핸들을 열고 검색한 각 항목을 읽고 싶다고 상상해 보십시오.

db.with_each_row_of_result(sql_stmt) do |row|
  yield row
end

블록 API는 블록이 종료될 때 잠재적으로 모든 종류의 정리를 수행할 수 있기 때문에 훌륭합니다. 그러나 일부 소비자는 다음과 같은 방식으로 데이터베이스 작업을 원할 수 있습니다.

@cursor = cursor
 
# later:
row = @cursor.next_row
send_row_to_event_stream(row)

실제로는 "지금은" 블록 실행을 "일시 중지"하고 나중에 블록 내에서 계속하기를 원한다는 의미입니다. 따라서 호출자는 호출 수신자(블록을 수행하는 메서드)의 손에 있는 대신 흐름 제어를 인계받습니다.

연이어 반복자

이 패턴의 가장 일반적인 용도 중 하나는 여러 반복자를 함께 연결하는 것입니다. 그렇게 할 때 반복에 사용했던 메소드(예:#each ), 대신 yield를 사용하여 블록이 우리에게 보내는 값을 "잡는" 데 사용할 수 있는 Enumerator 객체를 반환합니다. 성명:

range = 1..8
each_enum = range.each # => <Enumerator...>

그러면 열거자를 연결할 수 있습니다. 이를 통해 "인덱스를 제외한 모든 반복"과 같은 작업을 수행할 수 있습니다. 이 예에서는 #map을 호출합니다. 범위에 대해 Enumerable 가져오기 물체. 그런 다음 #with_index를 연결합니다. 인덱스를 사용하여 범위를 반복하려면:

(1..3).map.with_index {|element_n, index| [element_n, index] }
#=> [[1, 0], [2, 1], [3, 2]]

이것은 특히 시스템이 이벤트를 사용하는 경우 매우 유용할 수 있습니다. Ruby는 Enumerator 생성기로 모든 메서드를 래핑하는 내장 메서드를 제공하므로 정확히 이를 수행할 수 있습니다. with_each_row_of_result에서 행을 하나씩 "가져오기"한다고 상상해보십시오. , 우리에게 제공하는 방법 대신에.

@cursor = db.to_enum(:with_each_row_of_result, sql_stmt)
schedule_for_later do
  begin
    row = @cursor.next
    send_row_to_event_stream(row)
  rescue StopIteration # the block has ended and the cursor is empty, the cleanup has taken place
  end
end

이를 직접 구현하면 다음과 같이 될 것입니다.

cursor = Enumerator.new do |yielder|
  db.with_each_row_of_result(sql_stmt) do |row|
    yielder.yield row
  end
end

블록 뒤집기

Rails를 사용하면 응답 본문을 Enumerator로 할당할 수 있습니다. next를 호출합니다. Enumerator에서 응답 본문으로 할당하고 반환된 값은 Rack 응답에 기록될 문자열이 될 것으로 예상합니다. 예를 들어 #each에 대한 호출을 반환할 수 있습니다. Rails 응답 본문으로서의 Range 메소드:

class MyController < ApplicationController
  def index
    response.body = ('a'..'z').each
  end
end

이것이 제가 블록을 뒤집는 것이라고 부르는 것입니다. 본질적으로 블록(또는 Ruby의 블록이기도 한 루프)에서 "시간을 정지"할 수 있게 해주는 제어 흐름 도우미입니다.

그러나 열거자에는 약간 덜 유용하게 만드는 제한 속성이 있습니다. 다음과 같이 하고 싶다고 상상해 보세요.

File.open('output.tmp', 'wb') do |f|
  # Yield file for writing, continuously
  loop { yield(f) }
end

열거자로 감싸서 작성해 보겠습니다.

writer_enum = File.to_enum(:open, 'output.tmp', 'wb')
file = en.next
file << data
file << more_data

모든 것이 잘 작동합니다. 그러나 문제가 있습니다. 열거자가 블록을 "완료"하고 파일을 닫고 종료할 수 있도록 쓰기를 완료했다고 어떻게 알릴 수 있습니까? 이렇게 하면 리소스 정리(파일이 닫힘)와 같은 여러 중요한 단계가 수행되고 버퍼링된 모든 쓰기가 디스크로 플러시됩니다. File에 대한 액세스 권한이 있습니다. 개체를 닫을 수 있지만 열거자가 우리를 위해 닫는 것을 관리하기를 원합니다. 열거자가 블록을 지나도록 해야 합니다.

또 다른 장애물은 때때로 우리가 일시 중단된 블록 내에서 일어나는 일에 대한 인수를 전달하기를 원한다는 것입니다. 다음과 같은 의미를 가진 블록 수락 메서드가 있다고 상상해 보세요.

write_file_through_encryptor(file_name) do |writable|
  writable << "Some data"
  writable << "Some more data"
  writable << "Even more data"
end

하지만 호출 코드에서는 다음과 같이 사용하려고 합니다.

writable = write_file_through_encryptor(file_name)
writable << "Some data"
# ...later on
writable << "Some more data"
writable.finish

이상적으로는 메서드 호출을 다음과 같은 트릭을 허용하는 구조로 래핑합니다.

write_file_through_encryptor(file_name) do |writable|
  loop do
    yield_and_wait_for_next_call(writable)
    # Then we somehow break out of this loop to let the block complete
  end
end

이런 식으로 글을 마무리한다면 어떨까요?

deferred_writable = write_file_through_encryptor(file_name)
deferred_writable.next("Some data")
deferred_writable.next("Some more data")
deferred_writable.next("Even more data")
deferred_writable.next(:terminate)

이 경우 :terminate를 사용합니다. 블록을 완료하고 반환할 수 있음을 메서드에 알려주는 마법의 값입니다. Enumerator Enumerator#next에 인수를 전달할 수 없기 때문에 실제로 도움이 되지 않습니다. . 할 수 있다면 다음과 같이 할 수 있습니다.

deferred_writable = write_file_through_encryptor(file_name)
deferred_writable.next("Some data")
...
deferred_writable.next(:terminate)

Ruby's Fibers 입력

이것이 바로 Fibers가 허용하는 것입니다. Fiber를 사용하면 재진입할 때마다 인수를 수락할 수 있습니다. , 그래서 우리는 다음과 같이 래퍼를 구현할 수 있습니다:

deferred_writable = Fiber.new do |data_to_write_or_termination|
  write_file_through_encryptor(filename) do |f|
     # Here we enter the block context of the fiber, reentry will be to the start of this block
    loop do
      # When we call Fiber.yield our fiber will be suspended—we won't reach the
      # "data_to_write_or_termination = " assignment before our fiber gets resumed
      data_to_write_or_termination = Fiber.yield
    end
  end
end

작동 방식은 다음과 같습니다. .resume을 처음 호출할 때 deferred_writable에서 , 광섬유에 들어가 첫 번째 Fiber.yield까지 이동합니다. 문 또는 가장 바깥쪽 Fiber 블록의 끝 중 먼저 오는 것. Fiber.yield를 호출할 때 , 그것은 당신에게 다시 제어를 제공합니다. 열거자를 기억하십니까? 차단이 일시중지됩니다. , 다음에 .resume을 호출할 때 , resume에 대한 인수 새로운 data_to_write가 됩니다. .

deferred_writes = Fiber.new do |data_to_write|
  loop do
    $stderr.puts "Received #{data_to_write} to work with"
    data_to_write = Fiber.yield
  end
end
# => #<Fiber:0x007f9f531783e8>
deferred_writes.resume("Hello") #=> Received Hello to work with
deferred_writes.resume("Goodbye") #=> Received Goodbye to work with
 

따라서 Fiber 내에서 코드 흐름이 시작됩니다. Fiber#resume에 대한 첫 번째 호출 시 , Fiber.yield에 대한 첫 번째 호출에서 일시 중단됨 , 그리고 계속 Fiber#resume에 대한 후속 호출 시 , 반환 값이 Fiber.yield인 경우 resume에 대한 인수가 됨 . 코드는 Fiber.yield가 있는 지점에서 계속 실행됩니다. 마지막으로 호출되었습니다.

이것은 Fiber에 대한 초기 인수가 Fiber.yield의 반환 값이 아니라 블록 인수로 전달된다는 점에서 Fiber의 약간의 기이함입니다. .

이를 염두에 두고 resume에 특수 인수를 전달하면 , 중지할지 여부를 Fiber 내에서 결정할 수 있습니다. 시도해 봅시다:

deferred_writes = Fiber.new do |data_to_write|
  loop do
    $stderr.puts "Received #{data_to_write} to work with"
    break if data_to_write == :terminate # Break out of the loop, or...
    write_to_output(data_to_write)       # ...write to the output
    data_to_write = Fiber.yield          # suspend ourselves and wait for the next `resume`
  end
  # We end up here if we break out of the loop above. There is no Fiber.yield
  # statement anywhere, so the Fiber will terminate and become "dead".
end
 
deferred_writes.resume("Hello") #=> Received Hello to work with
deferred_writes.resume("Goodbye") #=> Received Goodbye to work with
deferred_writes.resume(:terminate)
deferred_writes.resume("Some more data after close") # FiberError: dead fiber called

이러한 시설이 매우 유용할 수 있는 상황이 많이 있습니다. Fiber는 수동으로 재개할 수 있는 일시 중단된 코드 블록을 포함하므로 Fiber는 이벤트 리액터를 구현하고 단일 스레드 내에서 동시 작업을 처리하는 데 사용할 수 있습니다. 가볍기 때문에 단일 클라이언트를 단일 Fiber에 할당하고 필요에 따라 이러한 Fiber 개체 간에 전환하여 Fiber를 사용하는 서버를 구현할 수 있습니다.

client_fiber = Fiber.new do |socket|
   loop do
     received_from_client = socket.read_nonblock(10)
     sent_to_client = socket.write_nonblock("OK")
     Fiber.yield # Return control back to the caller and wait for it to call 'resume' on us
   end
end
 
client_fibers << client_fiber
 
# and then in your main webserver loop
client_fibers.each do |client_fiber|
  client_fiber.resume # Receive data from the client if any, and send it an OK
end

Ruby에는 fiber라는 추가 표준 라이브러리가 있습니다. 이를 통해 한 광섬유에서 다른 광섬유로 제어를 명시적으로 전송할 수 있으며 이는 이러한 용도에 대한 추가 기능이 될 수 있습니다.

데이터 방출 속도 제어

루비 블록이 데이터를 내보내는 속도를 제어할 수 있기를 원할 때 파이버 및 열거자의 또 다른 훌륭한 용도가 발생할 수 있습니다. 예를 들어 zip_tricks에서 라이브러리를 사용하는 기본 방법으로 다음 블록 사용을 지원합니다.

ZipTricks::Streamer.open(output_io) do |z|
  z.write_deflated_file("big.csv") do |destination|
   columns.each do |col|
     destination << column
   end
  end
end

따라서 ZIP 아카이브를 생성하는 코드 부분에서 "푸시" 제어를 허용하며 출력되는 데이터의 양과 빈도를 제어하는 ​​것은 불가능합니다. ZIP를 예를 들어 5MB 단위로 작성하려면(AWS S3 객체 스토리지의 제한 사항임) 사용자 지정 output_io를 생성해야 합니다. << 수락을 "거부"하는 개체 이 메서드는 세그먼트를 S3 멀티파트 부분으로 분할해야 할 때 호출합니다. 그러나 컨트롤을 반전하여 "당기기"로 만들 수 있습니다. 우리는 여전히 큰 CSV 파일을 작성하는 데 동일한 블록을 사용하지만 제공하는 출력에 따라 파일을 재개하고 중지합니다. 따라서 다음과 같은 사용이 가능합니다.

output_enum = ZipTricks::Streamer.output_enum do |z|
  z.write_deflated_file("big.csv") do |destination|
   columns.each do |col|
     destination << column
   end
  end
end
 
# At this point nothing has been generated or written yet
enum = output_enum.each # Create an Enumerator
bin_str = enum.next # Let the block generate some binary data and then suspend it
output.write(bin_str) # Our block is suspended and waiting for the next invocation of `next`

이를 통해 ZIP 파일 생성기가 데이터를 내보내는 속도를 제어할 수 있습니다.

따라서 Enumerator와 Fiber는 제어 흐름 메커니즘입니다. "push" 블록을 메소드 호출을 허용하는 "pull" 객체로 바꾸기 위한 것입니다.

Fibers 및 Enumerators에는 단 하나의 함정이 있습니다. ensure와 같은 항목이 있는 경우 블록에서 또는 블록이 완료된 후 수행해야 하는 작업에서 충분한 시간을 호출하는 것은 호출자에게 달려 있습니다. 어떤 면에서는 JavaScript에서 Promise를 사용할 때의 제약 조건과 비슷합니다.

결론

이것으로 Ruby의 흐름 제어 열거형에 대한 조사를 마칩니다. 그 과정에서 Julik은 EnumerableFiber 클래스, 호출자가 데이터 흐름을 결정한 예제에 뛰어들었습니다. Fiber에 대해서도 배웠습니다. 의 추가 마법으로 각 블록 재진입에 인수를 전달할 수 있습니다. 즐거운 흐름 제어!

마법을 꾸준히 사용하려면 Ruby Magic을 구독하세요. 월간 에디션을 받은 편지함으로 바로 전달해 드리겠습니다.