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 Enumerator
s, 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 each
phươ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_each
có một each
phương thức cho kết quả là 3,2,1. Điều tra viên được trả về bằng kết chars
quả "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 next
nó 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 yield
câu lệnh an_iterator
dướ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_iterator
bị "đóng băng". Mỗi lần bạn gọi next
, nó tiếp tục chạy xuống yield
câ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_iterator
trong 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_iterator
quả 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ại và an_iterator
tiế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 take
phươ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 each
phươ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!