핵심 데이터 구조는 스레드로부터 안전하지 않습니다. 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 + b
is ok, a = b
is 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
스레드로부터 안전하지 않다는 것을 의미하지는 않습니다. 이는 올바른 컨텍스트에서 수행 할 수있는 완벽하게 합법적 인 작업입니다. 대신 전역 변수에서 변경 가능한 상태, 메서드에 전달 된 변경 가능한 객체를 처리하는 방법, 특히 방법을 찾아야합니다. 옵션 해시 처리).
마지막으로 스레드가 안전하지 않은 것은 전이 속성입니다. 스레드로부터 안전하지 않은 것을 사용하는 것은 그 자체로 스레드로부터 안전하지 않습니다.