Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ThreadLocalVar #22

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:/jdantonio/concurrent-ruby/blob/master/md/dataflow.md) loosely based on the syntax of Akka and Habanero Java
* [MVar](https:/jdantonio/concurrent-ruby/blob/master/md/mvar.md) inspired by Haskell
* [Thread local variables](https:/jdantonio/concurrent-ruby/blob/master/md/threadlocalvar.md) with default value

### Semantic Versioning

Expand Down
1 change: 1 addition & 0 deletions lib/concurrent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
require 'concurrent/scheduled_task'
require 'concurrent/stoppable'
require 'concurrent/supervisor'
require 'concurrent/threadlocalvar'
require 'concurrent/timer_task'
require 'concurrent/utilities'

Expand Down
115 changes: 115 additions & 0 deletions lib/concurrent/threadlocalvar.rb
Original file line number Diff line number Diff line change
@@ -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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we should allocate this from a global counter instead? Really make sure it is unique.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use SecureRandom.uuid? Would that provide sufficient uniqueness? http://www.ruby-doc.org/stdlib-2.1.1/libdoc/securerandom/rdoc/SecureRandom.html#method-c-uuid

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... that's still just probabilistic. So all we could say to users is this code will probably work. I'll do it properly and use a counter. People probably aren't creating these in an inner loop anyway.

I'm more worried about the space leak.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if it could be a good idea, but what about an "upside down" implementation?
At the moment threadlocals are implemented in MRI using thread storage, but we can reverse it keeping an internal map with threads as key.

This we'll avoid every memory leak or uniqueness; I've just launched a very basic benchmark (Thread.current.thread_variable vs a map guarded by a lock) and this option seems only slightly slower with respect to the current one.

If you think this is a good idea to explore, we could try to build better benchmark

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should take a look at how MRI implements Thread.current and thread locals internally. It's likely that's not using any shared hash at all, so if we have our own global hash with a local around, surely that won't scale very well.

I'll commit using this approach since with a global counter it will work, and the counter shouldn't really be a problem unless we're allocating ThreadLocalVar in an inner loop, and then we can go back and look at it empirically. It's not much code to change if we find a better approach later.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll be honest, I don't know enough about the MRI internals to have any idea how that's implemented. I'm curious so I'm going to take a look and see if I can figure it out. I trust your judgement on this.

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
33 changes: 33 additions & 0 deletions md/threadlocalvar.md
Original file line number Diff line number Diff line change
@@ -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.
97 changes: 97 additions & 0 deletions spec/concurrent/threadlocalvar_spec.rb
Original file line number Diff line number Diff line change
@@ -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