루비에서 스레드로부터 안전하지 않은 것이 무엇인지 아는 방법?


93

Rails 4 부터 모든 것이 기본적으로 스레드 환경에서 실행되어야합니다. 이것이 의미하는 것은 우리가 작성하는 모든 코드 우리가 사용하는 모든 gem은threadsafe

그래서 이것에 대해 몇 가지 질문이 있습니다.

  1. 루비 / 레일에서 스레드로부터 안전하지 않은 것은 무엇입니까? Vs 루비 / 레일에서 스레드로부터 안전한 것은 무엇입니까?
  2. 보석의 목록이 거기에 있다 스레드가 알려진 또는 그 반대는?
  3. 스레드 안전 예제가 아닌 코드의 일반적인 패턴 목록이 @result ||= some_method있습니까?
  4. 등의 루비 랭 코어의 데이터 구조는 Hash스레드 안전합니까?
  5. MRI에서를 제외하고 한 번에 하나의 루비 스레드 만 실행할 수 있음을 의미 하는 GVL/GILIO 가있는 경우 스레드 세이프 변경이 우리에게 영향을 줍니까?

2
모든 코드와 모든 gem이 스레드로부터 안전해야한다고 확신하십니까? 릴리스 노트에서 말하는 것은 Rails 자체가 스레드
세이프

다중 스레드 테스트는 최악의 스레드 안전 위험입니다. 테스트 케이스 주변의 환경 변수 값을 변경해야하는 경우 즉시 스레드로부터 안전하지 않습니다. 그 문제를 어떻게 해결 하시겠습니까? 그리고 예, 모든 보석은 스레드로부터 안전해야합니다.
Lukas Oberhuber 2013 년

답변:


110

핵심 데이터 구조는 스레드로부터 안전하지 않습니다. Ruby와 함께 제공되는 유일한 것은 표준 라이브러리 ( require 'thread'; q = Queue.new) 의 큐 구현입니다 .

MRI의 GIL은 스레드 안전 문제에서 우리를 구하지 않습니다. 두 개의 스레드가 동시에 Ruby 코드 실행할 수 없도록합니다 . 즉, 정확히 같은 시간에 서로 다른 두 개의 CPU에서 실행됩니다. 스레드는 코드의 어느 시점에서나 일시 중지 및 재개 될 수 있습니다. @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }예를 들어 여러 스레드에서 공유 변수를 변경하는 것과 같은 코드를 작성하면 나중에 공유 변수의 값이 결정적이지 않습니다. GIL은 다소간 단일 코어 시스템의 시뮬레이션이며 올바른 동시 프로그램 작성의 근본적인 문제를 변경하지 않습니다.

MRI가 Node.js와 같은 단일 스레드 였더라도 동시성에 대해 생각해야합니다. 증분 변수가있는 예제는 잘 작동하지만, 비 결정적 순서로 일이 발생하고 하나의 콜백이 다른 결과를 방해하는 경합 조건을 얻을 수 있습니다. 단일 스레드 비동기 시스템은 추론하기가 더 쉽지만 동시성 문제가 발생하지 않습니다. 여러 사용자가있는 응용 프로그램을 생각해보십시오. 두 명의 사용자가 거의 동시에 Stack Overflow 게시물에서 편집을 누르면 게시물을 편집하는 데 시간을 할애 한 다음 저장을 누르면 나중에 세 번째 사용자가 변경 사항을 볼 수 있습니다. 같은 게시물을 읽었습니까?

Ruby에서는 대부분의 다른 동시 런타임에서와 같이 둘 이상의 작업이 스레드로부터 안전하지 않습니다. @n += 1다중 작업이기 때문에 스레드로부터 안전하지 않습니다. @n = 1하나의 작업이기 때문에 스레드로부터 안전합니다 (내부에서 많은 작업이 수행되고 "스레드 안전"인 이유를 자세히 설명하려고하면 문제가 발생할 수 있지만 결국 할당에서 일관성없는 결과를 얻지 못할 것입니다. ). @n ||= 1, 그렇지 않으며 다른 속기 연산 + 할당도 없습니다. 내가 여러 번 저지른 한 가지 실수 return unless @started; @started = true는 쓰레드에 안전하지 않은을 쓰는 것입니다.

Ruby에 대한 스레드 안전 및 비 스레드 안전 명령문의 신뢰할 수있는 목록은 모르지만 간단한 경험 규칙이 있습니다. 표현식이 하나의 (부작용이없는) 작업 만 수행하면 스레드로부터 안전 할 수 있습니다. 예를 들어 : a + bis ok, a = bis also ok, and a.foo(b)is ok, 만약 메소드 foo가 부작용이 없다면 (루비의 거의 모든 것이 메소드 호출이고, 많은 경우 할당이기 때문에 이것은 다른 예제에도 적용됩니다). 이 맥락에서 부작용은 상태를 변경하는 것을 의미합니다. 부작용 def foo(x); @x = x; end없습니다 .

Ruby에서 스레드 안전 코드를 작성할 때 가장 어려운 점 중 하나는 배열, 해시 및 문자열을 포함한 모든 핵심 데이터 구조가 변경 가능하다는 것입니다. 실수로 상태의 일부를 유출하는 것은 매우 쉽습니다. 그리고 그 부분이 변경 가능할 때 상황이 정말 망가질 수 있습니다. 다음 코드를 고려하십시오.

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

이 클래스의 인스턴스는 스레드간에 공유 될 수 있으며 안전하게 항목을 추가 할 수 있지만 동시성 버그가 있습니다 (유일한 것이 아님). 객체의 내부 상태가 stuff접근 자를 통해 누출됩니다 . 캡슐화 관점에서 문제가되는 것 외에도 동시성 웜 캔을 엽니 다. 누군가 해당 배열을 가져 와서 다른 곳으로 전달하면 해당 코드는 이제 배열을 소유하고 원하는대로 수행 할 수 있다고 생각합니다.

또 다른 고전적인 Ruby 예제는 다음과 같습니다.

STANDARD_OPTIONS = {:color => 'red', :count => 10}

def find_stuff
  @some_service.load_things('stuff', STANDARD_OPTIONS)
end

find_stuff처음 사용할 때는 잘 작동하지만 두 번째에는 다른 것을 반환합니다. 왜? load_things방법은 전달 된 옵션 해시를 소유하고하지 생각하는 일 color = options.delete(:color). 이제 STANDARD_OPTIONS상수는 더 이상 동일한 값을 갖지 않습니다. 상수는 참조하는 내용에서만 일정하며 참조하는 데이터 구조의 일관성을 보장하지 않습니다. 이 코드가 동시에 실행되면 어떻게 될지 생각해보십시오.

공유 변경 가능 상태 (예 : 여러 스레드에 의해 액세스되는 객체의 인스턴스 변수, 여러 스레드에 의해 액세스되는 해시 및 배열과 같은 데이터 구조)를 피한다면 스레드 안전은 그렇게 어렵지 않습니다. 동시에 액세스하는 애플리케이션 부분을 최소화하고 여기에 집중하십시오. IIRC는 Rails 애플리케이션에서 모든 요청에 ​​대해 새로운 컨트롤러 객체가 생성되므로 단일 스레드에서만 사용되며 해당 컨트롤러에서 생성 한 모든 모델 객체에도 동일하게 적용됩니다. 그러나 Rails는 전역 변수 사용을 권장합니다 (전역 변수 User.find(...)사용User, 당신은 그것을 단지 클래스로 생각할 수 있으며, 그것은 클래스이지만 전역 변수의 네임 스페이스이기도합니다.) 이들 중 일부는 읽기 전용이기 때문에 안전하지만 때로는 이러한 전역 변수에 저장합니다. 편리합니다. 전 세계적으로 액세스 할 수있는 모든 것을 사용할 때는 매우주의하십시오.

꽤 오랫동안 스레드 환경에서 Rails를 실행할 수 있었기 때문에 Rails 전문가가 아니더라도 Rails 자체에 관해서는 스레드 안전성에 대해 걱정할 필요가 없다고 말할 수 있습니다. 위에서 언급 한 몇 가지 작업을 수행하여 스레드로부터 안전하지 않은 Rails 애플리케이션을 만들 수 있습니다. 다른 gem은 스레드가 안전하지 않다고 말하지 않는 한 스레드로부터 안전하지 않다고 가정하고 그렇지 않다고 가정하고 코드를 살펴 봅니다 (하지만@n ||= 1 스레드로부터 안전하지 않다는 것을 의미하지는 않습니다. 이는 올바른 컨텍스트에서 수행 할 수있는 완벽하게 합법적 인 작업입니다. 대신 전역 변수에서 변경 가능한 상태, 메서드에 전달 된 변경 가능한 객체를 처리하는 방법, 특히 방법을 찾아야합니다. 옵션 해시 처리).

마지막으로 스레드가 안전하지 않은 것은 전이 속성입니다. 스레드로부터 안전하지 않은 것을 사용하는 것은 그 자체로 스레드로부터 안전하지 않습니다.


좋은 대답입니다. 일반적인 Rails 앱이 다중 프로세스 (설명한 것처럼 동일한 앱에 액세스하는 여러 사용자)를 고려할 때 동시성 모델에 대한 스레드 의 한계 위험 이 무엇인지 궁금합니다. 즉, 얼마나 더 "위험"한지 궁금합니다. 이미 프로세스를 통해 일부 동시성을 처리하고 있다면 스레드 모드에서 실행됩니까?
gingerlime

2
@ Theo 감사합니다. 그 끊임없는 물건은 큰 폭탄입니다. 공정도 안전하지 않습니다. 하나의 요청에서 상수가 변경되면 이후 요청에서 단일 스레드에서도 변경된 상수를 볼 수 있습니다. 루비 상수가 이상합니다
rubish

5
수행 STANDARD_OPTIONS = {...}.freeze얕은 돌연변이에 인상에
glebm

정말 좋은 답변
체인로

3
" @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }[...] 와 같은 코드를 작성하면 나중에 공유 변수의 값이 결정적이지 않습니다." -이것이 Ruby 버전마다 다른지 아십니까? 예를 들어, 1.8에서 코드를 실행하는 다른 값을 제공 @n하지만, 나중에 1.9에 지속적으로 제공하는 것 @n(300)과 동일
user200783

10

Theo의 답변 외에도 config.threadsafe로 전환하는 경우 특별히 Rails에서 살펴볼 몇 가지 문제 영역을 추가하겠습니다!

  • 클래스 변수 :

    @@i_exist_across_threads

  • ENV :

    ENV['DONT_CHANGE_ME']

  • 스레드 :

    Thread.start


9

Rails 4부터 모든 것이 기본적으로 스레드 환경에서 실행되어야합니다.

이것은 100 % 정확하지 않습니다. Thread-safe Rails는 기본적으로 켜져 있습니다. Passenger (커뮤니티) 또는 Unicorn과 같은 다중 프로세스 앱 서버에 배포하면 전혀 차이가 없습니다. 이 변경은 Puma 또는 Passenger Enterprise> 4.0과 같은 다중 스레드 환경에 배포하는 경우에만 해당됩니다.

과거에는 다중 스레드 앱 서버에 배포하려는 경우 config.threadsafe 를 켜야했습니다. 현재 기본값 인 config.threadsafe 는 모두 효과가 없거나 단일 프로세스에서 실행되는 Rails 앱에도 적용 되었기 때문입니다 ( Prooflink ).

그러나 Rails 4 스트리밍의 모든 이점과 멀티 스레드 배포의 기타 실시간 기능을 원한다면 기사가 흥미로울 것입니다. @Theo 슬프게도 Rails 앱의 경우 실제로 요청 중에 변경되는 정적 상태를 생략하면됩니다. 이것은 따라야 할 간단한 관행이지만 불행히도 모든 보석에 대해 확신 할 수는 없습니다. 내가 기억하는 한, JRuby 프로젝트의 Charles Oliver Nutter는 팟 캐스트 에서 이에 대한 몇 가지 팁을 제공했습니다 .

그리고 하나 이상의 스레드에서 액세스하는 데이터 구조가 필요한 순수한 동시 Ruby 프로그래밍을 작성하려면 thread_safe gem이 유용 할 것입니다 .

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.