diff --git a/README.md b/README.md index b57dd9392..5774f8d50 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ The design goals of this gem are: agent in [F#](http://msdn.microsoft.com/en-us/library/ee370357.aspx) * [Dataflow](https://github.com/jdantonio/concurrent-ruby/blob/master/md/dataflow.md) loosely based on the syntax of Akka and Habanero Java * [MVar](https://github.com/jdantonio/concurrent-ruby/blob/master/md/mvar.md) inspired by Haskell +* [Thread local variables](https://github.com/jdantonio/concurrent-ruby/blob/master/md/threadlocalvar.md) with default value ### Semantic Versioning diff --git a/lib/concurrent.rb b/lib/concurrent.rb index 14ecbc3fd..352d22d58 100644 --- a/lib/concurrent.rb +++ b/lib/concurrent.rb @@ -23,6 +23,7 @@ require 'concurrent/scheduled_task' require 'concurrent/stoppable' require 'concurrent/supervisor' +require 'concurrent/threadlocalvar' require 'concurrent/timer_task' require 'concurrent/utilities' diff --git a/lib/concurrent/threadlocalvar.rb b/lib/concurrent/threadlocalvar.rb new file mode 100644 index 000000000..65cd3ddc9 --- /dev/null +++ b/lib/concurrent/threadlocalvar.rb @@ -0,0 +1,115 @@ +module Concurrent + + module ThreadLocalSymbolAllocator + + protected + + def allocate_symbol + # Warning: this will space leak if you create ThreadLocalVars in a loop - not sure what to do about it + @symbol = "thread_local_symbol_#{self.object_id}_#{Time.now.hash}".to_sym + end + + end + + module ThreadLocalOldStorage + + include ThreadLocalSymbolAllocator + + protected + + def allocate_storage + allocate_symbol + end + + def get + Thread.current[@symbol] + end + + def set(value) + Thread.current[@symbol] = value + end + + end + + module ThreadLocalNewStorage + + include ThreadLocalSymbolAllocator + + protected + + def allocate_storage + allocate_symbol + end + + def get + Thread.current.thread_variable_get(@symbol) + end + + def set(value) + Thread.current.thread_variable_set(@symbol, value) + end + + end + + module ThreadLocalJavaStorage + + protected + + def allocate_storage + @var = java.lang.ThreadLocal.new + end + + def get + @var.get + end + + def set(value) + @var.set(value) + end + + end + + class ThreadLocalVar + + NIL_SENTINEL = Object.new + + if defined? java.lang.ThreadLocal.new + include ThreadLocalJavaStorage + elsif Thread.current.respond_to?(:thread_variable_set) + include ThreadLocalNewStorage + else + include ThreadLocalOldStorage + end + + def initialize(default = nil) + @default = default + allocate_storage + end + + def value + value = get + + if value.nil? + @default + elsif value == NIL_SENTINEL + nil + else + value + end + end + + def value=(value) + if value.nil? + stored_value = NIL_SENTINEL + else + stored_value = value + end + + set stored_value + + value + end + + end + +end diff --git a/md/threadlocalvar.md b/md/threadlocalvar.md new file mode 100644 index 000000000..62aae5968 --- /dev/null +++ b/md/threadlocalvar.md @@ -0,0 +1,33 @@ +# Thread local variables + +A `ThreadLocalVar` is a variable where the value is different for each thread. +Each variable may have a default value, but when you modify the variable only +the current thread will ever see that change. + +```ruby +v = ThreadLocalVar.new(14) +v.value #=> 14 +v.value = 2 +v.value #=> 2 +``` + +```ruby +v = ThreadLocalVar.new(14) + +t1 = Thread.new do + v.value #=> 14 + v.value = 1 + v.value #=> 1 +end + +t2 = Thread.new do + v.value #=> 14 + v.value = 2 + v.value #=> 2 +end + +v.value #=> 14 +``` + +Note that except on JRuby, `ThreadLocalVar` needs to allocate a unique symbol +for each instance. This may lead to a space leak. diff --git a/spec/concurrent/threadlocalvar_spec.rb b/spec/concurrent/threadlocalvar_spec.rb new file mode 100644 index 000000000..9cbaafa82 --- /dev/null +++ b/spec/concurrent/threadlocalvar_spec.rb @@ -0,0 +1,97 @@ +require 'spec_helper' + +module Concurrent + + describe Future do + + context '#initialize' do + + it 'can set an initial value' do + v = ThreadLocalVar.new(14) + v.value.should eq 14 + end + + it 'sets nil as a default initial value' do + v = ThreadLocalVar.new + v.value.should be_nil + end + + it 'sets the same initial value for all threads' do + v = ThreadLocalVar.new(14) + t1 = Thread.new { v.value } + t2 = Thread.new { v.value } + t1.value.should eq 14 + t2.value.should eq 14 + end + + end + + context '#value' do + + it 'returns the current value' do + v = ThreadLocalVar.new(14) + v.value.should eq 14 + end + + it 'returns the value after modification' do + v = ThreadLocalVar.new(14) + v.value = 2 + v.value.should eq 2 + end + + end + + context '#value=' do + + it 'sets a new value' do + v = ThreadLocalVar.new(14) + v.value = 2 + v.value.should eq 2 + end + + it 'returns the new value' do + v = ThreadLocalVar.new(14) + (v.value = 2).should eq 2 + end + + it 'does not modify the initial value for other threads' do + v = ThreadLocalVar.new(14) + v.value = 2 + t = Thread.new { v.value } + t.value.should eq 14 + end + + it 'does not modify the value for other threads' do + v = ThreadLocalVar.new(14) + v.value = 2 + + b1 = CountDownLatch.new(2) + b2 = CountDownLatch.new(2) + + t1 = Thread.new do + b1.count_down + b1.wait + v.value = 1 + b2.count_down + b2.wait + v.value + end + + t2 = Thread.new do + b1.count_down + b1.wait + v.value = 2 + b2.count_down + b2.wait + v.value + end + + t1.value.should eq 1 + t2.value.should eq 2 + end + + end + + end + +end