Concurrent concurrency and global lock details in ruby, and ruby parallel global explanation
Preface
This article mainly introduces you to ruby concurrency and global locks. We will share the content for your reference and learning. I will not talk much about it below. Let's take a look at the details.
Concurrency and Parallelism
During development, we often come into contact with two concepts: concurrency and parallelism. Almost all articles about concurrency and parallelism will mention that concurrency is not equal to parallelism. so how can we understand this sentence?
- Concurrency: the cook receives the menus ordered by two guests at the same time.
- Sequential execution: if there is only one cook, one menu can be followed by another.
- Parallel Execution: if there are two chefs, they can cook in parallel.
Extend this example to our web development:
- Concurrency: the server simultaneously receives two client-initiated requests.
- Sequential execution: the server only has one process (thread) to process the request. After the first request is completed, the second request can be completed. Therefore, the second request needs to wait.
- Parallel Execution: the server has two processes (threads) to process requests, and both requests can receive responses without any successive problems.
Based on the example described above, how can we simulate such a concurrent behavior in ruby? See the following code:
1. sequential execution:
Simulate operations with only one thread.
require 'benchmark'def f1 puts "sleep 3 seconds in f1\n" sleep 3enddef f2 puts "sleep 2 seconds in f2\n" sleep 2 endBenchmark.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. sleep is used to simulate time-consuming operations. The time consumed during sequential execution.
2. Parallel Execution
Simulate multi-thread operations
# Follow the above Code Benchmark. bm do | B. report do threads = [] threads <Thread. new {f1} threads <Thread. new {f2} threads. each (&: join) 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 consumed in multithreading is similar to that in f1, which is as expected. using multithreading can achieve parallel processing.
Ruby multithreading can cope with IO blocks. When a thread is in the I/O Block State, other threads can continue to execute, greatly shortening the overall processing time.
Threads in Ruby
The preceding code example uses the Thread class in ruby. Ruby can easily write multi-threaded programs of the Thread class. the Ruby thread is a lightweight and effective way to achieve parallelism in your code.
Next, we will describe a concurrency scenario.
Def thread_test time = Time. now threads = 3. times. map do Thread. new do sleep 3 end puts "you don't have to wait 3 seconds to see me: # {Time. now-time} "threads. map (&: join) puts "now it takes 3 seconds to see me: # {Time. now-time} "end test # You don't have to wait 3 seconds to see me: 8.6e-05 # now it takes 3 seconds to see me: 3.003699
Thread creation is non-blocking, so the text can be output immediately. This simulates a concurrent behavior. Each Thread sleep for 3 seconds. In the case of blocking, multithreading can achieve parallel execution.
At this time, have we completed the parallel capability?
Unfortunately, I mentioned in the above description that we can simulate parallelism without blocking. Let's look at another example:
require 'benchmark'def multiple_threads count = 0 threads = 4.times.map do Thread.new do 2500000.times { count += 1} end end threads.map(&:join)enddef single_threads time = Time.now count = 0 Thread.new do 10000000.times { count += 1} end.joinendBenchmark.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)
From this we can see that even if we divide the same task into four threads in parallel, the time is not reduced. Why?
Because of the existence of a global lock (GIL !!!
Global lock
We usually use ruby using a mechanism called GIL.
Even if we want to use multiple threads to implement code parallelism, because of 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, We only provide several choices for the execution of each thread.
In the above Code, only one thread can execute count + = 1.
Ruby multithreading does not allow repeated use of multi-core CPUs. The overall time spent after multithreading is not shortened. However, due to the impact of thread switching, the time spent may increase slightly.
But when we sleep, we achieved parallel execution!
This is the place where Ruby is designed to be advanced-All blocking operations can be performed in parallel, including reading and writing files and network requests.
Require 'benchmark' require 'net/http' # simulate network request def multiple_threads uri = URI ("http://www.baidu.com") threads = 4. times. map do Thread. new do 25. times {Net: HTTP. get (uri)} end threads. map (&: join) enddef single_threads uri = URI ("http://www.baidu.com") Thread. new do 100. times {Net: HTTP. get (uri)} end. joinendBenchmark. bm do | B. report {multiple_threads} B. report {single_threads} end user system total real0.240000 0.110000 0.350000 (3.659640) 0.270000 0.120000 0.390000 (14.167703)
The program is congested during network requests, and these blocking tasks can be parallel in Ruby running, which greatly reduces the time consumption.
GIL thinking
So, since this GIL lock exists, does it mean that our code is thread-safe?
Unfortunately, GIL switches some work points to another working thread during ruby execution. If some class variables are shared, it may be difficult.
So when will GIL switch to another thread to work during ruby code execution?
There are several clear work points:
- The call of the method and the return of the method both check whether the gil lock of the current thread times out and whether to schedule it to another thread to work.
- All io-related operations will also release the gil lock for other threads to work.
- Manually release the gil lock in the c extension code
- It is hard to understand that gil detection is triggered when ruby stack enters c stack.
Example
@a = 1r = []10.times do |e|Thread.new { @c = 1 @c += @a r << [e, @c]}endr## [[3, 2], [1, 2], [2, 2], [0, 2], [5, 2], [6, 2], [7, 2], [8, 2], [9, 2], [4, 2]]
In the preceding example, although the order of e is different in r, the value of @ c is always kept as 2, that is, the value of @ c can be retained for each thread. there is no simple thread scheduling.
If you add a GIL operation to the above Code thread, such as puts printing to the screen:
@a = 1r = []10.times do |e|Thread.new { @c = 1 puts @c @c += @a r << [e, @c]}endr## [[2, 2], [0, 2], [4, 3], [5, 4], [7, 5], [9, 6], [1, 7], [3, 8], [6, 9], [8, 10]]
This will trigger the GIL lock and the data is abnormal.
Summary
Most Web applications are IO-intensive. Using the Ruby multi-process + multi-thread model can greatly increase the system throughput. the reason is: when a Ruby thread is in the IO Block State, other threads can continue to execute, thus reducing the overall impact of IO Block. however, due to the existence of Ruby GIL (Global Interpreter Lock), MRI Ruby does not really use multithreading for parallel computing.
PS. It is said that the removal of GIL by JRuby is a true multi-thread. It can cope with IO blocks and make full use of multi-core CPUs to speed up the overall operation.
Summary
The above is all the content of this article. I hope the content of this article has some reference and learning value for everyone's learning or work. If you have any questions, please leave a message to us, thank you for your support.