mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
Use a proper mutex for autoloading features. (#5788)
Object#autoload implements a custom per-thread "mutex" for blocking threads waiting on autoloading a feature. This causes problems when used with the fiber scheduler. We swap the implementation to use a Ruby mutex which is fiber aware.
This commit is contained in:
parent
679b6e43c7
commit
fd6cef79f5
Notes:
git
2022-05-08 07:23:27 +09:00
Merged-By: ioquatix <samuel@codeotaku.com>
3 changed files with 124 additions and 135 deletions
3
test/fiber/autoload.rb
Normal file
3
test/fiber/autoload.rb
Normal file
|
@ -0,0 +1,3 @@
|
|||
sleep 0.01
|
||||
module TestFiberSchedulerAutoload
|
||||
end
|
|
@ -104,4 +104,23 @@ class TestFiberScheduler < Test::Unit::TestCase
|
|||
|
||||
thread.join
|
||||
end
|
||||
|
||||
def test_autoload
|
||||
Object.autoload(:TestFiberSchedulerAutoload, File.expand_path("autoload.rb", __dir__))
|
||||
|
||||
thread = Thread.new do
|
||||
scheduler = Scheduler.new
|
||||
Fiber.set_scheduler scheduler
|
||||
|
||||
10.times do
|
||||
Fiber.schedule do
|
||||
Object.const_get(:TestFiberSchedulerAutoload)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
thread.join
|
||||
ensure
|
||||
Object.send(:remove_const, :TestFiberSchedulerAutoload)
|
||||
end
|
||||
end
|
||||
|
|
237
variable.c
237
variable.c
|
@ -2118,8 +2118,7 @@ struct autoload_const {
|
|||
struct autoload_state {
|
||||
struct autoload_const *ac;
|
||||
VALUE result;
|
||||
VALUE thread;
|
||||
struct ccan_list_head waitq;
|
||||
VALUE mutex;
|
||||
};
|
||||
|
||||
struct autoload_data_i {
|
||||
|
@ -2348,6 +2347,11 @@ autoload_delete(VALUE mod, ID id)
|
|||
}
|
||||
}
|
||||
|
||||
static int
|
||||
autoload_by_someone_else(struct autoload_data_i *ele) {
|
||||
return ele->state && ele->state->mutex != Qnil && !rb_mutex_owned_p(ele->state->mutex);
|
||||
}
|
||||
|
||||
static VALUE
|
||||
check_autoload_required(VALUE mod, ID id, const char **loadingpath)
|
||||
{
|
||||
|
@ -2357,12 +2361,14 @@ check_autoload_required(VALUE mod, ID id, const char **loadingpath)
|
|||
const char *loading;
|
||||
|
||||
if (!load || !(ele = get_autoload_data(load, 0))) {
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
file = ele->feature;
|
||||
|
||||
Check_Type(file, T_STRING);
|
||||
if (!RSTRING_LEN(file) || !*RSTRING_PTR(file)) {
|
||||
rb_raise(rb_eArgError, "empty file name");
|
||||
rb_raise(rb_eArgError, "empty file name");
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -2371,18 +2377,21 @@ check_autoload_required(VALUE mod, ID id, const char **loadingpath)
|
|||
* completes. We must wait until autoload_const_set finishes in
|
||||
* the other thread.
|
||||
*/
|
||||
if (ele->state && ele->state->thread != rb_thread_current()) {
|
||||
return load;
|
||||
if (autoload_by_someone_else(ele)) {
|
||||
return load;
|
||||
}
|
||||
|
||||
loading = RSTRING_PTR(file);
|
||||
|
||||
if (!rb_feature_provided(loading, &loading)) {
|
||||
return load;
|
||||
return load;
|
||||
}
|
||||
|
||||
if (loadingpath && loading) {
|
||||
*loadingpath = loading;
|
||||
return load;
|
||||
*loadingpath = loading;
|
||||
return load;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -2403,6 +2412,11 @@ rb_autoloading_value(VALUE mod, ID id, VALUE* value, rb_const_flag_t *flag)
|
|||
return TRUE;
|
||||
}
|
||||
|
||||
static int
|
||||
autoload_by_current(struct autoload_data_i *ele) {
|
||||
return ele->state && ele->state->mutex != Qnil && rb_mutex_owned_p(ele->state->mutex);
|
||||
}
|
||||
|
||||
struct autoload_const *
|
||||
autoloading_const_entry(VALUE mod, ID id)
|
||||
{
|
||||
|
@ -2414,11 +2428,12 @@ autoloading_const_entry(VALUE mod, ID id)
|
|||
return 0;
|
||||
}
|
||||
|
||||
if (ele->state && ele->state->thread == rb_thread_current()) {
|
||||
if (ac->value != Qundef) {
|
||||
if (autoload_by_current(ele)) {
|
||||
if (ac->value != Qundef) {
|
||||
return ac;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -2448,7 +2463,7 @@ autoload_const_set(struct autoload_const *ac)
|
|||
}
|
||||
RB_VM_LOCK_LEAVE();
|
||||
|
||||
return 0; /* ignored */
|
||||
return 0; /* ignored */
|
||||
}
|
||||
|
||||
static VALUE
|
||||
|
@ -2460,8 +2475,7 @@ autoload_require(VALUE arg)
|
|||
|
||||
ele = rb_check_typeddata(ac->ad, &autoload_data_i_type);
|
||||
/* this may release GVL and switch threads: */
|
||||
state->result = rb_funcall(rb_vm_top_self(), rb_intern("require"), 1,
|
||||
ele->feature);
|
||||
state->result = rb_funcall(rb_vm_top_self(), rb_intern("require"), 1, ele->feature);
|
||||
|
||||
return state->result;
|
||||
}
|
||||
|
@ -2470,17 +2484,19 @@ static VALUE
|
|||
autoload_reset(VALUE arg)
|
||||
{
|
||||
struct autoload_state *state = (struct autoload_state *)arg;
|
||||
int need_wakeups = 0;
|
||||
struct autoload_const *ac = state->ac;
|
||||
struct autoload_data_i *ele;
|
||||
|
||||
ele = rb_check_typeddata(ac->ad, &autoload_data_i_type);
|
||||
VALUE mutex = state->mutex;
|
||||
|
||||
if (ele->state == state) {
|
||||
need_wakeups = 1;
|
||||
ele->state = 0;
|
||||
ele->fork_gen = 0;
|
||||
}
|
||||
|
||||
rb_mutex_unlock(mutex);
|
||||
|
||||
/* At the last, move a value defined in autoload to constant table */
|
||||
if (RTEST(state->result)) {
|
||||
struct autoload_const *next;
|
||||
|
@ -2492,59 +2508,13 @@ autoload_reset(VALUE arg)
|
|||
}
|
||||
}
|
||||
|
||||
/* wakeup any waiters we had */
|
||||
if (need_wakeups) {
|
||||
struct autoload_state *cur = 0, *nxt;
|
||||
|
||||
ccan_list_for_each_safe(&state->waitq, cur, nxt, waitq.n) {
|
||||
VALUE th = cur->thread;
|
||||
|
||||
cur->thread = Qfalse;
|
||||
ccan_list_del_init(&cur->waitq.n); /* idempotent */
|
||||
|
||||
/*
|
||||
* cur is stored on the stack of cur->waiting_th,
|
||||
* do not touch cur after waking up waiting_th
|
||||
*/
|
||||
rb_thread_wakeup_alive(th);
|
||||
}
|
||||
}
|
||||
|
||||
return 0; /* ignored */
|
||||
}
|
||||
|
||||
static VALUE
|
||||
autoload_sleep(VALUE arg)
|
||||
{
|
||||
struct autoload_state *state = (struct autoload_state *)arg;
|
||||
|
||||
/*
|
||||
* autoload_reset in other thread will resume us and remove us
|
||||
* from the waitq list
|
||||
*/
|
||||
do {
|
||||
rb_thread_sleep_deadly();
|
||||
} while (state->thread != Qfalse);
|
||||
|
||||
return Qfalse;
|
||||
}
|
||||
|
||||
static VALUE
|
||||
autoload_sleep_done(VALUE arg)
|
||||
{
|
||||
struct autoload_state *state = (struct autoload_state *)arg;
|
||||
|
||||
if (state->thread != Qfalse && rb_thread_to_be_killed(state->thread)) {
|
||||
ccan_list_del(&state->waitq.n); /* idempotent after list_del_init */
|
||||
}
|
||||
|
||||
return Qfalse;
|
||||
return 0; /* ignored */
|
||||
}
|
||||
|
||||
VALUE
|
||||
rb_autoload_load(VALUE mod, ID id)
|
||||
{
|
||||
VALUE load, result;
|
||||
VALUE load;
|
||||
const char *loading = 0, *src;
|
||||
struct autoload_data_i *ele;
|
||||
struct autoload_const *ac;
|
||||
|
@ -2568,34 +2538,28 @@ rb_autoload_load(VALUE mod, ID id)
|
|||
|
||||
/* set ele->state for a marker of autoloading thread */
|
||||
if (!(ele = get_autoload_data(load, &ac))) {
|
||||
return Qfalse;
|
||||
return Qfalse;
|
||||
}
|
||||
|
||||
state.ac = ac;
|
||||
state.thread = rb_thread_current();
|
||||
|
||||
if (!ele->state) {
|
||||
ele->state = &state;
|
||||
ele->fork_gen = GET_VM()->fork_gen;
|
||||
ele->state = &state;
|
||||
ele->state->mutex = rb_mutex_new();
|
||||
ele->fork_gen = GET_VM()->fork_gen;
|
||||
}
|
||||
else if (rb_mutex_owned_p(ele->state->mutex)) {
|
||||
return Qfalse;
|
||||
} else {
|
||||
state.mutex = ele->state->mutex;
|
||||
}
|
||||
|
||||
/*
|
||||
* autoload_reset will wake up any threads added to this
|
||||
* if and only if the GVL is released during autoload_require
|
||||
*/
|
||||
ccan_list_head_init(&state.waitq);
|
||||
}
|
||||
else if (state.thread == ele->state->thread) {
|
||||
return Qfalse;
|
||||
}
|
||||
else {
|
||||
ccan_list_add_tail(&ele->state->waitq, &state.waitq.n);
|
||||
|
||||
rb_ensure(autoload_sleep, (VALUE)&state,
|
||||
autoload_sleep_done, (VALUE)&state);
|
||||
}
|
||||
// Block all other threads that come here until we are done in autoload_reset. At that point, all threads can continue. Current implementation prevents threads from executing in parallel even though at that point there are no data races.
|
||||
rb_mutex_lock(state.mutex);
|
||||
|
||||
/* autoload_data_i can be deleted by another thread while require */
|
||||
state.result = Qfalse;
|
||||
result = rb_ensure(autoload_require, (VALUE)&state,
|
||||
autoload_reset, (VALUE)&state);
|
||||
VALUE result = rb_ensure(autoload_require, (VALUE)&state, autoload_reset, (VALUE)&state);
|
||||
|
||||
if (!(ce = rb_const_lookup(mod, id)) || ce->value == Qundef) {
|
||||
rb_const_remove(mod, id);
|
||||
|
@ -2603,7 +2567,9 @@ rb_autoload_load(VALUE mod, ID id)
|
|||
else if (flag > 0) {
|
||||
ce->flag |= flag;
|
||||
}
|
||||
|
||||
RB_GC_GUARD(load);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -2669,8 +2635,8 @@ rb_const_search_from(VALUE klass, ID id, int exclude, int recurse, int visibilit
|
|||
RTEST(current);
|
||||
current = RCLASS_SUPER(current), first_iteration = false) {
|
||||
VALUE tmp;
|
||||
VALUE am = 0;
|
||||
rb_const_entry_t *ce;
|
||||
VALUE am = 0;
|
||||
rb_const_entry_t *ce;
|
||||
|
||||
if (!first_iteration && RCLASS_ORIGIN(current) != current) {
|
||||
// This item in the super chain has an origin iclass
|
||||
|
@ -2685,28 +2651,28 @@ rb_const_search_from(VALUE klass, ID id, int exclude, int recurse, int visibilit
|
|||
if (BUILTIN_TYPE(tmp) == T_ICLASS) tmp = RBASIC(tmp)->klass;
|
||||
|
||||
// Do the lookup. Loop in case of autoload.
|
||||
while ((ce = rb_const_lookup(tmp, id))) {
|
||||
if (visibility && RB_CONST_PRIVATE_P(ce)) {
|
||||
GET_EC()->private_const_reference = tmp;
|
||||
return Qundef;
|
||||
}
|
||||
rb_const_warn_if_deprecated(ce, tmp, id);
|
||||
value = ce->value;
|
||||
if (value == Qundef) {
|
||||
while ((ce = rb_const_lookup(tmp, id))) {
|
||||
if (visibility && RB_CONST_PRIVATE_P(ce)) {
|
||||
GET_EC()->private_const_reference = tmp;
|
||||
return Qundef;
|
||||
}
|
||||
rb_const_warn_if_deprecated(ce, tmp, id);
|
||||
value = ce->value;
|
||||
if (value == Qundef) {
|
||||
struct autoload_const *ac;
|
||||
if (am == tmp) break;
|
||||
am = tmp;
|
||||
if (am == tmp) break;
|
||||
am = tmp;
|
||||
ac = autoloading_const_entry(tmp, id);
|
||||
if (ac) return ac->value;
|
||||
rb_autoload_load(tmp, id);
|
||||
continue;
|
||||
}
|
||||
rb_autoload_load(tmp, id);
|
||||
continue;
|
||||
}
|
||||
if (exclude && tmp == rb_cObject) {
|
||||
goto not_found;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
if (!recurse) break;
|
||||
goto not_found;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
if (!recurse) break;
|
||||
}
|
||||
|
||||
not_found:
|
||||
|
@ -3197,9 +3163,10 @@ current_autoload_data(VALUE mod, ID id, struct autoload_const **acp)
|
|||
if (!load) return 0;
|
||||
ele = get_autoload_data(load, acp);
|
||||
if (!ele) return 0;
|
||||
|
||||
/* for autoloading thread, keep the defined value to autoloading storage */
|
||||
if (ele->state && (ele->state->thread == rb_thread_current())) {
|
||||
return ele;
|
||||
if (autoload_by_current(ele)) {
|
||||
return ele;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
@ -3216,16 +3183,16 @@ const_tbl_update(struct autoload_const *ac)
|
|||
rb_const_entry_t *ce;
|
||||
|
||||
if (rb_id_table_lookup(tbl, id, &value)) {
|
||||
ce = (rb_const_entry_t *)value;
|
||||
if (ce->value == Qundef) {
|
||||
struct autoload_data_i *ele = current_autoload_data(klass, id, &ac);
|
||||
ce = (rb_const_entry_t *)value;
|
||||
if (ce->value == Qundef) {
|
||||
struct autoload_data_i *ele = current_autoload_data(klass, id, &ac);
|
||||
|
||||
if (ele) {
|
||||
rb_clear_constant_cache_for_id(id);
|
||||
if (ele) {
|
||||
rb_clear_constant_cache_for_id(id);
|
||||
|
||||
ac->value = val; /* autoload_i is non-WB-protected */
|
||||
ac->value = val; /* autoload_i is non-WB-protected */
|
||||
ac->file = rb_source_location(&ac->line);
|
||||
}
|
||||
}
|
||||
else {
|
||||
/* otherwise autoloaded constant, allow to override */
|
||||
autoload_delete(klass, id);
|
||||
|
@ -3235,29 +3202,29 @@ const_tbl_update(struct autoload_const *ac)
|
|||
ce->line = ac->line;
|
||||
}
|
||||
return;
|
||||
}
|
||||
else {
|
||||
VALUE name = QUOTE_ID(id);
|
||||
visibility = ce->flag;
|
||||
if (klass == rb_cObject)
|
||||
rb_warn("already initialized constant %"PRIsVALUE"", name);
|
||||
else
|
||||
rb_warn("already initialized constant %"PRIsVALUE"::%"PRIsVALUE"",
|
||||
rb_class_name(klass), name);
|
||||
if (!NIL_P(ce->file) && ce->line) {
|
||||
rb_compile_warn(RSTRING_PTR(ce->file), ce->line,
|
||||
"previous definition of %"PRIsVALUE" was here", name);
|
||||
}
|
||||
}
|
||||
rb_clear_constant_cache_for_id(id);
|
||||
setup_const_entry(ce, klass, val, visibility);
|
||||
}
|
||||
else {
|
||||
VALUE name = QUOTE_ID(id);
|
||||
visibility = ce->flag;
|
||||
if (klass == rb_cObject)
|
||||
rb_warn("already initialized constant %"PRIsVALUE"", name);
|
||||
else
|
||||
rb_warn("already initialized constant %"PRIsVALUE"::%"PRIsVALUE"",
|
||||
rb_class_name(klass), name);
|
||||
if (!NIL_P(ce->file) && ce->line) {
|
||||
rb_compile_warn(RSTRING_PTR(ce->file), ce->line,
|
||||
"previous definition of %"PRIsVALUE" was here", name);
|
||||
}
|
||||
}
|
||||
rb_clear_constant_cache_for_id(id);
|
||||
setup_const_entry(ce, klass, val, visibility);
|
||||
}
|
||||
else {
|
||||
rb_clear_constant_cache_for_id(id);
|
||||
rb_clear_constant_cache_for_id(id);
|
||||
|
||||
ce = ZALLOC(rb_const_entry_t);
|
||||
rb_id_table_insert(tbl, id, (VALUE)ce);
|
||||
setup_const_entry(ce, klass, val, visibility);
|
||||
ce = ZALLOC(rb_const_entry_t);
|
||||
rb_id_table_insert(tbl, id, (VALUE)ce);
|
||||
setup_const_entry(ce, klass, val, visibility);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue