Tại sao chúng ta cần sợi


100

Đối với sợi, chúng tôi có ví dụ cổ điển: tạo số Fibonacci

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

Tại sao chúng ta cần Fibers ở đây? Tôi có thể viết lại điều này chỉ với cùng một Proc (thực ra là đóng)

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

Vì thế

10.times { puts fib.resume }

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

sẽ trả về cùng một kết quả.

Vậy ưu điểm của sợi là gì. Loại nội dung nào tôi có thể viết với Fibers mà tôi không thể làm với lambdas và các tính năng thú vị khác của Ruby?


4
Ví dụ fibonacci cũ chỉ là động lực tồi tệ nhất có thể xảy ra ;-) Thậm chí có một công thức bạn có thể sử dụng để tính bất kỳ số fibonacci nào trong O (1).
usr

17
Vấn đề không phải là về thuật toán, nhưng về sự hiểu biết sợi :)
fl00r

Câu trả lời:


229

Sợi là thứ mà bạn có thể sẽ không bao giờ sử dụng trực tiếp trong mã cấp ứng dụng. Chúng là một nguyên thủy điều khiển luồng mà bạn có thể sử dụng để xây dựng các bản tóm tắt khác, sau đó bạn sẽ sử dụng trong mã cấp cao hơn.

Có lẽ cách sử dụng số 1 của các sợi trong Ruby là thực thi Enumerators, là một lớp Ruby cốt lõi trong Ruby 1.9. Chúng vô cùng hữu ích.

Trong Ruby 1.9, nếu bạn gọi hầu hết bất kỳ phương thức vòng lặp nào trên các lớp lõi, mà không chuyển một khối, nó sẽ trả về một 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)>

Các Enumeratorđối tượng này là các đối tượng Enumerable, và các eachphương thức của chúng mang lại các phần tử mà lẽ ra phương thức trình lặp ban đầu sẽ mang lại, nếu nó được gọi bằng một khối. Trong ví dụ tôi vừa đưa ra, Enumerator được trả về reverse_eachcó một eachphương thức cho kết quả là 3,2,1. Điều tra viên được trả về bằng kết charsquả "c", "b", "a" (v.v.). NHƯNG, không giống như phương thức trình lặp ban đầu, Enumerator cũng có thể trả về từng phần tử một nếu bạn gọi nextnó nhiều lần:

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"

Bạn có thể đã nghe nói về "trình vòng lặp nội bộ" và "trình vòng lặp bên ngoài" (mô tả tốt về cả hai được đưa ra trong cuốn sách Mẫu thiết kế "Nhóm bốn người"). Ví dụ trên cho thấy rằng Enumerator có thể được sử dụng để biến một trình lặp bên trong thành một trình lặp bên ngoài.

Đây là một cách để tạo điều tra viên của riêng bạn:

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

Hãy thử nó:

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

Chờ một chút ... có gì lạ ở đó không? Bạn đã viết các yieldcâu lệnh an_iteratordưới dạng mã dòng thẳng, nhưng Điều tra viên có thể chạy từng câu một . Giữa các lần gọi tới next, việc thực thi an_iteratorbị "đóng băng". Mỗi lần bạn gọi next, nó tiếp tục chạy xuống yieldcâu lệnh sau , rồi lại "đóng băng".

Bạn có thể đoán làm thế nào điều này được thực hiện? Enumerator kết thúc cuộc gọi đến an_iteratortrong một sợi, và chuyển một khối để tạm dừng sợi . Vì vậy, mỗi khi kết an_iteratorquả cho khối, sợi mà nó đang chạy sẽ bị treo và quá trình thực thi tiếp tục trên luồng chính. Lần tới khi bạn gọi next, nó sẽ chuyển quyền điều khiển cho sợi, khối sẽ quay trở lạian_iteratortiếp tục ở nơi nó dừng lại.

Sẽ rất hữu ích khi nghĩ về những gì cần thiết để làm điều này nếu không có sợi. MỌI lớp muốn cung cấp cả trình vòng lặp bên trong và bên ngoài sẽ phải chứa mã rõ ràng để theo dõi trạng thái giữa các lần gọi đến next. Mỗi cuộc gọi tới tiếp theo sẽ phải kiểm tra trạng thái đó và cập nhật nó trước khi trả về một giá trị. Với các sợi, chúng tôi có thể tự động chuyển đổi bất kỳ bộ lặp bên trong nào thành bộ lặp bên ngoài.

Điều này không liên quan đến các sợi liên tục, nhưng hãy để tôi đề cập đến một điều nữa bạn có thể làm với các Enumerator: chúng cho phép bạn áp dụng các phương thức Enumerable bậc cao hơn cho các trình vòng lặp khác each. Hãy suy nghĩ về nó: bình thường tất cả các phương pháp Enumerable, bao gồm map, select, include?, inject, và như vậy, tất cả các công việc trên các yếu tố mang lại bởi each. Nhưng điều gì sẽ xảy ra nếu một đối tượng có các trình vòng lặp khác 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]

Việc gọi trình lặp không có khối sẽ trả về một Enumerator và sau đó bạn có thể gọi các phương thức Enumerable khác trên đó.

Quay lại với các sợi, bạn đã sử dụng takephương pháp từ Enumerable chưa?

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

Nếu bất cứ điều gì gọi eachphương thức đó, có vẻ như nó sẽ không bao giờ trở lại, phải không? Kiểm tra cái này:

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

Tôi không biết liệu điều này có sử dụng sợi dưới mui xe hay không, nhưng nó có thể. Fibre có thể được sử dụng để triển khai danh sách vô hạn và đánh giá lười biếng của một chuỗi. Để có ví dụ về một số phương thức lười biếng được xác định bằng Enumerator, tôi đã định nghĩa một số phương thức tại đây: https://github.com/alexdowad/showcase/blob/master/ruby-core/collections.rb

Bạn cũng có thể xây dựng một cơ sở điều tra mục đích chung bằng cách sử dụng sợi. Tôi chưa bao giờ sử dụng coroutines trong bất kỳ chương trình nào của mình, nhưng đó là một khái niệm tốt cần biết.

Tôi hy vọng điều này cung cấp cho bạn một số ý tưởng về các khả năng. Như tôi đã nói ở phần đầu, sợi là một dạng nguyên thủy điều khiển dòng chảy cấp thấp. Chúng giúp bạn có thể duy trì nhiều "vị trí" luồng điều khiển trong chương trình của bạn (như các "dấu trang" khác nhau trong các trang của sách) và chuyển đổi giữa chúng như mong muốn. Vì mã tùy ý có thể chạy trong một sợi, bạn có thể gọi vào mã của bên thứ 3 trên sợi, sau đó "đóng băng" nó và tiếp tục làm việc khác khi nó gọi lại thành mã mà bạn kiểm soát.

Hãy tưởng tượng một cái gì đó như thế này: bạn đang viết một chương trình máy chủ sẽ phục vụ nhiều máy khách. Một tương tác hoàn chỉnh với một máy khách bao gồm việc trải qua một loạt các bước, nhưng mỗi kết nối chỉ là tạm thời và bạn phải nhớ trạng thái cho mỗi máy khách giữa các kết nối. (Nghe giống như lập trình web?)

Thay vì lưu trữ trạng thái đó một cách rõ ràng và kiểm tra nó mỗi khi một máy khách kết nối (để xem "bước" tiếp theo họ phải làm là gì), bạn có thể duy trì một sợi quang cho từng máy khách. Sau khi xác định được khách hàng, bạn sẽ lấy sợi quang của họ và khởi động lại. Sau đó, khi kết thúc mỗi kết nối, bạn sẽ tạm ngưng cáp quang và lưu trữ lại. Bằng cách này, bạn có thể viết mã dòng thẳng để triển khai tất cả logic cho một tương tác hoàn chỉnh, bao gồm tất cả các bước (giống như bạn thường làm nếu chương trình của bạn được tạo để chạy cục bộ).

Tôi chắc rằng có nhiều lý do khiến một điều như vậy có thể không thực tế (ít nhất là hiện tại), nhưng một lần nữa, tôi chỉ đang cố gắng cho bạn thấy một số khả năng. Ai biết; một khi bạn có khái niệm, bạn có thể nghĩ ra một số ứng dụng hoàn toàn mới mà chưa ai nghĩ ra!


Cảm ơn bạn vì câu trả lời của bạn! Vì vậy, tại sao họ không thực hiện charshoặc các điều tra viên khác chỉ với các đóng cửa?
fl00r

@ fl00r, tôi đang nghĩ đến việc bổ sung thêm thông tin, nhưng tôi không biết câu trả lời này đã quá dài chưa ... bạn có muốn thêm không?
Alex D

13
Câu trả lời này hay đến nỗi nó nên được viết như một bài đăng trên blog ở đâu đó, siêu sao.
Jason Voegele

1
CẬP NHẬT: Có vẻ như Enumerablesẽ bao gồm một số phương thức "lười biếng" trong Ruby 2.0.
Alex D

2
takekhông yêu cầu một chất xơ. Thay vào đó, takechỉ cần ngắt quãng trong năng suất thứ n. Khi được sử dụng bên trong một khối, breaktrả lại quyền điều khiển cho khung xác định khối. a = [] ; InfiniteSeries.new.each { |x| a << x ; break if a.length == 10 } ; a
Matthew

22

Không giống như các bao đóng, có một điểm vào và ra xác định, các sợi có thể bảo toàn trạng thái của chúng và quay trở lại (năng suất) nhiều lần:

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

in cái này:

some code
return
received param: param
etc

Việc thực hiện logic này với các tính năng khác của ruby ​​sẽ khó đọc hơn.

Với tính năng này, việc sử dụng sợi tốt là lập lịch trình hợp tác thủ công (thay thế sợi chỉ). Ilya Grigorik có một ví dụ điển hình về cách biến một thư viện không đồng bộ ( eventmachinetrong trường hợp này) thành một API đồng bộ mà không làm mất đi những lợi thế của việc lập lịch IO cho quá trình thực thi không đồng bộ. Đây là liên kết .


Cảm ơn bạn! Tôi đọc tài liệu, vì vậy tôi hiểu tất cả điều kỳ diệu này với nhiều mục nhập và lối ra bên trong sợi. Nhưng tôi không chắc rằng công cụ này làm cho cuộc sống dễ dàng hơn. Tôi không nghĩ rằng cố gắng làm theo tất cả những lý lịch và kết quả này là một ý kiến ​​hay. Nó trông giống như một cái khóa rất khó gỡ rối. Vì vậy, tôi muốn hiểu nếu có những trường hợp mà nắm chặt các sợi này là giải pháp tốt. Eventmachine rất tuyệt nhưng không phải là nơi tốt nhất để hiểu về sợi, vì trước tiên bạn nên hiểu tất cả những thứ về mô hình lò phản ứng này. Vì vậy, tôi tin rằng tôi có thể hiểu sợi physical meaningtrong một ví dụ đơn giản hơn
fl00r
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.