Ractor to solve the problem of GVL
GIL/GVL
What is the Global VM Lock (GVL)?
- Also known as the Global Interpreter Lock (GIL)
- A mutex that prevents multiple native threads from executing Ruby code in parallel
- Present in CRuby (the reference implementation of Ruby)
Purpose of the GVL
- Protects internal data structures from race conditions
- Ensures thread safety in the Ruby interpreter
- Simplifies the implementation of C extensions
How the GVL works
- Only one thread can execute Ruby code at a time
- Threads take turns acquiring the GVL
- The GVL is released during I/O operations or sleeping
Impact on Concurrency
- Limits true parallelism in multi-threaded Ruby programs
- CPU-bound tasks cannot run in parallel
- I/O-bound tasks can still benefit from concurrency
GVL and Performance
- Single-threaded performance is not affected
- Can limit performance in multi-core systems
- Encourages use of process-based parallelism (e.g., forking)
Working around the GVL
- Use of native extensions for CPU-intensive tasks
- Leveraging I/O concurrency
- Utilizing multiple processes instead of threads
- Exploring alternative Ruby implementations (e.g., JRuby, which doesn’t have a GVL)
Recent Developments
- Gradual improvements in GVL management in newer Ruby versions
- Introduction of Ractor in Ruby 3.0 for parallelism without GVL restrictions
- Ongoing research and development to improve concurrency in Ruby
Thread
- Definition: Lightweight unit of execution within a process
- Pros: Shared memory, relatively low overhead
- Cons: Limited by GVL, potential race conditions
- Use case: I/O-bound tasks, background jobs
Process
- Definition: Independent unit of execution with its own memory space
- Pros: True parallelism, isolation
- Cons: Higher memory usage, slower inter-process communication
- Use case: CPU-bound tasks, improving fault tolerance
Fiber
- Definition: Cooperative, lightweight concurrency primitive
- Pros: Very low overhead, explicit scheduling
- Cons: No parallelism, requires careful management
- Use case: Concurrency within a single thread, implementing coroutines
Ractor (Ruby 3.0+)
- Definition: Actor-model concurrency with memory isolation
- Pros: True parallelism, reduced risk of race conditions
- Cons: Limited sharing between Ractors, experimental feature
- Use case: Parallel execution of independent tasks
Comparison Chart
| Feature | Thread | Process | Fiber | Ractor | |—————|——–|———|——-|——–| | Parallelism | Limited| Yes | No | Yes | | Memory Shared | Yes | No | Yes | Limited| | Overhead | Low | High | Very Low | Medium | | Communication | Easy | Complex | Simple| Controlled |
Memory Model
- Thread: Shared memory
- Process: Separate memory spaces
- Fiber: Shared memory within a thread
- Ractor: Isolated memory with controlled sharing
Concurrency vs Parallelism
- Thread: Concurrent, limited parallelism (GVL)
- Process: Parallel
- Fiber: Concurrent, no parallelism
- Ractor: Parallel with isolated state
Use Case Scenarios
- Thread: Web servers, background jobs
- Process: Heavy computations, system commands
- Fiber: Event-driven programming, generators
- Ractor: Parallel data processing, actor-model implementations
Code Complexity
- Thread: Moderate (need to handle synchronization)
- Process: Simple (but IPC can be complex)
- Fiber: Can be complex (manual scheduling)
- Ractor: Moderate (new concept, restricted object sharing)
Exmaples: Thread VS Ractor
- Fib(37)
- Run 6 times of the calculation
Fib Process
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
require "gvl-tracing"
def fib(n)
return n if n <= 1
fib(n - 1) + fib(n - 2)
end
NR_CORES = 6
def calc
result = []
pipes = []
pids = []
NR_CORES.times do |i|
pid = fork do
fib(37)
end
pids << pid
end
pids.each do |pid|
Process.waitpid(pid)
end
end
GvlTracing.start("fib_process.json") do
calc
end
1
2
time ruby fab_process.rb
ruby fab_process.rb 15.92s user 0.12s system 508% cpu 3.157 total
Fib Thread
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
require "gvl-tracing"
def fib(n)
return n if n <= 1
fib(n - 1) + fib(n - 2)
end
GvlTracing.start("fib_thread.json") do
Thread.new { sleep(0.05) while true }
sleep(0.05)
6.times.map { Thread.new { fib(37) } }.map(&:join)
sleep(0.05)
end
1
2
time ruby fab_thread.rb
ruby fab_thread.rb 12.59s user 0.07s system 95% cpu 13.238 total
Fab ractor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
require 'gvl-tracing'
def fib n
if n < 2
1
else
fib(n-2) + fib(n-1)
end
end
RN = 6
def ractor
rs = (1..RN).map do |i|
Ractor.new i do |i|
[i, fib(37)]
end
end
until rs.empty?
r, v = Ractor.select(*rs)
rs.delete r
#p answer: v
end
end
GvlTracing.start("fib_ractor.json") do
ractor
end
1
2
3
time ruby fab_ractor.rb
fab_ractor.rb:14: warning: Ractor is experimental, and the behavior may change in future versions of Ruby! Also there are many implementation issues.
ruby fab_ractor.rb 23.01s user 0.15s system 515% cpu 4.492 total
Conclusion
- GVL is a crucial part of CRuby’s architecture
- Understanding its implications is important for optimizing Ruby applications
- Future Ruby versions may bring more improvements in parallel execution
Reference
- https://blog.heroku.com/concurrency_is_not_parallelism
- https://en.wikipedia.org/wiki/Fibonacci_sequence
- https://docs.ruby-lang.org/en/3.3/ractor_md.html
- https://ui.perfetto.dev/
- https://github.com/ivoanjo/gvl-tracing
This post is licensed under CC BY 4.0 by the author.