diff --git a/file.c b/file.c
index 64f5965aef53bd..41f3c372cef995 100644
--- a/file.c
+++ b/file.c
@@ -2879,15 +2879,27 @@ chmod_internal(const char *path, void *mode)
/*
* call-seq:
- * File.chmod(mode_int, file_name, ... ) -> integer
+ * File.chmod(mode, *paths) -> integer
*
- * Changes permission bits on the named file(s) to the bit pattern
- * represented by mode_int. Actual effects are operating system
- * dependent (see the beginning of this section). On Unix systems, see
- * chmod(2) for details. Returns the number of files
- * processed.
+ * Changes the mode (i.e., permissions) of the entries of each the given +paths+;
+ * see {File Permissions}[rdoc-ref:File@File+Permissions].
+ * Returns the count of the given +paths+:
+ *
+ * filepath = 't.tmp'
+ * File.write(filepath, 'foo')
+ * dirpath = 'tempdir'
+ * Dir.mkdir(dirpath)
+ * File::Stat.new(filepath).mode.to_s(8) # => "100664"
+ * File::Stat.new(dirpath).mode.to_s(8) # => "40775"
+ * File.chmod(0775, filepath, dirpath) # => 2
+ * File::Stat.new(filepath).mode.to_s(8) # => "100775"
+ * File::Stat.new(dirpath).mode.to_s(8) # => "40775"
+ * File.chmod(0664, filepath, dirpath) # => 2
+ * File::Stat.new(filepath).mode.to_s(8) # => "100664"
+ * File::Stat.new(dirpath).mode.to_s(8) # => "40664"
+ * File.delete(filepath)
+ * Dir.rmdir(dirpath)
*
- * File.chmod(0644, "testfile", "out") #=> 2
*/
static VALUE
@@ -3047,16 +3059,46 @@ chown_internal(const char *path, void *arg)
/*
* call-seq:
- * File.chown(owner_int, group_int, file_name, ...) -> integer
+ * File.chown(owner_int, group_int, *paths) -> integer
+ *
+ * Changes the owner and group of the entry at each of the given +paths+;
+ * returns the count of the given +paths+:
+ *
+ * # Super user; all privileges.
+ * Process.uid => 0
+ * Process.gid => 0
+ * # Create a directory and a file.
+ * dirpath = 'doc/foo'
+ * Dir.mkdir(dirpath)
+ * filepath = 't.tmp'
+ * File.write(filepath, 'foo')
+ * # Get their user and group ids.
+ * dirstat = File::Stat.new(dirpath)
+ * dirstat.uid => 0
+ * dirstat.gid => 0
+ * filestat = File::Stat.new(filepath)
+ * filestat.uid => 0
+ * filestat.gid => 0
+ * # Change ownership of both.
+ * File.chown(1000, 1000, filepath, dirpath) => 2
+ * dirstat = File::Stat.new(dirpath)
+ * dirstat.uid => 1000
+ * dirstat.gid => 1000
+ * filestat = File::Stat.new(filepath)
+ * filestat.uid => 1000
+ * filestat.gid => 1000
+ * # Clean up.
+ * Dir.rmdir(dirpath)
+ * File.delete(filepath)
*
- * Changes the owner and group of the named file(s) to the given
- * numeric owner and group id's. Only a process with superuser
- * privileges may change the owner of a file. The current owner of a
- * file may change the file's group to any group to which the owner
- * belongs. A nil or -1 owner or group id is ignored.
- * Returns the number of files processed.
+ * Notes:
*
- * File.chown(nil, 100, "testfile")
+ * - On Windows, the owner and group are not changed.
+ * - Only a process with superuser privileges can change the owner of an entry.
+ * - The owner of an entry can change its group to any group
+ * to which the owner belongs.
+ * - A +nil+ or +-1+ owner or group id is ignored.
+ * - The method follows symbolic links to the target entry.
*
*/
diff --git a/proc.c b/proc.c
index 586d02139c4215..aeaf6de448027f 100644
--- a/proc.c
+++ b/proc.c
@@ -2210,6 +2210,20 @@ rb_proc_parameters(int argc, VALUE *argv, VALUE self)
return rb_iseq_parameters(iseq, is_proc);
}
+static st_index_t
+iseq_location_hash(st_index_t hash, const rb_iseq_t *iseq)
+{
+ const struct rb_iseq_constant_body *body = ISEQ_BODY(iseq);
+ if (body) {
+ const rb_iseq_location_t *loc = &body->location;
+ hash = rb_st_hash_uint(hash, (st_index_t)loc->code_location.beg_pos.lineno);
+ hash = rb_st_hash_uint(hash, (st_index_t)loc->code_location.beg_pos.column);
+ hash = rb_st_hash_uint(hash, (st_index_t)loc->code_location.end_pos.lineno);
+ hash = rb_st_hash_uint(hash, (st_index_t)loc->code_location.end_pos.column);
+ }
+ return hash;
+}
+
st_index_t
rb_hash_proc(st_index_t hash, VALUE prc)
{
@@ -2224,13 +2238,13 @@ rb_hash_proc(st_index_t hash, VALUE prc)
VALUE recipe = rb_proc_refinements_recipe(prc);
long len = RARRAY_LEN(recipe);
hash = rb_st_hash_uint(hash, (st_index_t)RARRAY_AREF(recipe, REFINEMENT_RECIPE_BASE_CREF));
- hash = rb_st_hash_uint(hash, (st_index_t)((const rb_iseq_t *)RARRAY_AREF(recipe, REFINEMENT_RECIPE_SRC_ISEQ))->body);
+ hash = iseq_location_hash(hash, (const rb_iseq_t *)RARRAY_AREF(recipe, REFINEMENT_RECIPE_SRC_ISEQ));
for (long i = REFINEMENT_RECIPE_MODS; i < len; i++) {
hash = rb_st_hash_uint(hash, (st_index_t)RARRAY_AREF(recipe, i));
}
}
else {
- hash = rb_st_hash_uint(hash, (st_index_t)ISEQ_BODY(proc->block.as.captured.code.iseq));
+ hash = iseq_location_hash(hash, proc->block.as.captured.code.iseq);
}
break;
case block_type_ifunc:
diff --git a/thread_pthread.c b/thread_pthread.c
index 44b02032a6fa69..b17bbbc3f3ea06 100644
--- a/thread_pthread.c
+++ b/thread_pthread.c
@@ -1482,7 +1482,7 @@ ractor_sched_enq(rb_vm_t *vm, rb_ractor_t *r)
// With every snt dedicated or retired, only the timer thread's
// timeout branch can serve the entry or widen the pool: wake it
// (a no-op unless it sleeps untimed).
- if (vm->ractor.sched.snt_cnt == 0) {
+ if (RUBY_ATOMIC_LOAD(vm->ractor.sched.snt_cnt) == 0) {
timer_thread_wakeup_locked(vm);
}
@@ -1528,8 +1528,8 @@ ractor_sched_deq(rb_vm_t *vm, rb_ractor_t *cr)
RUBY_DEBUG_LOG("wait grq_cnt:%d", (int)vm->ractor.sched.grq_cnt);
if (SNT_IDLE_RETIRE >= 0 && ++idle_streak > SNT_IDLE_RETIRE &&
- (int)vm->ractor.sched.snt_cnt > SNT_KEEP_MINIMUM) {
- vm->ractor.sched.snt_cnt--;
+ (int)RUBY_ATOMIC_LOAD(vm->ractor.sched.snt_cnt) > SNT_KEEP_MINIMUM) {
+ RUBY_ATOMIC_DEC(vm->ractor.sched.snt_cnt);
RUBY_DEBUG_LOG("retire, snt_cnt:%d", (int)vm->ractor.sched.snt_cnt);
break; // returning NULL ends this nt; see the caller
}
@@ -1862,11 +1862,15 @@ thread_sched_atfork(struct rb_thread_sched *sched)
if (th_has_dedicated_nt(th)) {
vm->ractor.sched.snt_cnt = 0;
+#if USE_RUBY_DEBUG_LOG
vm->ractor.sched.dnt_cnt = 1;
+#endif
}
else {
vm->ractor.sched.snt_cnt = 1;
+#if USE_RUBY_DEBUG_LOG
vm->ractor.sched.dnt_cnt = 0;
+#endif
}
vm->ractor.sched.running_cnt = 0;
@@ -2029,7 +2033,9 @@ Init_native_thread(rb_thread_t *main_th)
main_th->nt->vm = vm;
// setup mn
+#if USE_RUBY_DEBUG_LOG
vm->ractor.sched.dnt_cnt = 1;
+#endif
}
extern int ruby_mn_threads_enabled;
@@ -2074,18 +2080,21 @@ native_thread_dedicated_inc(rb_vm_t *vm, rb_ractor_t *cr, struct rb_native_threa
RUBY_DEBUG_LOG("nt:%d %d->%d", nt->serial, nt->dedicated, nt->dedicated + 1);
if (nt->dedicated == 0) {
- ractor_sched_lock(vm, cr);
- {
- vm->ractor.sched.snt_cnt--;
- vm->ractor.sched.dnt_cnt++;
-
- // This may have dedicated the last snt away from a pending
- // entry whose enqueue saw snt_cnt > 0 (see ractor_sched_enq).
- if (vm->ractor.sched.snt_cnt == 0 && vm->ractor.sched.grq_cnt > 0) {
- timer_thread_wakeup_locked(vm);
+ // Lock-free; pairs with ractor_sched_enq (enq: grq_cnt up then read
+ // snt_cnt / here: snt_cnt down then read grq_cnt) against lost wakeups.
+ if (RUBY_ATOMIC_FETCH_SUB(vm->ractor.sched.snt_cnt, 1) == 1) {
+ // the last snt went dedicated; pending entries need the timer thread
+ ractor_sched_lock(vm, cr);
+ {
+ if (vm->ractor.sched.grq_cnt > 0) {
+ timer_thread_wakeup_locked(vm);
+ }
}
+ ractor_sched_unlock(vm, cr);
}
- ractor_sched_unlock(vm, cr);
+#if USE_RUBY_DEBUG_LOG
+ vm->ractor.sched.dnt_cnt++;
+#endif
}
nt->dedicated++;
@@ -2099,21 +2108,21 @@ native_thread_dedicated_dec(rb_vm_t *vm, rb_ractor_t *cr, struct rb_native_threa
nt->dedicated--;
if (nt->dedicated == 0) {
- ractor_sched_lock(vm, cr);
- {
- /* max_cpu bounds the shared threads and this is where one rejoins
- * them, so this is where the cap has to hold. A thread with no room
- * to come back to belongs to neither count until it ends. */
- if (vm->ractor.sched.snt_cnt < vm->ractor.sched.max_cpu ||
- (int)vm->ractor.sched.snt_cnt <= MINIMUM_SNT) {
- vm->ractor.sched.snt_cnt++;
+ // Rejoin under the max_cpu cap; with no room this nt retires and
+ // belongs to neither count until it ends.
+ while (1) {
+ rb_atomic_t snt = RUBY_ATOMIC_LOAD(vm->ractor.sched.snt_cnt);
+ if (snt < vm->ractor.sched.max_cpu || (int)snt <= MINIMUM_SNT) {
+ if (RUBY_ATOMIC_CAS(vm->ractor.sched.snt_cnt, snt, snt + 1) == snt) break;
}
else {
nt->retiring = true;
+ break;
}
- vm->ractor.sched.dnt_cnt--;
}
- ractor_sched_unlock(vm, cr);
+#if USE_RUBY_DEBUG_LOG
+ vm->ractor.sched.dnt_cnt--;
+#endif
}
}
diff --git a/thread_pthread_mn.c b/thread_pthread_mn.c
index 97674d466d51f2..4226a39cb045db 100644
--- a/thread_pthread_mn.c
+++ b/thread_pthread_mn.c
@@ -824,19 +824,25 @@ native_thread_check_and_create_shared(rb_vm_t *vm)
if (!vm->ractor.main_ractor->threads.sched.enable_mn_threads)
schedulable_ractor_cnt--; // do not need snt for main ractor
- unsigned int snt_cnt = vm->ractor.sched.snt_cnt;
- if (((int)snt_cnt < MINIMUM_SNT) ||
- (snt_cnt < schedulable_ractor_cnt &&
- snt_cnt < vm->ractor.sched.max_cpu)) {
+ // CAS keeps a concurrent rejoin from pushing snt_cnt past the cap
+ rb_atomic_t snt_cnt = RUBY_ATOMIC_LOAD(vm->ractor.sched.snt_cnt);
+ while (((int)snt_cnt < MINIMUM_SNT) ||
+ (snt_cnt < schedulable_ractor_cnt &&
+ snt_cnt < vm->ractor.sched.max_cpu)) {
+ rb_atomic_t prev = RUBY_ATOMIC_CAS(vm->ractor.sched.snt_cnt, snt_cnt, snt_cnt + 1);
+ if (prev == snt_cnt) {
+ need_to_make = true;
+ break;
+ }
+ snt_cnt = prev;
+ }
+ if (need_to_make) {
RUBY_DEBUG_LOG("added snt:%u dnt:%u ractor_cnt:%u grq_cnt:%u",
vm->ractor.sched.snt_cnt,
vm->ractor.sched.dnt_cnt,
vm->ractor.cnt,
vm->ractor.sched.grq_cnt);
-
- vm->ractor.sched.snt_cnt++;
- need_to_make = true;
}
else {
RUBY_DEBUG_LOG("snt:%d ractor_cnt:%d", (int)vm->ractor.sched.snt_cnt, (int)vm->ractor.cnt);
@@ -852,7 +858,7 @@ native_thread_check_and_create_shared(rb_vm_t *vm)
// Roll back, or this function would conclude forever that the
// pool is wide enough and never try again.
ractor_sched_lock(vm, NULL);
- vm->ractor.sched.snt_cnt--;
+ RUBY_ATOMIC_DEC(vm->ractor.sched.snt_cnt);
ractor_sched_unlock(vm, NULL);
native_thread_destroy(nt);
}
diff --git a/vm_core.h b/vm_core.h
index b9a57e210bd3a1..35c2d69512317f 100644
--- a/vm_core.h
+++ b/vm_core.h
@@ -749,8 +749,8 @@ typedef struct rb_vm_struct {
bool locked;
rb_nativethread_cond_t cond; // GRQ
- unsigned int snt_cnt; // count of shared NTs
- unsigned int dnt_cnt; // count of dedicated NTs
+ rb_atomic_t snt_cnt; // count of shared NTs; lock-free (see native_thread_dedicated_inc)
+ unsigned int dnt_cnt; // count of dedicated NTs; logging only (USE_RUBY_DEBUG_LOG), not atomic
unsigned int running_cnt;