làm thế nào để biết những gì KHÔNG an toàn sợi trong ruby?


93

bắt đầu từ Rails 4 , mọi thứ sẽ phải chạy trong môi trường luồng theo mặc định. Điều này có nghĩa là tất cả mã chúng tôi viết TẤT CẢ các viên ngọc chúng tôi sử dụng đều được yêu cầuthreadsafe

vì vậy, tôi có một số câu hỏi về điều này:

  1. điều gì KHÔNG an toàn cho sợi trong ruby ​​/ rails? Vs Chỉ an toàn trong ruby ​​/ rails là gì?
  2. Có danh sách các gem được biết là threadsafe hoặc ngược lại không?
  3. có Danh sách các mẫu mã phổ biến KHÔNG phải là ví dụ về threadsafe @result ||= some_method?
  4. Có phải các cấu trúc dữ liệu trong lõi ruby ​​lang như Hashetc threadsafe không?
  5. Trên MRI, nơi có a GVL/GIL có nghĩa là chỉ có 1 sợi ruby ​​có thể chạy tại một thời điểm ngoại trừ IOviệc thay đổi threadsafe có ảnh hưởng đến chúng ta không?

2
Bạn có chắc chắn rằng tất cả mã và tất cả đá quý sẽ PHẢI được an toàn không? Có gì release notes nói là Rails chính nó sẽ được threadsafe, không phải là tất cả mọi thứ khác sử dụng với nó phải được
enthrops

Kiểm tra đa luồng sẽ là rủi ro an toàn luồng tồi tệ nhất có thể. Khi bạn phải thay đổi giá trị của một biến môi trường xung quanh trường hợp thử nghiệm của mình, bạn sẽ không an toàn ngay lập tức. Bạn sẽ giải quyết vấn đề đó như thế nào? Và vâng, tất cả các viên ngọc phải được an toàn.
Lukas Oberhuber

Câu trả lời:


110

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 += 1không phải là chuỗi an toàn, vì nó là nhiều hoạt động. @n = 1luồ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 + blà ok, a = bcũng ok, và a.foo(b)ok, nếu phương thức foonà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; endkhô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_stuffhoạ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_thingsphươ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_OPTIONShằ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.


Câu trả lời chính xác. Xem xét rằng một ứng dụng rails điển hình là đa quy trình (như bạn đã mô tả, nhiều người dùng khác nhau truy cập vào cùng một ứng dụng), tôi đang tự hỏi rủi ro biên của các luồng đối với mô hình đồng thời là gì ... Nói cách khác, "nguy hiểm" hơn bao nhiêu nó có chạy ở chế độ phân luồng nếu bạn đang xử lý một số đồng thời thông qua các quy trình không?
Gingerlime

2
@Theo Cảm ơn rất nhiều. Những thứ liên tục đó là một quả bom lớn. Nó thậm chí không phải là quá trình an toàn. Nếu hằng số được thay đổi trong một yêu cầu, nó sẽ khiến các yêu cầu sau đó nhìn thấy hằng số đã thay đổi ngay cả trong một luồng đơn lẻ. Ruby hằng là lạ
rubish

5
Làm STANDARD_OPTIONS = {...}.freezeđể tăng đột biến trên cạn
glebm

Câu trả lời thực sự tuyệt vời
Cheyne

3
"Nếu bạn viết mã như @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }[...], giá trị của biến được chia sẻ sau đó là không xác định." - Bạn có biết điều này có khác nhau giữa các phiên bản của Ruby không? Ví dụ, chạy mã của bạn trên 1.8 cung cấp cho các giá trị khác nhau của @n, nhưng trên 1.9 và sau đó nó dường như luôn đưa ra @nbằng 300.
user200783

10

Ngoài câu trả lời của Theo, tôi sẽ thêm một số vấn đề cần tìm kiếm cụ thể trong Rails, nếu bạn đang chuyển sang config.threadsafe!

  • Các biến lớp :

    @@i_exist_across_threads

  • VIV :

    ENV['DONT_CHANGE_ME']

  • Chủ đề :

    Thread.start


9

bắt đầu từ Rails 4, mọi thứ sẽ phải chạy trong môi trường luồng theo mặc định

Điều này không đúng 100%. Rails an toàn luồng chỉ được bật theo mặc định. Nếu bạn triển khai trên một máy chủ ứng dụng đa quy trình như Passenger (cộng đồng) hoặc Unicorn thì sẽ không có sự khác biệt nào cả. Thay đổi này chỉ liên quan đến bạn, nếu bạn triển khai trên môi trường đa luồng như Puma hoặc Passenger Enterprise> 4.0

Trước đây, nếu bạn muốn triển khai trên một máy chủ ứng dụng đa luồng, bạn phải bật config.threadsafe , đây là chế độ mặc định, vì tất cả những gì nó làm không có tác dụng hoặc cũng được áp dụng cho ứng dụng Rails chạy trong một quy trình ( Liên kết (Prooflink ).

Nhưng nếu bạn làm muốn tất cả các Rails 4 luồng lợi ích và các công cụ thời gian thực khác của việc triển khai đa luồng thì có lẽ bạn sẽ tìm thấy này thú vị bài viết. Thật đáng buồn là @Theo, đối với một ứng dụng Rails, bạn thực sự chỉ phải bỏ qua trạng thái tĩnh thay đổi trong một yêu cầu. Mặc dù đây là một thực hành đơn giản để làm theo, nhưng tiếc là bạn không thể chắc chắn về điều này cho mỗi viên ngọc bạn tìm thấy. Theo như tôi nhớ Charles Oliver Nutter từ dự án JRuby đã có một số lời khuyên về nó trong podcast này .

Và nếu bạn muốn viết một lập trình Ruby thuần túy đồng thời, nơi bạn sẽ cần một số cấu trúc dữ liệu được truy cập bởi nhiều hơn một luồng, bạn có thể sẽ thấy gem thread_safe hữu ích.

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.