>  기사  >  백엔드 개발  >  Ruby의 동시성, 병렬성 및 전역 잠금 코드 공유에 대한 자세한 설명

Ruby의 동시성, 병렬성 및 전역 잠금 코드 공유에 대한 자세한 설명

巴扎黑
巴扎黑원래의
2017-09-13 09:44:481641검색

최근 Ruby를 배우고 있는데, 배운 내용을 요약해서 공유하고 싶습니다. 다음 글에서는 주로 Ruby의 동시성 및 전역 잠금에 대한 관련 정보를 예제 코드를 통해 자세히 소개합니다. 참고할 수 있으니 아래를 살펴보자.

머리말

이 글은 루비 동시성과 전역 잠금에 대한 관련 내용을 주로 소개하고 참고 및 학습을 위해 공유합니다. 아래에서는 많은 말을 하지 않겠습니다. 자세한 소개.

동시성과 병렬성

우리는 종종 동시성과 병렬성이라는 두 가지 개념을 접하게 됩니다. 동시성과 병렬성을 언급하는 거의 모든 기사에서는 동시성이 병렬성과 동일하지 않다는 사실을 언급합니다. 이 문장을 어떻게 이해해야 할까요?

  • 동시성: 셰프는 손님 2명이 주문한 메뉴를 동시에 받아 처리해야 합니다.

  • 순차 실행: 셰프가 한 명만 있으면 그 사람만 갈 수 있습니다. 하나의 메뉴가 완성되었습니다.

  • 병렬 실행: 두 명의 셰프가 있으면 동시에 요리할 수 있고 두 사람이 함께 요리할 수 있습니다.

이 예를 웹 개발로 확장하면 다음과 같이 이해할 수 있습니다.

  • 동시성: 서버는 두 클라이언트가 시작한 요청을 동시에 받았습니다.

  • 순차 실행: 서버에는 요청을 처리할 프로세스(스레드)가 하나만 있고 두 번째 요청은 첫 번째 요청까지 완료할 수 없습니다. 요청이 완료되었으므로 두 번째 요청은 기다려야 합니다.

  • 병렬 실행: 서버에는 요청을 처리하는 두 개의 프로세스(스레드)가 있으며 두 요청 모두 순서 문제 없이 응답할 수 있습니다.

에 따르면 위에서 설명한 예는 Ruby에서 이러한 동시 동작을 시뮬레이션하는 방법은 무엇입니까? 다음 코드를 살펴보십시오.

1. 순차 실행:

스레드가 하나만 있는 경우 작업을 시뮬레이션합니다.

require 'benchmark'

def f1
 puts "sleep 3 seconds in f1\n"
 sleep 3
end

def f2
 puts "sleep 2 seconds in f2\n"
 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)

위 코드는 매우 간단합니다. 시간이 많이 소요되는 작업을 시뮬레이션합니다.


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 시간 소모가 비슷하다는 점은 예상한 대로 멀티스레딩을 사용하면 병렬화를 달성할 수 있다는 것입니다.


Ruby의 멀티스레딩은 스레드가 IO 블록에 있을 때 대처할 수 있습니다. IO 블록 상태에서는 다른 스레드가 계속 실행될 수 있으므로 전체 처리 시간이 크게 단축됩니다.


Threads in Ruby

위 코드 예제에서는 Ruby에서 Thread의 스레드 클래스를 사용합니다. Thread 클래스의 다중 스레드 프로그램을 작성하세요. Ruby 스레드는 코드에서 병렬 처리를 달성하는 확장 가능하고 효과적인 방법입니다. 다음으로 동시성 시나리오를 설명하겠습니다.

 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

스레드 생성은 비차단입니다. 이렇게 하면 동시 동작을 시뮬레이션할 수 있습니다. 차단하는 경우 여러 스레드가 병렬성을 달성할 수 있습니다.

죄송합니다. 위 설명에서는 비차단 상황에서 병렬성을 시뮬레이션할 수 있다고만 언급했습니다. 다른 예를 살펴보겠습니다.


require &#39;benchmark&#39;
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)

여기에서 볼 수 있듯이 동일한 작업을 4개의 스레드가 병렬로 실행되는데 시간이 줄어들지 않는 이유는 무엇입니까?

글로벌락(GIL)이 존재하기 때문이죠! ! !

전역 잠금



우리가 일반적으로 사용하는 루비는 GIL이라는 메커니즘을 사용합니다.

코드 병렬성을 달성하기 위해 멀티스레딩을 사용하더라도 이 전역 잠금이 있기 때문에 모든 time 하나의 스레드만 코드를 실행할 수 있으며, 실행할 수 있는 스레드는 기본 운영 체제의 구현에 따라 다릅니다.

CPU가 여러 개 있더라도 각 스레드 실행에 대해 몇 가지 추가 옵션만 제공합니다. 위 코드에서는 한 번에 하나의 스레드만 count += 1을 실행할 수 있습니다.
Ruby 멀티스레딩은 멀티코어 CPU를 재사용할 수 없습니다. 반대로 전체 시간은 단축되지 않습니다. 스레드 전환의 영향으로 인해 소요 시간도 약간 증가할 수 있습니다.

하지만 전에 자고 나니 분명히 평행성이 달성됐네요!

이것은 Ruby의 고급 설계입니다. 파일 읽기 및 쓰기, 네트워크 요청을 포함한 모든 차단 작업을 병렬화할 수 있습니다.

require &#39;benchmark&#39;
require &#39;net/http&#39;

# 模拟网络请求
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)

프로그램은 네트워크 요청 차단 중에 발생하며 이러한 차단은 다음에서 병렬로 실행될 수 있습니다. Ruby이므로 시간이 많이 단축됩니다.

GIL에 대한 생각


이 GIL 잠금이 존재한다는 것은 우리 코드가 스레드로부터 안전하다는 뜻인가요?

안타깝게도 GIL은 그렇지 않습니다. Ruby 실행 중에 특정 작업 지점에서 다른 작업 스레드로 전환됩니다. 일부 클래스 변수가 공유되면 함정이 있을 수 있습니다.

那么, GIL 在 ruby代码的执行中什么时候会切换到另外一个线程去工作呢?

有几个明确的工作点:

  • 方法的调用和方法的返回, 在这两个地方都会检查一下当前线程的gil的锁是否超时,是否要调度到另外线程去工作

  • 所有io相关的操作, 也会释放gil的锁让其它线程来工作

  • 在c扩展的代码中手动释放gil的锁

  • 还有一个比较难理解, 就是ruby stack 进入 c stack的时候也会触发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, 数据异常了.

小结

Web 应用大多是 IO 密集型的,利用 Ruby 多进程+多线程模型将能大幅提升系统吞吐量.其原因在于:当Ruby 某个线程处于 IO Block 状态时,其它的线程还可以继续执行,从而降低 IO Block 对整体的影响.但由于存在 Ruby GIL (Global Interpreter Lock),MRI Ruby 并不能真正利用多线程进行并行计算.

PS. 据说 JRuby 去除了GIL,是真正意义的多线程,既能应付 IO Block,也能充分利用多核 CPU 加快整体运算速度,有计划了解一些.

위 내용은 Ruby의 동시성, 병렬성 및 전역 잠금 코드 공유에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.