ruby에서 병렬 및 전역 자물쇠 상세 정보

앞말
본고는 주로 루비의 병발 병행과 전역 자물쇠에 관한 내용을 소개하고 여러분의 참고 학습을 위해 공유합니다. 다음은 더 이상 할 말이 없습니다. 상세한 소개를 봅시다.
병렬
개발할 때 우리는 두 가지 개념을 자주 접할 수 있다. 병발과 병행이다. 거의 모든 병발과 병행에 관한 문장은 한 가지를 언급한다. 병발은 병발과 같지 않다.그렇다면 이 말을 어떻게 이해합니까?
  • 병발: 요리사가 두 명의 손님이 주문한 메뉴를 동시에 접수하여 처리해야 한다.
  • 순서 집행: 만약에 요리사가 한 명만 있다면, 그는 단지 하나의 메뉴를 이어서 하나의 메뉴를 완성할 수 있을 뿐이다
  • 병행 집행: 요리사가 두 명이면 병행해서 두 사람이 함께 요리를 할 수 있다
  • 이 예시를 우리의 웹 개발로 확장하면 다음과 같이 이해할 수 있다.
  • 동시 발송: 서버는 두 클라이언트가 발기한 요청을 동시에 받았습니다.
  • 순서 실행: 서버는 하나의 프로세스(루틴)만 요청을 처리하고 첫 번째 요청을 완성해야만 두 번째 요청을 완성할 수 있기 때문에 두 번째 요청은 기다려야 합니다
  • 병행 실행: 서버에는 두 개의 프로세스(루틴) 처리 요청이 있고 두 개의 요청은 모두 응답을 받을 수 있으며 선후의 문제가 존재하지 않습니다
  • 상술한 예에 근거하여 우리는 루비에서 어떻게 이런 것을 모의하여 발행합니까?다음 코드를 참조하십시오.
    1. 순서대로 실행:
    시뮬레이션은 하나의 라인만 있을 때의 조작이다.
    
    require 'benchmark'
    
    def f1
     puts "sleep 3 seconds in f1
    " sleep 3 end def f2 puts "sleep 2 seconds in f2
    " sleep 2 end Benchmark.bm do |b| b.report do f1 f2 end end ## ## user system total real ## sleep 3 seconds in f1 ## sleep 2 seconds in f2 ## 0.000000 0.000000 0.000000 ( 5.009620)
    상술한 코드는 매우 간단해서sleep로 시간을 소모하는 조작을 시뮬레이션한다.순서대로 실행할 때의 소모 시간.
    2. 병행 실행
    다중 스레드 시뮬레이션 작업
    
    #  
    Benchmark.bm do |b|
     b.report do
     threads = []
     threads << Thread.new { f1 }
     threads << Thread.new { f2 }
     threads.each(&:join)
     end 
    end
    ##
    ## user  system  total  real
    ## sleep 3 seconds in f1
    ## sleep 2 seconds in f2
    ## 0.000000 0.000000 0.000000 ( 3.005115)
    우리는 다중 노드에서 소모되는 시간과 f1의 소모 시간이 비슷하다는 것을 발견했다. 이것은 우리가 예상한 것과 마찬가지로 다중 노드를 사용하면 병행할 수 있다.
    루비의 다중 스레드는 IO Block에 대응할 수 있으며, 어떤 스레드가 IO Block 상태에 있을 때, 다른 스레드는 계속 실행할 수 있어 전체 처리 시간을 대폭 단축시킬 수 있다.
    루비의 스레드
    상기 코드 예시에서 루비의Thread 루틴 클래스를 사용했습니다. 루비는Thread 클래스의 다중 루틴 프로그램을 쉽게 쓸 수 있습니다.루비 라인은 당신의 코드의 병행을 실현하기 위해 경량급과 효과적인 방식이다.
    이어서 병발할 때의 정경을 묘사하다
    
     def thread_test
     time = Time.now
     threads = 3.times.map do 
      Thread.new do
      sleep 3 
      end
     end
     puts " 3 :#{Time.now - time}"
     threads.map(&:join)
     puts " 3 :#{Time.now - time}"
     end
     test
     ##  3 :8.6e-05
     ##  3 :3.003699
    Thread 생성이 막히지 않으므로 텍스트를 즉시 출력할 수 있습니다.이렇게 해서 하나의 병행 행위를 모의하였다.각 스레드sleep 3초, 막힌 상황에서 다중 스레드는 병행할 수 있습니다.
    그러면 이쯤에서 저희가 병행 능력을 완성하지 않았을까요?
    매우 유감스럽지만, 나의 상술한 묘사에서 단지 우리가 막히지 않는 상황에서 시뮬레이션을 병행할 수 있다는 것을 언급했을 뿐이다.다른 예를 살펴보겠습니다.
    
    require 'benchmark'
    def multiple_threads
     count = 0
     threads = 4.times.map do 
     Thread.new do
      2500000.times { count += 1}
     end
     end
     threads.map(&:join)
    end
    
    def single_threads
     time = Time.now
     count = 0
     Thread.new do
     10000000.times { count += 1}
     end.join
    end
    
    Benchmark.bm do |b|
     b.report { multiple_threads }
     b.report { single_threads }
    end
    ##  user  system  total  real
    ## 0.600000 0.010000 0.610000 ( 0.607230)
    ## 0.610000 0.000000 0.610000 ( 0.623237)
    여기서 알 수 있듯이 우리가 같은 임무를 네 개의 라인으로 나누어 병행하더라도 시간이 줄어들지 않는 것은 왜일까?
    글로벌 자물쇠(GIL)가 있으니까!!!
    전역 잠금
    우리가 일반적으로 사용하는 루비는 GIL이라고 불리는 메커니즘을 채택했다.
    설령 우리가 다중 스레드를 사용하여 코드의 병행을 실현하기를 원한다 하더라도, 이 전역 자물쇠의 존재로 인해 매번 하나의 스레드만 코드를 실행할 수 있고, 어느 스레드가 실행될 수 있는지는 하부 운영체제의 실현에 달려 있다.
    설령 우리가 여러 개의 CPU를 가지고 있다 하더라도, 모든 라인의 실행을 위해 몇 가지 선택을 더 제공할 뿐이다.
    우리 위의 코드에는 매번 하나의 라인만 실행할 수 있습니다count + = 1.
    Ruby 멀티스레드는 멀티 코어 CPU를 재사용할 수 없습니다. 멀티스레드를 사용한 후 전체적으로 소요되는 시간이 짧지 않고 오히려 스레드 전환의 영향으로 소요되는 시간이 약간 증가할 수 있습니다.
    하지만 우리가 전에 sleep를 했을 때 분명히 병행을 이루었잖아!
    이것이 바로 루비 디자인의 고급 부분이다. 모든 차단 조작은 병행할 수 있고, 파일 읽기, 네트워크 요청을 포함한 조작은 병행할 수 있다.
    
    require 'benchmark'
    require 'net/http'
    
    #  
    def multiple_threads
     uri = URI("http://www.baidu.com")
     threads = 4.times.map do 
     Thread.new do
      25.times { Net::HTTP.get(uri) }
     end
     end
     threads.map(&:join)
    end
    
    def single_threads
     uri = URI("http://www.baidu.com")
     Thread.new do
     100.times { Net::HTTP.get(uri) }
     end.join
    end
    
    Benchmark.bm do |b|
     b.report { multiple_threads }
     b.report { single_threads }
    end
    
     user  system  total  real
    0.240000 0.110000 0.350000 ( 3.659640)
    0.270000 0.120000 0.390000 ( 14.167703)
    네트워크가 요청할 때 프로그램이 막혔는데, 이러한 막힘은 루비의 운행하에서 병행할 수 있기 때문에 소모 시간이 크게 단축되었다.
    GIL의 사고
    그렇다면 이 GIL 자물쇠가 존재한다는 것은 우리의 코드가 안전하다는 것을 의미하는 것입니까?
    안타깝게도 GIL은 루비 실행 중 어떤 작업점이 있을 때 다른 작업 라인으로 전환됩니다. 어떤 변수를 공유할 때 구덩이를 밟을 수 있습니다.
    그러면 GIL은 루비 코드의 실행 중 언제 다른 라인으로 전환되어 일을 합니까?
    몇 가지 명확한 업무 지점이 있다.
  • 방법의 호출과 방법의 반환은 이 두 곳에서 현재 라인의gil의 자물쇠가 시간을 초과했는지, 다른 라인으로 스케줄링을 해야 하는지 검사합니다
  • 모든io와 관련된 조작은gil의 자물쇠를 방출하여 다른 라인이 작동하도록 합니다
  • c 확장 코드에서gil의 자물쇠를 수동으로 방출합니다
  • 또 하나 이해하기 어려운 것은 루비스택이 c스택에 들어갈 때gil의 검측을 촉발한다는 것이다
  • 하나의 예
    
    @a = 1
    r = []
    10.times do |e|
    
    Thread.new {
     @c = 1
     @c += @a
     r << [e, @c]
    }
    end
    r
    ## [[3, 2], [1, 2], [2, 2], [0, 2], [5, 2], [6, 2], [7, 2], [8, 2], [9, 2], [4, 2]]
    상술한 r에서 e의 전후 순서는 다르지만 @c의 값은 시종 2로 유지됩니다. 즉, 모든 라인이 현재의 @c의 값을 잘 보존할 수 있습니다.간단한 스케줄링이 없습니다.
    위의 코드 라인에 추가하면 GIL을 터치할 수 있습니다. 예를 들어puts를 화면에 출력합니다.
    
    @a = 1
    r = []
    10.times do |e|
    
    Thread.new {
     @c = 1
     puts @c
     @c += @a
     r << [e, @c]
    }
    end
    r
    ## [[2, 2], [0, 2], [4, 3], [5, 4], [7, 5], [9, 6], [1, 7], [3, 8], [6, 9], [8, 10]]
    이것은 GIL의 lock을 촉발합니다. 데이터가 이상합니다.
    작은 매듭
    웹 응용 프로그램은 대부분 IO 밀집형이므로 루비 다중 프로세스 + 다중 스레드 모델을 이용하여 시스템의 토출량을 대폭 향상시킬 수 있다.그 이유는 루비의 어떤 스레드가 IO Block 상태에 있을 때 다른 스레드가 계속 실행되어 IO Block이 전체에 미치는 영향을 낮출 수 있기 때문이다.그러나 루비 GIL(Global Interpreter Lock)이 존재하기 때문에 MRI 루비는 다중 스레드를 이용하여 병렬 계산을 할 수 없다.
    PS. JRuby는 GIL을 제거한 진정한 의미의 다중 스레드로 IO 블록에 대처할 수 있을 뿐만 아니라 멀티 코어 CPU를 충분히 이용하여 전체 연산 속도를 가속화할 수 있다고 합니다.
    총결산
    이상은 바로 이 글의 전체 내용입니다. 본고의 내용이 여러분의 학습이나 업무에 대해 참고 학습 가치가 있기를 바랍니다. 의문이 있으면 댓글로 교류해 주십시오. 저희에 대한 지지에 감사드립니다.

    좋은 웹페이지 즐겨찾기