Skip to content

Commit

Permalink
Merge pull request #1682 from herwinw/thread_variables
Browse files Browse the repository at this point in the history
Add Thread local variables
  • Loading branch information
seven1m committed Jan 9, 2024
2 parents d6a726e + 116ed9c commit 1dded0d
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 0 deletions.
6 changes: 6 additions & 0 deletions include/natalie/thread_object.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ class ThreadObject : public Object {
Value ref(Env *env, Value key);
Value refeq(Env *env, Value key, Value value);

bool has_thread_variable(Env *, Value) const;
Value thread_variable_get(Env *, Value);
Value thread_variable_set(Env *, Value, Value);
Value thread_variables(Env *) const;

void set_sleeping(bool sleeping) { m_sleeping = sleeping; }
bool is_sleeping() const { return m_sleeping; }

Expand Down Expand Up @@ -239,6 +244,7 @@ class ThreadObject : public Object {
std::thread m_thread {};
std::atomic<ExceptionObject *> m_exception { nullptr };
Value m_value { nullptr };
HashObject *m_thread_variables { nullptr };
FiberObject *m_main_fiber { nullptr };
FiberObject *m_current_fiber { nullptr };
#ifdef __SANITIZE_ADDRESS__
Expand Down
4 changes: 4 additions & 0 deletions lib/natalie/compiler/binding_gen.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1344,6 +1344,10 @@ def generate_name
gen.binding('Thread', 'run', 'ThreadObject', 'run', argc: 0, pass_env: true, pass_block: false, return_type: :Object)
gen.binding('Thread', 'status', 'ThreadObject', 'status', argc: 0, pass_env: true, pass_block: false, return_type: :Object)
gen.binding('Thread', 'stop?', 'ThreadObject', 'is_stopped', argc: 0, pass_env: false, pass_block: false, return_type: :bool)
gen.binding('Thread', 'thread_variable?', 'ThreadObject', 'has_thread_variable', argc: 1, pass_env: true, pass_block: false, return_type: :bool)
gen.binding('Thread', 'thread_variable_get', 'ThreadObject', 'thread_variable_get', argc: 1, pass_env: true, pass_block: false, return_type: :Object)
gen.binding('Thread', 'thread_variable_set', 'ThreadObject', 'thread_variable_set', argc: 2, pass_env: true, pass_block: false, return_type: :Object)
gen.binding('Thread', 'thread_variables', 'ThreadObject', 'thread_variables', argc: 0, pass_env: true, pass_block: false, return_type: :Object)
gen.binding('Thread', 'to_s', 'ThreadObject', 'to_s', argc: 0, pass_env: true, pass_block: false, aliases: ['inspect'], return_type: :Object)
gen.binding('Thread', 'value', 'ThreadObject', 'value', argc: 0, pass_env: true, pass_block: false, return_type: :Object)
gen.binding('Thread', 'wakeup', 'ThreadObject', 'wakeup', argc: 0, pass_env: true, pass_block: false, return_type: :Object)
Expand Down
25 changes: 25 additions & 0 deletions spec/core/thread/thread_variable_get_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
require_relative '../../spec_helper'

describe "Thread#thread_variable_get" do
before :each do
@t = Thread.new { }
end

after :each do
@t.join
end

it "returns nil if the variable is not set" do
@t.thread_variable_get(:a).should be_nil
end

it "returns the value previously set by #thread_variable_set" do
@t.thread_variable_set :a, 49
@t.thread_variable_get(:a).should == 49
end

it "returns a value private to self" do
@t.thread_variable_set :thread_variable_get_spec, 82
Thread.current.thread_variable_get(:thread_variable_get_spec).should be_nil
end
end
26 changes: 26 additions & 0 deletions spec/core/thread/thread_variable_set_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
require_relative '../../spec_helper'

describe "Thread#thread_variable_set" do
before :each do
@t = Thread.new { }
end

after :each do
@t.join
end

it "returns the value set" do
(@t.thread_variable_set :a, 2).should == 2
end

it "sets a value that will be returned by #thread_variable_get" do
@t.thread_variable_set :a, 49
@t.thread_variable_get(:a).should == 49
end

it "sets a value private to self" do
@t.thread_variable_set :thread_variable_get_spec, 82
@t.thread_variable_get(:thread_variable_get_spec).should == 82
Thread.current.thread_variable_get(:thread_variable_get_spec).should be_nil
end
end
21 changes: 21 additions & 0 deletions spec/core/thread/thread_variable_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
require_relative '../../spec_helper'

describe "Thread#thread_variable?" do
before :each do
@t = Thread.new { }
end

after :each do
@t.join
end

it "returns false if the thread variables do not contain 'key'" do
@t.thread_variable_set :a, 2
@t.thread_variable?(:b).should be_false
end

it "returns true if the thread variables contain 'key'" do
@t.thread_variable_set :a, 2
@t.thread_variable?(:a).should be_true
end
end
29 changes: 29 additions & 0 deletions spec/core/thread/thread_variables_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
require_relative '../../spec_helper'

describe "Thread#thread_variables" do
before :each do
@t = Thread.new { }
end

after :each do
@t.join
end

it "returns the keys of all the values set" do
@t.thread_variable_set :a, 2
@t.thread_variable_set :b, 4
@t.thread_variable_set :c, 6
@t.thread_variables.sort.should == [:a, :b, :c]
end

it "sets a value private to self" do
@t.thread_variable_set :a, 82
@t.thread_variable_set :b, 82
Thread.current.thread_variables.should_not include(:a, :b)
end

it "only contains user thread variables and is empty initially" do
Thread.current.thread_variables.should == []
@t.thread_variables.should == []
end
end
39 changes: 39 additions & 0 deletions src/thread_object.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,44 @@ Value ThreadObject::refeq(Env *env, Value key, Value value) {
return value;
}

bool ThreadObject::has_thread_variable(Env *env, Value key) const {
if (!key->is_symbol() && !key->is_string() && key->respond_to(env, "to_str"_s))
key = key->to_str(env);
if (key->is_string())
key = key->as_string()->to_sym(env);
return m_thread_variables && m_thread_variables->has_key(env, key);
}

Value ThreadObject::thread_variable_get(Env *env, Value key) {
if (!m_thread_variables)
return NilObject::the();
if (!key->is_symbol() && !key->is_string() && key->respond_to(env, "to_str"_s))
key = key->to_str(env);
if (key->is_string())
key = key->as_string()->to_sym(env);
return m_thread_variables->ref(env, key);
}

Value ThreadObject::thread_variable_set(Env *env, Value key, Value value) {
if (is_frozen())
env->raise("FrozenError", "can't modify frozen thread locals");
if (!key->is_symbol() && !key->is_string() && key->respond_to(env, "to_str"_s))
key = key->to_str(env);
if (key->is_string())
key = key->as_string()->to_sym(env);
if (!key->is_symbol())
env->raise("TypeError", "{} is not a symbol", key->inspect_str(env));
if (!m_thread_variables)
m_thread_variables = new HashObject;
return m_thread_variables->refeq(env, key, value);
}

Value ThreadObject::thread_variables(Env *env) const {
if (!m_thread_variables)
return new ArrayObject;
return m_thread_variables->keys(env);
}

Value ThreadObject::list(Env *env) {
std::lock_guard<std::mutex> lock(g_thread_mutex);
auto ary = new ArrayObject { s_list.size() };
Expand Down Expand Up @@ -480,6 +518,7 @@ void ThreadObject::visit_children(Visitor &visitor) {
visitor.visit(m_exception);
visitor.visit(m_main_fiber);
visitor.visit(m_value);
visitor.visit(m_thread_variables);
for (auto pair : m_mutexes)
visitor.visit(pair.first);
visitor.visit(m_fiber_scheduler);
Expand Down
101 changes: 101 additions & 0 deletions test/natalie/thread_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,105 @@ def create_threads
t1.report_on_exception.should == false
end
end

describe 'Thread#thread_variable?' do
before :each do
@t = Thread.new { }
end

after :each do
@t.join
end

it 'converts String keys to Symbol' do
@t.thread_variable_set(:a, 42)
@t.should.thread_variable?('a')
end

it 'tries to convert the key that is neither String nor Symbol with #to_str' do
key = mock('key')
key.should_receive(:to_str).and_return('a')
@t.thread_variable_set(:a, 42)
@t.should.thread_variable?(key)
end

it 'does not raise a TypeError if the key is not a Symbol' do
@t.should_not.thread_variable?(123)
end

it 'does not try to convert the key with #to_sym' do
key = mock('key')
key.should_not_receive(:to_sym)
@t.should_not.thread_variable?(key)
end
end

describe 'Thread#thread_variable_get' do
before :each do
@t = Thread.new { }
end

after :each do
@t.join
end

it 'converts String keys to Symbol' do
@t.thread_variable_set(:a, 42)
@t.thread_variable_get('a').should == 42
end

it 'tries to convert the key that is neither String nor Symbol with #to_str' do
key = mock('key')
key.should_receive(:to_str).and_return('a')
@t.thread_variable_set(:a, 42)
@t.thread_variable_get(key).should == 42
end

it 'does not raise a TypeError if the key is not a Symbol' do
@t.thread_variable_get(123).should be_nil
end

it 'does not try to convert the key with #to_sym' do
key = mock('key')
key.should_not_receive(:to_sym)
@t.thread_variable_get(key).should be_nil
end
end

describe 'Thread#thread_variable_set' do
before :each do
@t = Thread.new { }
end

after :each do
@t.join
end

it 'converts String keys to Symbol' do
@t.thread_variable_set('a', 42)
@t.thread_variable_get(:a).should == 42
end

it 'tries to convert the key that is neither String nor Symbol with #to_str' do
key = mock('key')
key.should_receive(:to_str).and_return('a')
@t.thread_variable_set(key, 42)
@t.thread_variable_get(:a).should == 42
end

it 'raises a FrozenError if the thread is frozen' do
@t.freeze
-> { @t.thread_variable_set(:a, 1) }.should raise_error(FrozenError, "can't modify frozen thread locals")
end

it 'raises a TypeError if the key is not a Symbol' do
-> { @t.thread_variable_set(123, 1) }.should raise_error(TypeError, '123 is not a symbol')
end

it 'does not try to convert the key with #to_sym' do
key = mock('key')
key.should_not_receive(:to_sym)
-> { @t.thread_variable_set(key, 42) }.should raise_error(TypeError, "#{key.inspect} is not a symbol")
end
end
end

0 comments on commit 1dded0d

Please sign in to comment.