Home >Backend Development >Python Tutorial >Detailed explanation of concurrency, parallelism and global lock code sharing in ruby
I am learning ruby recently and want to summarize and share what I have learned. The following article mainly introduces you to the relevant information about concurrency and global locks in ruby. The article introduces it in detail through example code. Friends in need can refer to it, let’s take a look below.
Preface
This article mainly introduces the relevant content about ruby concurrency and global locks, and shares it for your reference and study. Below Not much to say, let’s take a look at the detailed introduction.
Concurrency and parallelism
When developing, we often come into contact with two concepts: concurrency and parallelism, almost all talk about Articles about concurrency and parallelism will mention one thing: Concurrency does not equal parallelism. So how to understand this sentence?
Concurrency: The chef received the menus ordered by 2 guests at the same time. Need to be processed.
Sequential execution: If there is only one chef, then he can only complete one menu after another.
Parallel execution : If there are two chefs, then they can cook in parallel and two people cook together.
Extend this example to our web development, you can understand it like this:
Concurrency: The server received requests initiated by two clients at the same time.
Sequential execution: The server has only one process (thread) to process the request and completes the first One request can complete the second request, so the second request needs to wait.
Parallel execution: The server has two processes (threads) to process the request, and both requests can Get a response without any issue of sequence.
According to the example described above, how can we simulate such a concurrent behavior in ruby? Look at the following code:
1. Sequential execution:
Simulate the operation when there is only one thread.
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)
The above code is very simple. Use sleep to simulate time-consuming operations. The time consumed during sequential execution.
2. Parallel execution
Simulate multiple Operations in threads
# 接上述代码 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)
We found that the time consuming under multi-threading is similar to that of f1, which is as expected. Parallelism can be achieved using multi-threading. .
Ruby's multi-threading can cope with IO Block. When a thread is in the IO Block state, other threads can continue to execute, thus greatly shortening the overall processing time.
Threads in Ruby
#The above code example uses the Thread class in Ruby. Ruby can easily write many Thread classes. Thread program. Ruby threads are a lightweight and effective way to achieve parallelism in your code.
The following describes a concurrency scenario
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
The creation of Thread is non-blocking, so text can be output immediately. This simulates a concurrent behavior. Each thread sleeps for 3 seconds. In the case of blocking, multiple threads can achieve parallelism .
So at this time have we completed the parallel capability?
Unfortunately, my above description only mentioned that we are in non- Parallelism can be simulated in the case of blocking. Let's look at other examples:
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)
As can be seen from here, even if we divide the same task into 4 The threads are parallel, but the time is not reduced. Why is this?
Because there is a global lock (GIL)! ! !
Global lock
The ruby we usually use uses a mechanism called GIL.
Even if we want to use multiple threads to achieve code parallelism, due to the existence of this global lock, only one thread can execute the code at a time. As for which thread can execute, this depends on the implementation of the underlying operating system.
Even if we have multiple CPUs, it only provides a few more options for the execution of each thread.
In our above code, only one thread can execute count += 1 at a time.
Ruby multi-threading cannot reuse multi-core CPUs. The overall time spent is not shortened after using multi-threading. On the contrary, due to the impact of thread switching, the time spent may increase slightly.
But when we slept before, we obviously achieved parallelism!
This is the advanced design of Ruby - all blocking operations can be parallelized, including reading and writing files, and network requests.
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)
The program is blocked during network requests, and these blockings can be parallelized when running in Ruby, so the time consumption is greatly shortened.
Thoughts on GIL
So, since the existence of this GIL lock, does it mean that our code is thread-safe?
Unfortunately, no, GIL will switch to another worker thread at certain work points during Ruby execution. If some class variables are shared, it may cause trouble.
那么, 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 加快整体运算速度,有计划了解一些.
The above is the detailed content of Detailed explanation of concurrency, parallelism and global lock code sharing in ruby. For more information, please follow other related articles on the PHP Chinese website!