Không có cấu trúc dữ liệu cốt lõi nào là an toàn luồng. Điều duy nhất tôi biết trong số đó được cung cấp với Ruby là triển khai hàng đợi trong thư viện chuẩn ( require 'thread'; q = Queue.new
).
GIL của MRI không giúp chúng ta thoát khỏi các vấn đề về an toàn luồng. Nó chỉ đảm bảo rằng hai luồng không thể chạy mã Ruby cùng một lúc , tức là trên hai CPU khác nhau cùng một lúc. Các luồng vẫn có thể bị tạm dừng và tiếp tục tại bất kỳ thời điểm nào trong mã của bạn. Nếu bạn viết mã như thay @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }
đổi một biến được chia sẻ từ nhiều luồng, giá trị của biến được chia sẻ sau đó không phải là xác định. GIL ít nhiều là một mô phỏng của một hệ thống lõi đơn, nó không thay đổi các vấn đề cơ bản của việc viết các chương trình đồng thời chính xác.
Ngay cả khi MRI là một luồng như Node.js, bạn vẫn sẽ phải nghĩ về đồng thời. Ví dụ với biến tăng dần sẽ hoạt động tốt, nhưng bạn vẫn có thể nhận được các điều kiện đua trong đó mọi thứ xảy ra theo thứ tự không xác định và một lệnh gọi lại chặn kết quả của một biến khác. Các hệ thống không đồng bộ một luồng dễ lý giải hơn, nhưng chúng không tránh khỏi các vấn đề đồng thời. Chỉ cần nghĩ đến một ứng dụng có nhiều người dùng: nếu hai người dùng nhấn chỉnh sửa trên một bài đăng Stack Overflow cùng một lúc, hãy dành một chút thời gian để chỉnh sửa bài đăng và sau đó nhấn lưu, người dùng thứ ba sẽ thấy những thay đổi của họ sau đó khi họ đọc cùng một bài viết?
Trong Ruby, cũng như hầu hết các thời gian chạy đồng thời khác, bất kỳ thứ gì có nhiều hơn một thao tác đều không an toàn cho chuỗi. @n += 1
không phải là chuỗi an toàn, vì nó là nhiều hoạt động. @n = 1
luồng an toàn vì nó là một hoạt động (đó là rất nhiều hoạt động ẩn và tôi có thể sẽ gặp rắc rối nếu tôi cố gắng mô tả chi tiết lý do tại sao nó "an toàn luồng", nhưng cuối cùng bạn sẽ không nhận được kết quả không nhất quán từ các nhiệm vụ ). @n ||= 1
, không và không có phép toán viết tắt + phép gán nào khác. Một sai lầm mà tôi đã mắc phải nhiều lần là viết return unless @started; @started = true
, điều này không an toàn cho chủ đề.
Tôi không biết bất kỳ danh sách có thẩm quyền nào về các câu lệnh an toàn luồng và không an toàn cho chuỗi cho Ruby, nhưng có một quy tắc đơn giản: nếu một biểu thức chỉ thực hiện một thao tác (không có tác dụng phụ) thì có thể là chuỗi an toàn. Ví dụ: a + b
là ok, a = b
cũng ok, và a.foo(b)
ok, nếu phương thức foo
này không có tác dụng phụ (vì hầu như mọi thứ trong Ruby đều là lời gọi phương thức, thậm chí là gán trong nhiều trường hợp, điều này cũng áp dụng cho các ví dụ khác). Tác dụng phụ trong ngữ cảnh này có nghĩa là những thứ thay đổi trạng thái. def foo(x); @x = x; end
là không tác dụng phụ miễn phí.
Một trong những điều khó nhất khi viết mã an toàn luồng trong Ruby là tất cả các cấu trúc dữ liệu cốt lõi, bao gồm mảng, băm và chuỗi, đều có thể thay đổi được. Rất dễ vô tình làm rò rỉ một phần trạng thái của bạn và khi phần đó có thể thay đổi, mọi thứ có thể thực sự bị rối. Hãy xem xét đoạn mã sau:
class Thing
attr_reader :stuff
def initialize(initial_stuff)
@stuff = initial_stuff
@state_lock = Mutex.new
end
def add(item)
@state_lock.synchronize do
@stuff << item
end
end
end
Một thể hiện của lớp này có thể được chia sẻ giữa các luồng và họ có thể thêm mọi thứ vào nó một cách an toàn, nhưng có một lỗi đồng thời (nó không phải là lỗi duy nhất): trạng thái bên trong của đối tượng bị rò rỉ qua trình truy cập stuff
. Bên cạnh vấn đề từ góc độ đóng gói, nó cũng mở ra một nhóm sâu đồng thời. Có thể ai đó lấy mảng đó và chuyển nó cho một nơi khác, và đến lượt nó, đoạn mã đó lại nghĩ rằng nó hiện sở hữu mảng đó và có thể làm bất cứ điều gì nó muốn với nó.
Một ví dụ Ruby cổ điển khác là:
STANDARD_OPTIONS = {:color => 'red', :count => 10}
def find_stuff
@some_service.load_things('stuff', STANDARD_OPTIONS)
end
find_stuff
hoạt động tốt trong lần đầu tiên sử dụng, nhưng trả về thứ khác vào lần thứ hai. Tại sao? Các load_things
phương pháp xảy ra để suy nghĩ nó sở hữu các tùy chọn băm truyền cho nó, và làm color = options.delete(:color)
. Bây giờ STANDARD_OPTIONS
hằng số không có cùng giá trị nữa. Hằng số chỉ không đổi trong những gì chúng tham chiếu, chúng không đảm bảo tính ổn định của cấu trúc dữ liệu mà chúng tham chiếu. Chỉ cần nghĩ điều gì sẽ xảy ra nếu mã này được chạy đồng thời.
Nếu bạn tránh trạng thái có thể thay đổi được chia sẻ (ví dụ: biến cá thể trong các đối tượng được nhiều luồng truy cập, cấu trúc dữ liệu như băm và mảng được nhiều luồng truy cập) thì an toàn luồng không quá khó. Cố gắng giảm thiểu các phần của ứng dụng được truy cập đồng thời và tập trung nỗ lực của bạn vào đó. IIRC, trong ứng dụng Rails, một đối tượng bộ điều khiển mới được tạo cho mọi yêu cầu, vì vậy nó sẽ chỉ được sử dụng bởi một luồng duy nhất và tương tự với bất kỳ đối tượng mô hình nào bạn tạo từ bộ điều khiển đó. Tuy nhiên, Rails cũng khuyến khích việc sử dụng các biến toàn cục ( User.find(...)
sử dụng biến toàn cụcUser
, bạn có thể nghĩ nó chỉ là một lớp và nó là một lớp, nhưng nó cũng là không gian tên cho các biến toàn cục), một số trong số này an toàn vì chúng chỉ được đọc, nhưng đôi khi bạn lưu mọi thứ trong các biến toàn cục này vì nó là thuận tiện. Hãy rất cẩn thận khi bạn sử dụng bất kỳ thứ gì có thể truy cập được trên toàn cầu.
Hiện tại đã có thể chạy Rails trong môi trường luồng, vì vậy nếu không phải là một chuyên gia về Rails, tôi vẫn sẽ đi xa hơn khi nói rằng bạn không phải lo lắng về sự an toàn của luồng khi nói đến chính Rails. Bạn vẫn có thể tạo các ứng dụng Rails không an toàn cho luồng bằng cách thực hiện một số điều tôi đã đề cập ở trên. Khi nói đến các loại đá quý khác cho rằng chúng không an toàn trừ khi họ nói rằng chúng như vậy và nếu họ nói rằng chúng không phải vậy và hãy xem qua mã của chúng (nhưng chỉ vì bạn thấy rằng chúng đi những thứ như@n ||= 1
không có nghĩa là chúng không an toàn cho chuỗi, đó là điều hoàn toàn hợp pháp để làm trong ngữ cảnh phù hợp - thay vào đó, bạn nên tìm kiếm những thứ như trạng thái có thể thay đổi trong các biến toàn cục, cách nó xử lý các đối tượng có thể thay đổi được truyền cho các phương thức của nó và đặc biệt là cách nó xử lý các băm tùy chọn).
Cuối cùng, luồng không an toàn là một thuộc tính bắc cầu. Bất cứ thứ gì sử dụng thứ gì đó không phải là sợi an toàn thì bản thân nó không phải là sợi an toàn.