2012-01-29 55 views
80

Đối với Sợi chúng tôi đã có ví dụ điển hình: tạo các số FibonacciTại sao chúng ta cần sợi

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 Sợi đây? Tôi có thể viết lại này chỉ với cùng Proc (đóng cửa, trên thực tế)

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

Vì vậy

10.times { puts fib.resume } 

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

sẽ trở lại chỉ là kết quả tương tự.

Vậy ưu điểm của sợi là gì. Tôi có thể viết những thứ gì với các loại sợi mà tôi không thể làm với lambdas và các tính năng tuyệt vời khác của Ruby?

+4

Ví dụ về cú pháp cũ chỉ là động cơ thúc đẩy tồi tệ nhất ;-) Thậm chí còn có công thức bạn có thể sử dụng để tính số _any_ fibonacci trong O (1). – usr

+15

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

Trả lời

197

Sợi là thứ 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 kiểm soát dòng chảy mà bạn có thể sử dụng để xây dựng các trừu tượng khác, mà sau đó bạn sử dụng trong mã mức cao hơn.

Có lẽ việc sử dụng # 1 các sợi trong Ruby là triển khai Enumerator s, là một lớp Ruby cốt lõi trong Ruby 1.9. Đây là vô cùng hữu ích.

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

Những Enumerator s là những đối tượng Enumerable, và each phương pháp của họ mang lại những yếu tố mà có thể đã được mang lại bởi các phương pháp lặp ban đầu, mà nếu nó được gọi với một khối. Trong ví dụ tôi vừa đưa ra, ĐTV được trả về bởi reverse_each có phương pháp each có năng suất 3,2,1. Điều tra viên trả về bởi chars sản lượng "c", "b", "a" (và vân vân). NHƯNG, không giống như các phương pháp lặp gốc, Enumerator cũng có thể trở lại các yếu tố từng người một nếu bạn gọi next vào nó lặp đi lặp lại:

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ề "lặp nội bộ" và "lặp bên ngoài" (tốt mô tả của cả hai được đưa ra trong cuốn sách "Gang of Four" Design Patterns). Ví dụ trên cho thấy rằng Enumerators có thể được sử dụng để biến một iterator nội bộ thành một bên ngoài.

Đây là một cách để làm cho đ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 phút ... không bất cứ điều gì có vẻ kỳ lạ đó? Bạn đã viết các câu lệnh yield trong an_iterator dưới dạng mã đường thẳng, nhưng Điều tra viên có thể chạy chúng một tại một thời điểm. Giữa các cuộc gọi đến next, việc thực hiện an_iterator bị "cố định". Mỗi lần bạn gọi next, nó sẽ tiếp tục chạy xuống theo câu lệnh yield sau và sau đó "đóng băng" lại.

Bạn có thể đoán cách triển khai này không? Điều tra viên kết thúc cuộc gọi đến an_iterator bằng sợi quang và chuyển một khối mà treo sợi. Vì vậy, mỗi lần an_iterator mang lại khối, sợi mà nó đang chạy bị treo và việc thực hiện tiếp tục trên luồng chính. Lần tới, bạn gọi next, nó chuyển quyền kiểm soát tới sợi, khối trả vềan_iterator tiếp tục ở nơi nó bị tắt.

Sẽ có tính hướng dẫn để suy nghĩ về những gì cần thiết để thực hiện việc này mà không cần sợi. MỌI lớp mà muốn cung cấp cả bộ lặp nội bộ 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 cuộc gọi đến next. Mỗi cuộc gọi đến 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 sợi, chúng tôi có thể tự động chuyển đổi bất kỳ trình lặp nội bộ nào sang một bộ lặp bên ngoài.

Điều này không liên quan gì đến sợi, nhưng hãy để tôi đề cập đến một điều nữa bạn có thể làm với Điều tra viên: chúng cho phép bạn áp dụng các phương pháp đếm được bậc cao hơn cho các trình lặp khác ngoài each. Hãy suy nghĩ về nó: thông thường tất cả các phương pháp liệt kê, bao gồm map, select, include?, inject, v.v. tất cả hoạt động trên các thành phần được cung cấp bởi each. Nhưng nếu một đối tượng có các trình lặp khác ngoài each thì sao?

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

Gọi trình vòng lặp không trả về bộ đếm, sau đó bạn có thể gọi các phương thức khác.

Lấy lại sợi, bạn đã sử dụng phương pháp take từ Enumerable chưa?

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

Nếu có bất kỳ điều gì gọi là phương pháp each, có vẻ như không bao giờ nên quay lại, phải không? Kiểm tra điều này:

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

Tôi không biết nếu điều này sử dụng sợi dưới mui xe, nhưng nó có thể. Sợi có thể được sử dụng để thực hiện danh sách vô hạn và đánh giá lười biếng của một loạt. Ví dụ về một số phương pháp lười biếng được xác định với Điều tra viên, tôi đã xác định một số ở đâ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ở coroutine có mục đích chung 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 tôi, nhưng đó là một khái niệm tốt để biết.

Tôi hy vọng điều này sẽ cung cấp cho bạn một số ý tưởng về các khả năng. Như tôi đã nói lúc đầu, sợi là một nguyên thủy kiểm soát dòng chảy cấp thấp. Chúng có thể duy trì nhiều vị trí "điều khiển" trong chương trình của bạn (giống như các "dấu trang" khác nhau trong các trang của một cuốn sách) và chuyển đổi giữa chúng như mong muốn. Kể từ khi 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ứ ba trên một sợi, và sau đó "đóng băng" nó và tiếp tục làm một cái gì đó khác khi nó gọi lại vào 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 khách hàng. Một sự tương tác hoàn chỉnh với một khách hàng liên quan đến việc đi qua một loạt các bước, nhưng mỗi kết nối 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. Thay vì lưu trữ rõ ràng trạng thái đó và kiểm tra trạng thái đó mỗi lần khách hàng kết nối (để xem bước "tiếp theo" mà họ phải làm là), bạn có thể duy trì chất xơ cho từng khách hàng . Sau khi xác định khách hàng, bạn sẽ lấy lại sợi của họ và khởi động lại nó. Sau đó, ở cuối mỗi kết nối, bạn sẽ đình chỉ sợi và lưu trữ nó một lần nữa. Bằng cách này, bạn có thể viết mã thẳng để thực hiện tất cả cá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 tự nhiên nếu chương trình của bạn được thực hiện để chạy cục bộ).

Tôi chắc chắn có nhiều lý do tại sao một điều như vậy có thể không thực tế (ít nhất là cho bây giờ), 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 nhận được khái niệm, bạn có thể đưa ra một số ứng dụng hoàn toàn mới mà không ai khác nghĩ đến!

+0

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

+0

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

+0

Tôi muốn! :) Với niềm vui lớn! – fl00r

17

Không giống như một đóng cửa, trong đó có một xuất nhập cảnh điểm xác định, sợi có thể duy trì trạng thái và trở về (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 này:

some code 
return 
received param: param 
etc 

Thực hiện điều này logic với các tính năng ruby ​​khác sẽ ít có thể đọc được.

Với tính năng này, việc sử dụng xơ tốt là thực hiện lập lịch hợp tác thủ công (như là Chủ đề thay thế). Ilya Grigorik có một ví dụ tốt về cách biến một thư viện không đồng bộ (eventmachine trong trường hợp này) thành một API trông giống như một API đồng bộ mà không mất đi các ưu điểm của việc lập lịch trình IO của việc thực hiện không đồng bộ. Đây là link.

+0

Cảm ơn bạn! Tôi đọc tài liệu, vì vậy tôi hiểu tất cả ma thuật này với nhiều mục và lối thoát bên trong sợi. Nhưng tôi không chắc chắn 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 đó là ý tưởng hay khi cố gắng làm theo tất cả các hồ sơ và sản lượng này. Nó trông giống như một cái khóa khó có thể tháo gỡ được. Vì vậy, tôi muốn hiểu nếu có những trường hợp mà sợi dây này là giải pháp tốt. Eventmachine thật tuyệt nhưng không phải là nơi tốt nhất để hiểu sợi, bởi vì trước tiên bạn nên hiểu tất cả những điều mẫu lò phản ứng này. Vì vậy, tôi tin rằng tôi có thể hiểu sợi 'ý nghĩa vật lý' trong ví dụ đơn giản hơn – fl00r

Các vấn đề liên quan