Skip to content

Instantly share code, notes, and snippets.

@mboeh
Created August 7, 2012 18:09
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mboeh/3287930 to your computer and use it in GitHub Desktop.
Save mboeh/3287930 to your computer and use it in GitHub Desktop.
thread and GC nuance
# Summary: In MRI 1.9, if you keep threads in an array in a local variable, the threads may outlive the local variable.
# I have not yet been able to produce a scenario in which the threads are ever garbage-collected.
# You have to remove the thread from the array before it goes out of scope, e.g. using #pop or #clear.
# INPUT DESIRED: If anyone can demonstrate a condition in which a local array keeps threads and those threads are eventually GC'd.
# INPUT DESIRED: Is this expected behavior or a bug?
# NOTE: This test will not work as expected in JRuby, because its garbage collection works differently. Whether the same behavior exists in JRuby is an exercise for someone smarter than I am.
class Foo
THREAD_PROC = lambda{ (0..10).to_a.map do Foo.new end }
def self.gc_bench(label)
before = ObjectSpace.each_object(Thread).count
yield
after = ObjectSpace.each_object(Thread).count
GC.start
gcd = ObjectSpace.each_object(Thread).count
puts "#{label}: #{before} -> #{after} -> #{gcd} (#{gcd - after})"
end
def self.do_it_one_local
gc_bench "local array" do
threads = 10.times.map do
Thread.new &THREAD_PROC
end
threads.each(&:join)
threads = nil
end
end
def self.do_it_one_local_clear
gc_bench "local array, use #clear" do
threads = 10.times.map do
Thread.new &THREAD_PROC
end
threads.each(&:join)
threads.clear
end
end
def self.do_it_one_local_pops
gc_bench "local array, use many #pops" do
threads = 10.times.map do
Thread.new &THREAD_PROC
end
threads.each(&:join)
:spin while threads.pop
end
end
def self.do_it_many_locals
gc_bench "many locals" do
thread0 = Thread.new &THREAD_PROC
thread1 = Thread.new &THREAD_PROC
thread2 = Thread.new &THREAD_PROC
thread3 = Thread.new &THREAD_PROC
thread4 = Thread.new &THREAD_PROC
thread5 = Thread.new &THREAD_PROC
thread6 = Thread.new &THREAD_PROC
thread7 = Thread.new &THREAD_PROC
thread8 = Thread.new &THREAD_PROC
thread9 = Thread.new &THREAD_PROC
(0..9).each do |i|
eval "thread#{i}.join"
end
end
end
end
Foo.do_it_one_local # Leaks 10 threads
Foo.do_it_many_locals # Leaks no threads
Foo.do_it_one_local_clear # Leaks no threads
Foo.do_it_one_local_pops # Leaks no threads
@mboeh
Copy link
Author

mboeh commented Aug 7, 2012

Interesting note:


[mboeh@orz:~]#ruby-1.8.7-p370  % ruby gogo.rb
local array: 1 -> 11 -> 1 (-10)
many locals: 1 -> 11 -> 11 (0)
local array, use #clear: 11 -> 21 -> 1 (-20)
local array, use many #pops: 1 -> 11 -> 1 (-10)

Ruby 1.8.7 behaves correctly for the array but doesn't manage to clean up the many locals test until the next test.

@mboeh
Copy link
Author

mboeh commented Aug 7, 2012

1.9.3, wrapping do_it_one_local in 10.times do ... end:

local array: 1 -> 11 -> 11 (0)
local array: 11 -> 21 -> 11 (-10)
local array: 11 -> 21 -> 11 (-10)
local array: 11 -> 21 -> 11 (-10)
local array: 11 -> 21 -> 11 (-10)
local array: 11 -> 21 -> 11 (-10)
local array: 11 -> 21 -> 11 (-10)
local array: 11 -> 21 -> 11 (-10)
local array: 11 -> 21 -> 11 (-10)
local array: 11 -> 21 -> 11 (-10)
many locals: 11 -> 21 -> 11 (-10)
local array, use #clear: 11 -> 21 -> 11 (-10)
local array, use many #pops: 11 -> 21 -> 11 (-10)

???

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment