mirror of
https://github.com/ruby/ruby.git
synced 2026-10-28 08:25:20 +08:00
Matches Thread#alive? and Fiber#alive?. Returns false once the ractor's result has been recorded. Reads sync.legacy under the ractor lock, as Ractor#monitor does, so it cannot race with termination. [Feature #22441]
1970 lines
56 KiB
C
1970 lines
56 KiB
C
// this file is included by ractor.c
|
|
|
|
struct ractor_port {
|
|
rb_ractor_t *r;
|
|
st_data_t id_;
|
|
};
|
|
|
|
static st_data_t
|
|
ractor_port_id(const struct ractor_port *rp)
|
|
{
|
|
return rp->id_;
|
|
}
|
|
|
|
static VALUE rb_cRactorPort;
|
|
|
|
static VALUE ractor_receive(rb_execution_context_t *ec, const struct ractor_port *rp, const rb_hrtime_t *end);
|
|
static VALUE ractor_send(rb_execution_context_t *ec, const struct ractor_port *rp, VALUE obj, VALUE move);
|
|
static struct ractor_basket *ractor_basket_new_exit(VALUE sender, VALUE token);
|
|
static void ractor_send_basket(rb_execution_context_t *ec, const struct ractor_port *rp, struct ractor_basket *b, bool raise_on_error);
|
|
static void ractor_add_port(rb_ractor_t *r, st_data_t id);
|
|
|
|
// The off-heap courier a copy or a move payload travels in. Defined in ractor.c.
|
|
struct rb_ractor_courier *rb_ractor_courier_build_move(VALUE obj, struct rb_ractor_courier **slot);
|
|
VALUE rb_ractor_courier_materialize(struct rb_ractor_courier *c);
|
|
void rb_ractor_courier_free(struct rb_ractor_courier *c);
|
|
static void ractor_off_queue_add(rb_ractor_t *cr, struct ractor_basket *b);
|
|
static void ractor_off_queue_remove(struct ractor_basket *b);
|
|
void rb_ractor_courier_mark(const struct rb_gc_mark_ctx *ctx, struct rb_ractor_courier *c);
|
|
struct rb_ractor_courier *rb_ractor_courier_build_copy(VALUE obj, struct rb_ractor_courier **slot);
|
|
|
|
static void ractor_port_note_alive(const struct ractor_port *rp);
|
|
|
|
static void
|
|
ractor_port_mark(void *ptr)
|
|
{
|
|
const struct ractor_port *rp = (struct ractor_port *)ptr;
|
|
|
|
if (rp->r) {
|
|
rb_gc_mark(rp->r->pub.self);
|
|
|
|
/* Only a mark that covers every objspace can call a port dead. Ask the single
|
|
* objspace first: it answers without the VM, which a GC worker thread cannot
|
|
* reach (mmtk marks from several of them). */
|
|
if (rb_gc_single_objspace_p() || rb_gc_during_global_gc_p()) {
|
|
ractor_port_note_alive(rp);
|
|
}
|
|
}
|
|
}
|
|
|
|
static const rb_data_type_t ractor_port_data_type = {
|
|
"ractor/port",
|
|
{
|
|
ractor_port_mark,
|
|
RUBY_TYPED_DEFAULT_FREE,
|
|
NULL, // memsize
|
|
NULL, // update
|
|
},
|
|
0, 0, RUBY_TYPED_THREAD_SAFE_FREE | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_FROZEN_SHAREABLE | RUBY_TYPED_EMBEDDABLE,
|
|
};
|
|
|
|
static st_data_t
|
|
ractor_genid_for_port(rb_ractor_t *cr)
|
|
{
|
|
// TODO: enough?
|
|
return cr->sync.next_port_id++;
|
|
}
|
|
|
|
static struct ractor_port *
|
|
RACTOR_PORT_PTR(VALUE self)
|
|
{
|
|
VM_ASSERT(rb_typeddata_is_kind_of(self, &ractor_port_data_type));
|
|
return RTYPEDDATA_GET_DATA(self);
|
|
}
|
|
|
|
// r is NULL between Ractor::Port.allocate and ractor_port_init()
|
|
static struct ractor_port *
|
|
ractor_port_ptr_check(VALUE self)
|
|
{
|
|
struct ractor_port *rp = RACTOR_PORT_PTR(self);
|
|
|
|
if (UNLIKELY(rp->r == NULL)) {
|
|
rb_raise(rb_eTypeError, "uninitialized %"PRIsVALUE, rb_obj_class(self));
|
|
}
|
|
|
|
return rp;
|
|
}
|
|
|
|
static VALUE
|
|
ractor_port_alloc(VALUE klass)
|
|
{
|
|
struct ractor_port *rp;
|
|
VALUE rpv = TypedData_Make_Struct(klass, struct ractor_port, &ractor_port_data_type, rp);
|
|
rb_obj_freeze(rpv);
|
|
return rpv;
|
|
}
|
|
|
|
static VALUE
|
|
ractor_port_init(VALUE rpv, rb_ractor_t *r)
|
|
{
|
|
// Child threads can still run ensure blocks and report exceptions after
|
|
// ractor_notify_exit has freed the ports.
|
|
if (!r->sync.ports) {
|
|
rb_raise(rb_eRactorClosedError, "The ractor has terminated");
|
|
}
|
|
|
|
struct ractor_port *rp = RACTOR_PORT_PTR(rpv);
|
|
|
|
rp->r = r;
|
|
RB_OBJ_WRITTEN(rpv, Qundef, r->pub.self);
|
|
rp->id_ = ractor_genid_for_port(r);
|
|
|
|
ractor_add_port(r, ractor_port_id(rp));
|
|
|
|
rb_obj_freeze(rpv);
|
|
|
|
return rpv;
|
|
}
|
|
|
|
/*
|
|
* call-seq:
|
|
* Ractor::Port.new -> new_port
|
|
*
|
|
* Returns a new Ractor::Port object.
|
|
*/
|
|
static VALUE
|
|
ractor_port_initialize(VALUE self)
|
|
{
|
|
return ractor_port_init(self, GET_RACTOR());
|
|
}
|
|
|
|
/* :nodoc: */
|
|
static VALUE
|
|
ractor_port_initialize_copy(VALUE self, VALUE orig)
|
|
{
|
|
struct ractor_port *dst = RACTOR_PORT_PTR(self); // uninitialized by definition
|
|
struct ractor_port *src = ractor_port_ptr_check(orig);
|
|
dst->r = src->r;
|
|
RB_OBJ_WRITTEN(self, Qundef, dst->r->pub.self);
|
|
dst->id_ = ractor_port_id(src);
|
|
|
|
return self;
|
|
}
|
|
|
|
static VALUE
|
|
ractor_port_new(rb_ractor_t *r)
|
|
{
|
|
VALUE rpv = ractor_port_alloc(rb_cRactorPort);
|
|
ractor_port_init(rpv, r);
|
|
return rpv;
|
|
}
|
|
|
|
static bool
|
|
ractor_port_p(VALUE self)
|
|
{
|
|
return rb_typeddata_is_kind_of(self, &ractor_port_data_type);
|
|
}
|
|
|
|
static const rb_hrtime_t *ractor_timeout_deadline(VALUE timeout, rb_hrtime_t *storage);
|
|
|
|
static VALUE
|
|
ractor_port_receive(rb_execution_context_t *ec, VALUE self, VALUE timeout)
|
|
{
|
|
const struct ractor_port *rp = ractor_port_ptr_check(self);
|
|
|
|
if (rp->r != rb_ec_ractor_ptr(ec)) {
|
|
rb_raise(rb_eRactorError, "only allowed from the creator Ractor of this port");
|
|
}
|
|
|
|
rb_hrtime_t deadline;
|
|
const rb_hrtime_t *end = ractor_timeout_deadline(timeout, &deadline);
|
|
|
|
VALUE v = ractor_receive(ec, rp, end);
|
|
RB_GC_GUARD(self);
|
|
|
|
// no message before the timeout
|
|
return UNDEF_P(v) ? Qnil : v;
|
|
}
|
|
|
|
static VALUE
|
|
ractor_port_send(rb_execution_context_t *ec, VALUE self, VALUE obj, VALUE move)
|
|
{
|
|
const struct ractor_port *rp = ractor_port_ptr_check(self);
|
|
ractor_send(ec, rp, obj, RTEST(move));
|
|
RB_GC_GUARD(self);
|
|
return self;
|
|
}
|
|
|
|
static bool ractor_closed_port_p(rb_execution_context_t *ec, rb_ractor_t *r, const struct ractor_port *rp);
|
|
static bool ractor_close_port(rb_execution_context_t *ec, rb_ractor_t *r, const struct ractor_port *rp);
|
|
|
|
static VALUE
|
|
ractor_port_closed_p(rb_execution_context_t *ec, VALUE self)
|
|
{
|
|
const struct ractor_port *rp = ractor_port_ptr_check(self);
|
|
rb_ractor_t *r = rp->r;
|
|
bool closed;
|
|
|
|
if (rb_ec_ractor_ptr(ec) == r) {
|
|
/* The owner's threads are serialized by the ractor GVL, so the ports
|
|
* table can't change under this lookup. */
|
|
closed = ractor_closed_port_p(ec, r, rp);
|
|
}
|
|
else {
|
|
/* A foreign Ractor races the owner's st_insert/st_delete on the ports
|
|
* table; take the lock like every other foreign reader. ractor_closed_port_p
|
|
* asserts the lock is held for foreign access, and Port#closed? was the
|
|
* only path reaching it without the lock. */
|
|
RACTOR_LOCK(r);
|
|
{
|
|
closed = ractor_closed_port_p(ec, r, rp);
|
|
}
|
|
RACTOR_UNLOCK(r);
|
|
}
|
|
|
|
return closed ? Qtrue : Qfalse;
|
|
}
|
|
|
|
static VALUE
|
|
ractor_port_close(rb_execution_context_t *ec, VALUE self)
|
|
{
|
|
const struct ractor_port *rp = ractor_port_ptr_check(self);
|
|
rb_ractor_t *cr = rb_ec_ractor_ptr(ec);
|
|
|
|
if (cr != rp->r) {
|
|
rb_raise(rb_eRactorError, "closing port by other ractors is not allowed");
|
|
}
|
|
|
|
ractor_close_port(ec, cr, rp);
|
|
return self;
|
|
}
|
|
|
|
// ractor-internal
|
|
|
|
// ractor-internal - ractor_basket
|
|
|
|
enum ractor_basket_type {
|
|
// basket is empty
|
|
basket_type_none,
|
|
|
|
// value is available
|
|
basket_type_ref,
|
|
basket_type_copy,
|
|
basket_type_move,
|
|
/* A ractor's exit token. The pair it becomes is built on the receiving side,
|
|
* so a terminating ractor adds no shareable object to the vm, and building it
|
|
* needs neither a tag nor a courier -- which the sender has no stack for. */
|
|
basket_type_exit,
|
|
};
|
|
|
|
struct ractor_basket {
|
|
VALUE sender;
|
|
st_data_t port_id;
|
|
|
|
/* The payload: either a VALUE, or the off-heap (xmalloc) courier its graph was
|
|
* serialized into (copy and move both use one). */
|
|
union {
|
|
VALUE v;
|
|
struct rb_ractor_courier *courier;
|
|
} p;
|
|
|
|
/* The one list it is on: a port queue while it waits there, or its holder's
|
|
* sync.off_queue_baskets while it is being built or materialized. */
|
|
struct ccan_list_node node;
|
|
|
|
enum ractor_basket_type type : 8;
|
|
bool has_courier;
|
|
bool exception;
|
|
};
|
|
|
|
#if 0
|
|
static inline bool
|
|
ractor_basket_type_p(const struct ractor_basket *b, enum ractor_basket_type type)
|
|
{
|
|
return b->type == type;
|
|
}
|
|
|
|
static inline bool
|
|
ractor_basket_none_p(const struct ractor_basket *b)
|
|
{
|
|
return ractor_basket_type_p(b, basket_type_none);
|
|
}
|
|
#endif
|
|
|
|
static void
|
|
ractor_basket_mark(const struct rb_gc_mark_ctx *ctx, const struct ractor_basket *b)
|
|
{
|
|
if (b->has_courier) {
|
|
/* The payload became this Ractor's to root the moment the message was enqueued
|
|
* here: the sender's own roots stop at the send. Before and after the queue the
|
|
* basket is on its holder's off_queue_baskets instead, so a courier is rooted
|
|
* from the moment it is allocated to the moment it is freed. */
|
|
rb_ractor_courier_mark(ctx, b->p.courier);
|
|
}
|
|
else {
|
|
rb_gc_mark_ctx(ctx, b->p.v);
|
|
}
|
|
|
|
/* An exit token names a ractor that may be reachable from nothing else. */
|
|
rb_gc_mark_ctx(ctx, b->sender);
|
|
}
|
|
|
|
static void
|
|
ractor_basket_free(struct ractor_basket *b)
|
|
{
|
|
ractor_off_queue_remove(b);
|
|
if (b->has_courier && b->p.courier) {
|
|
/* A courier that was never consumed (a queue being torn down, say). */
|
|
rb_ractor_courier_free(b->p.courier);
|
|
b->p.courier = NULL;
|
|
}
|
|
SIZED_FREE(b);
|
|
}
|
|
|
|
static struct ractor_basket *
|
|
ractor_basket_alloc(void)
|
|
{
|
|
struct ractor_basket *b = ALLOC(struct ractor_basket);
|
|
|
|
/* Empty and mark-safe from the start: a basket goes on its holder's in-flight list
|
|
* before it has a payload, so a GC can walk it while it is still being filled. */
|
|
b->type = basket_type_none;
|
|
b->sender = Qnil;
|
|
b->port_id = 0;
|
|
b->p.v = Qnil;
|
|
b->has_courier = false;
|
|
b->exception = false;
|
|
ccan_list_node_init(&b->node);
|
|
|
|
return b;
|
|
}
|
|
|
|
/* A basket is rooted by whoever holds it: a port queue while it waits there, and its
|
|
* holder's off-queue list while it is being built or materialized. */
|
|
static void
|
|
ractor_off_queue_add(rb_ractor_t *cr, struct ractor_basket *b)
|
|
{
|
|
VM_ASSERT(cr == rb_current_ractor_raw(false));
|
|
ccan_list_add_tail(&cr->sync.off_queue_baskets, &b->node);
|
|
}
|
|
|
|
static void
|
|
ractor_off_queue_remove(struct ractor_basket *b)
|
|
{
|
|
ccan_list_del_init(&b->node);
|
|
}
|
|
|
|
static void
|
|
ractor_mark_off_queue_baskets(const struct rb_gc_mark_ctx *ctx, rb_ractor_t *r)
|
|
{
|
|
struct ractor_basket *b;
|
|
ccan_list_for_each(&r->sync.off_queue_baskets, b, node) {
|
|
ractor_basket_mark(ctx, b);
|
|
}
|
|
}
|
|
|
|
// ractor-internal - ractor_queue
|
|
|
|
struct ractor_queue {
|
|
struct ccan_list_head set;
|
|
bool closed;
|
|
bool alive; /* its Ractor::Port is still reachable; see the reap below */
|
|
};
|
|
|
|
static void
|
|
ractor_queue_init(struct ractor_queue *rq)
|
|
{
|
|
ccan_list_head_init(&rq->set);
|
|
rq->closed = false;
|
|
rq->alive = true;
|
|
}
|
|
|
|
static struct ractor_queue *
|
|
ractor_queue_new(void)
|
|
{
|
|
struct ractor_queue *rq = ALLOC(struct ractor_queue);
|
|
ractor_queue_init(rq);
|
|
return rq;
|
|
}
|
|
|
|
static void
|
|
ractor_port_note_alive(const struct ractor_port *rp)
|
|
{
|
|
struct ractor_queue *rq;
|
|
|
|
if (rp->r->sync.ports && st_lookup(rp->r->sync.ports, rp->id_, (st_data_t *)&rq)) {
|
|
/* Several markers can reach the same port at once (mmtk marks from its GC worker
|
|
* threads), but they all store the same value and the reap reads it once marking
|
|
* is over. */
|
|
rq->alive = true;
|
|
}
|
|
}
|
|
|
|
static void
|
|
ractor_queue_mark(const struct rb_gc_mark_ctx *ctx, const struct ractor_queue *rq)
|
|
{
|
|
const struct ractor_basket *b;
|
|
|
|
ccan_list_for_each(&rq->set, b, node) {
|
|
ractor_basket_mark(ctx, b);
|
|
}
|
|
}
|
|
|
|
static void
|
|
ractor_queue_free(struct ractor_queue *rq)
|
|
{
|
|
struct ractor_basket *b, *nxt;
|
|
|
|
ccan_list_for_each_safe(&rq->set, b, nxt, node) {
|
|
ccan_list_del_init(&b->node);
|
|
ractor_basket_free(b);
|
|
}
|
|
|
|
VM_ASSERT(ccan_list_empty(&rq->set));
|
|
|
|
SIZED_FREE(rq);
|
|
}
|
|
|
|
RBIMPL_ATTR_MAYBE_UNUSED()
|
|
static size_t
|
|
ractor_queue_size(const struct ractor_queue *rq)
|
|
{
|
|
size_t size = 0;
|
|
const struct ractor_basket *b;
|
|
|
|
ccan_list_for_each(&rq->set, b, node) {
|
|
size++;
|
|
}
|
|
return size;
|
|
}
|
|
|
|
// Returns whether this call is the one that closed it.
|
|
static bool
|
|
ractor_queue_close(struct ractor_queue *rq)
|
|
{
|
|
bool closed_now = !rq->closed;
|
|
rq->closed = true;
|
|
return closed_now;
|
|
}
|
|
|
|
static void
|
|
ractor_queue_move(struct ractor_queue *dst_rq, struct ractor_queue *src_rq)
|
|
{
|
|
struct ccan_list_head *src = &src_rq->set;
|
|
struct ccan_list_head *dst = &dst_rq->set;
|
|
|
|
dst->n.next = src->n.next;
|
|
dst->n.prev = src->n.prev;
|
|
dst->n.next->prev = &dst->n;
|
|
dst->n.prev->next = &dst->n;
|
|
ccan_list_head_init(src);
|
|
}
|
|
|
|
#if 0
|
|
static struct ractor_basket *
|
|
ractor_queue_head(rb_ractor_t *r, struct ractor_queue *rq)
|
|
{
|
|
return ccan_list_top(&rq->set, struct ractor_basket, node);
|
|
}
|
|
#endif
|
|
|
|
static bool
|
|
ractor_queue_empty_p(rb_ractor_t *r, const struct ractor_queue *rq)
|
|
{
|
|
return ccan_list_empty(&rq->set);
|
|
}
|
|
|
|
static struct ractor_basket *
|
|
ractor_queue_deq(rb_ractor_t *r, struct ractor_queue *rq)
|
|
{
|
|
VM_ASSERT(GET_RACTOR() == r);
|
|
|
|
struct ractor_basket *b = ccan_list_pop(&rq->set, struct ractor_basket, node);
|
|
if (b) ccan_list_node_init(&b->node);
|
|
return b;
|
|
}
|
|
|
|
static void
|
|
ractor_queue_enq(rb_ractor_t *r, struct ractor_queue *rq, struct ractor_basket *basket)
|
|
{
|
|
ccan_list_add_tail(&rq->set, &basket->node);
|
|
}
|
|
|
|
#if 0
|
|
static void
|
|
rq_dump(const struct ractor_queue *rq)
|
|
{
|
|
int i=0;
|
|
struct ractor_basket *b;
|
|
ccan_list_for_each(&rq->set, b, node) {
|
|
fprintf(stderr, "%d type:%s %p\n", i, basket_type_name(b->type), (void *)b);
|
|
i++;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
static void ractor_delete_port(rb_ractor_t *cr, st_data_t id, bool locked);
|
|
|
|
static struct ractor_queue *
|
|
ractor_get_queue(rb_ractor_t *cr, st_data_t id, bool locked)
|
|
{
|
|
VM_ASSERT(cr == GET_RACTOR());
|
|
|
|
struct ractor_queue *rq;
|
|
|
|
if (cr->sync.ports && st_lookup(cr->sync.ports, id, (st_data_t *)&rq)) {
|
|
if (rq->closed && ractor_queue_empty_p(cr, rq)) {
|
|
ractor_delete_port(cr, id, locked);
|
|
return NULL;
|
|
}
|
|
else {
|
|
return rq;
|
|
}
|
|
}
|
|
else {
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
// ractor-internal - ports
|
|
|
|
static void
|
|
ractor_add_port(rb_ractor_t *r, st_data_t id)
|
|
{
|
|
struct ractor_queue *rq = ractor_queue_new();
|
|
ASSERT_ractor_unlocking(r);
|
|
|
|
RUBY_DEBUG_LOG("id:%u", (unsigned int)id);
|
|
|
|
// Rebuilding the table on insertion can run GC by the allocation and the
|
|
// GC acquires the VM lock, which is prohibited under the ractor lock.
|
|
st_table *const old_tab = r->sync.ports;
|
|
bool inserted;
|
|
|
|
RACTOR_LOCK(r);
|
|
{
|
|
inserted = st_insert_no_rebuild(old_tab, id, (st_data_t)rq) >= 0;
|
|
}
|
|
RACTOR_UNLOCK(r);
|
|
|
|
if (!inserted) {
|
|
// The table is full. Rebuild it outside of the ractor lock (mutators
|
|
// are serialized by the per-ractor GVL) and swap it under the lock
|
|
// to exclude the readers (other ractors).
|
|
st_table *new_tab;
|
|
|
|
// Those allocations can run a global GC, whose reap frees dead ports and
|
|
// drops them from old_tab; a copy taken across one would republish the
|
|
// freed queues. Only the owner inserts into its own table, so a changed
|
|
// count means a reap ran. Check after each allocation: st_copy fills the
|
|
// header before it allocates the entries, so a reap in between leaves the
|
|
// copy counting rows it does not have, which st_insert must not be given.
|
|
while (1) {
|
|
const st_index_t entries = st_table_size(old_tab);
|
|
|
|
new_tab = st_copy(old_tab);
|
|
|
|
if (st_table_size(old_tab) == entries) {
|
|
st_insert(new_tab, id, (st_data_t)rq);
|
|
|
|
if (st_table_size(old_tab) == entries) break;
|
|
}
|
|
|
|
st_free_table(new_tab);
|
|
}
|
|
|
|
RACTOR_LOCK(r);
|
|
{
|
|
VM_ASSERT(r->sync.ports == old_tab);
|
|
r->sync.ports = new_tab;
|
|
}
|
|
RACTOR_UNLOCK(r);
|
|
|
|
st_free_table(old_tab);
|
|
}
|
|
}
|
|
|
|
static void
|
|
ractor_delete_port_locked(rb_ractor_t *cr, st_data_t id)
|
|
{
|
|
ASSERT_ractor_locking(cr);
|
|
|
|
RUBY_DEBUG_LOG("id:%u", (unsigned int)id);
|
|
|
|
struct ractor_queue *rq;
|
|
|
|
if (st_delete(cr->sync.ports, &id, (st_data_t *)&rq)) {
|
|
ractor_queue_free(rq);
|
|
}
|
|
else {
|
|
VM_ASSERT(0);
|
|
}
|
|
}
|
|
|
|
static void
|
|
ractor_delete_port(rb_ractor_t *cr, st_data_t id, bool locked)
|
|
{
|
|
if (locked) {
|
|
ractor_delete_port_locked(cr, id);
|
|
}
|
|
else {
|
|
RACTOR_LOCK_SELF(cr);
|
|
{
|
|
ractor_delete_port_locked(cr, id);
|
|
}
|
|
RACTOR_UNLOCK_SELF(cr);
|
|
}
|
|
}
|
|
|
|
static const struct ractor_port *
|
|
ractor_default_port(rb_ractor_t *r)
|
|
{
|
|
return RACTOR_PORT_PTR(r->sync.default_port_value);
|
|
}
|
|
|
|
static VALUE
|
|
ractor_default_port_value(rb_ractor_t *r)
|
|
{
|
|
return r->sync.default_port_value;
|
|
}
|
|
|
|
static bool
|
|
ractor_closed_port_p(rb_execution_context_t *ec, rb_ractor_t *r, const struct ractor_port *rp)
|
|
{
|
|
VM_ASSERT(rb_ec_ractor_ptr(ec) == rp->r ? 1 : (ASSERT_ractor_locking(rp->r), 1));
|
|
|
|
const struct ractor_queue *rq;
|
|
|
|
if (rp->r->sync.ports && st_lookup(rp->r->sync.ports, ractor_port_id(rp), (st_data_t *)&rq)) {
|
|
return rq->closed;
|
|
}
|
|
else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
static void ractor_deliver_incoming_messages(rb_execution_context_t *ec, rb_ractor_t *cr);
|
|
static bool ractor_queue_empty_p(rb_ractor_t *r, const struct ractor_queue *rq);
|
|
|
|
static bool ractor_wakeup_all(rb_ractor_t *r, enum ractor_wakeup_status wakeup_status);
|
|
|
|
static bool
|
|
ractor_close_port(rb_execution_context_t *ec, rb_ractor_t *cr, const struct ractor_port *rp)
|
|
{
|
|
VM_ASSERT(cr == rp->r);
|
|
struct ractor_queue *rq = NULL;
|
|
bool closed_now = false;
|
|
|
|
RACTOR_LOCK_SELF(cr);
|
|
{
|
|
ractor_deliver_incoming_messages(ec, cr); // check incoming messages
|
|
|
|
if (st_lookup(rp->r->sync.ports, ractor_port_id(rp), (st_data_t *)&rq)) {
|
|
closed_now = ractor_queue_close(rq);
|
|
|
|
if (ractor_queue_empty_p(cr, rq)) {
|
|
// delete from the table
|
|
ractor_delete_port(cr, ractor_port_id(rp), true);
|
|
}
|
|
|
|
// TODO: free rq
|
|
}
|
|
}
|
|
RACTOR_UNLOCK_SELF(cr);
|
|
|
|
if (closed_now) {
|
|
// Only when this call closed it: waking the Ractor is not free to the
|
|
// other waiters, and a re-close of a closed port is news to nobody.
|
|
ractor_wakeup_all(cr, wakeup_by_close);
|
|
}
|
|
|
|
return rq != NULL;
|
|
}
|
|
|
|
/* A port is the only way to receive from its queue, so a queue whose port is gone is
|
|
* unreachable -- but the table is keyed by id, so no sweep finds it. Mark and sweep the
|
|
* table itself: ractor_port_mark sets the flag, this clears it for the next cycle. */
|
|
static int
|
|
ractor_reap_dead_ports_i(st_data_t port_id, st_data_t val, st_data_t dat)
|
|
{
|
|
struct ractor_queue *rq = (struct ractor_queue *)val;
|
|
|
|
if (rq->alive) {
|
|
rq->alive = false;
|
|
return ST_CONTINUE;
|
|
}
|
|
else {
|
|
ractor_queue_free(rq);
|
|
return ST_DELETE;
|
|
}
|
|
}
|
|
|
|
/* A message is only moved from recv_queue to its port queue when the owner receives or
|
|
* closes, so one addressed to a port that died first is left here, out of the sweep
|
|
* above. Its port is gone from the table by now: drop it. */
|
|
static void
|
|
ractor_reap_undeliverable_messages(rb_ractor_t *r)
|
|
{
|
|
struct ractor_queue *recv_q = r->sync.recv_queue;
|
|
if (recv_q == NULL) return;
|
|
|
|
struct ractor_basket *b, *nxt;
|
|
ccan_list_for_each_safe(&recv_q->set, b, nxt, node) {
|
|
if (!st_lookup(r->sync.ports, b->port_id, NULL)) {
|
|
ccan_list_del_init(&b->node);
|
|
ractor_basket_free(b);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
rb_ractor_reap_dead_ports(rb_ractor_t *r)
|
|
{
|
|
/* No sync lock here: the caller's gate is what keeps foreign senders out. */
|
|
VM_ASSERT(rb_gc_single_objspace_p() || rb_gc_during_global_gc_p());
|
|
|
|
if (r->sync.ports) {
|
|
st_foreach(r->sync.ports, ractor_reap_dead_ports_i, 0);
|
|
ractor_reap_undeliverable_messages(r);
|
|
}
|
|
}
|
|
|
|
static int
|
|
ractor_free_all_ports_i(st_data_t port_id, st_data_t val, st_data_t dat)
|
|
{
|
|
struct ractor_queue *rq = (struct ractor_queue *)val;
|
|
// rb_ractor_t *cr = (rb_ractor_t *)dat;
|
|
|
|
ractor_queue_free(rq);
|
|
return ST_CONTINUE;
|
|
}
|
|
|
|
static void
|
|
ractor_free_all_ports(rb_ractor_t *cr)
|
|
{
|
|
if (cr->sync.ports) {
|
|
st_foreach(cr->sync.ports, ractor_free_all_ports_i, (st_data_t)cr);
|
|
st_free_table(cr->sync.ports);
|
|
cr->sync.ports = NULL;
|
|
}
|
|
|
|
if (cr->sync.recv_queue) {
|
|
ractor_queue_free(cr->sync.recv_queue);
|
|
cr->sync.recv_queue = NULL;
|
|
}
|
|
}
|
|
|
|
#if defined(HAVE_WORKING_FORK)
|
|
static void
|
|
ractor_sync_terminate_atfork(rb_vm_t *vm, rb_ractor_t *r)
|
|
{
|
|
ractor_free_all_ports(r);
|
|
r->sync.legacy = Qnil;
|
|
}
|
|
#endif
|
|
|
|
// Ractor#monitor
|
|
|
|
struct ractor_monitor {
|
|
struct ractor_port port;
|
|
struct ccan_list_node node;
|
|
};
|
|
|
|
/* Mark the Ractors monitoring r. ractor_notify_exit sends the exit token through each
|
|
* entry's port, so the monitoring Ractor's struct must outlive r, and its wrapper is
|
|
* what keeps it alive. */
|
|
static void
|
|
ractor_mark_monitors(const struct rb_gc_mark_ctx *ctx, rb_ractor_t *r)
|
|
{
|
|
const struct ractor_monitor *rm;
|
|
ccan_list_for_each(&r->sync.monitors, rm, node) {
|
|
rb_gc_mark_ctx(ctx, rm->port.r->pub.self);
|
|
}
|
|
}
|
|
|
|
/* Paired with the ractor it is about when it is received, so that many ractors
|
|
* can report to one port. */
|
|
static VALUE
|
|
ractor_exit_token(const rb_ractor_t *r)
|
|
{
|
|
if (r->sync.legacy_exc) {
|
|
RUBY_DEBUG_LOG("aborted");
|
|
return ID2SYM(idAborted);
|
|
}
|
|
else {
|
|
RUBY_DEBUG_LOG("exited");
|
|
return ID2SYM(idExited);
|
|
}
|
|
}
|
|
|
|
static VALUE
|
|
ractor_monitor(rb_execution_context_t *ec, VALUE self, VALUE port)
|
|
{
|
|
rb_ractor_t *r = RACTOR_PTR(self);
|
|
bool terminated = false;
|
|
const struct ractor_port *rp = ractor_port_ptr_check(port);
|
|
struct ractor_monitor *rm = ALLOC(struct ractor_monitor);
|
|
rm->port = *rp; // copy port information
|
|
|
|
RACTOR_LOCK(r);
|
|
{
|
|
if (UNDEF_P(r->sync.legacy)) { // not terminated
|
|
RUBY_DEBUG_LOG("OK/r:%u -> port:%u@r%u", (unsigned int)rb_ractor_id(r), (unsigned int)ractor_port_id(&rm->port), (unsigned int)rb_ractor_id(rm->port.r));
|
|
ccan_list_add_tail(&r->sync.monitors, &rm->node);
|
|
}
|
|
else {
|
|
RUBY_DEBUG_LOG("NG/r:%u -> port:%u@r%u", (unsigned int)rb_ractor_id(r), (unsigned int)ractor_port_id(&rm->port), (unsigned int)rb_ractor_id(rm->port.r));
|
|
terminated = true;
|
|
}
|
|
}
|
|
RACTOR_UNLOCK(r);
|
|
|
|
if (terminated) {
|
|
SIZED_FREE(rm);
|
|
ractor_send_basket(ec, rp, ractor_basket_new_exit(self, ractor_exit_token(r)), false);
|
|
}
|
|
/* rp points into port, which is embedded and may be referenced only from here */
|
|
RB_GC_GUARD(port);
|
|
|
|
return terminated ? Qfalse : Qtrue;
|
|
}
|
|
|
|
static VALUE
|
|
ractor_alive_p(rb_execution_context_t *ec, VALUE self)
|
|
{
|
|
rb_ractor_t *r = RACTOR_PTR(self);
|
|
bool alive;
|
|
|
|
RACTOR_LOCK(r);
|
|
{
|
|
alive = UNDEF_P(r->sync.legacy);
|
|
}
|
|
RACTOR_UNLOCK(r);
|
|
|
|
// rb_ractor_terminate_atfork terminates ractors without setting legacy
|
|
return RBOOL(alive && !rb_ractor_status_p(r, ractor_terminated));
|
|
}
|
|
|
|
static VALUE
|
|
ractor_unmonitor(rb_execution_context_t *ec, VALUE self, VALUE port)
|
|
{
|
|
rb_ractor_t *r = RACTOR_PTR(self);
|
|
const struct ractor_port *rp = ractor_port_ptr_check(port);
|
|
|
|
RACTOR_LOCK(r);
|
|
{
|
|
if (UNDEF_P(r->sync.legacy)) { // not terminated
|
|
struct ractor_monitor *rm, *nxt;
|
|
|
|
ccan_list_for_each_safe(&r->sync.monitors, rm, nxt, node) {
|
|
if (rm->port.r == rp->r && ractor_port_id(&rm->port) == ractor_port_id(rp)) {
|
|
RUBY_DEBUG_LOG("r:%u -> port:%u@r%u",
|
|
(unsigned int)rb_ractor_id(r),
|
|
(unsigned int)ractor_port_id(&rm->port),
|
|
(unsigned int)rb_ractor_id(rm->port.r));
|
|
ccan_list_del(&rm->node);
|
|
SIZED_FREE(rm);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
RACTOR_UNLOCK(r);
|
|
|
|
return self;
|
|
}
|
|
|
|
static void
|
|
ractor_notify_exit(rb_execution_context_t *ec, rb_ractor_t *cr, VALUE legacy, bool exc)
|
|
{
|
|
RUBY_DEBUG_LOG("exc:%d", exc);
|
|
VM_ASSERT(!UNDEF_P(legacy));
|
|
VM_ASSERT(cr->sync.legacy == Qundef);
|
|
|
|
RACTOR_LOCK_SELF(cr);
|
|
{
|
|
ractor_free_all_ports(cr);
|
|
|
|
cr->sync.legacy = legacy;
|
|
cr->sync.legacy_exc = exc;
|
|
}
|
|
RACTOR_UNLOCK_SELF(cr);
|
|
|
|
}
|
|
|
|
/* Sent after the dying thread's post-mortem collection: waking a joiner any earlier makes
|
|
* ractor_value spin for the whole of that collection. */
|
|
static void
|
|
ractor_send_exit_tokens(rb_execution_context_t *ec, rb_ractor_t *cr)
|
|
{
|
|
VALUE token = ractor_exit_token(cr);
|
|
struct ractor_monitor *rm, *nxt;
|
|
|
|
ccan_list_for_each_safe(&cr->sync.monitors, rm, nxt, node)
|
|
{
|
|
RUBY_DEBUG_LOG("port:%u@r%u", (unsigned int)ractor_port_id(&rm->port), (unsigned int)rb_ractor_id(rm->port.r));
|
|
|
|
ractor_send_basket(ec, &rm->port, ractor_basket_new_exit(cr->pub.self, token), false);
|
|
|
|
ccan_list_del(&rm->node);
|
|
SIZED_FREE(rm);
|
|
}
|
|
|
|
VM_ASSERT(ccan_list_empty(&cr->sync.monitors));
|
|
}
|
|
|
|
// ractor-internal - initialize, mark, free, memsize
|
|
|
|
static int
|
|
ractor_mark_ports_i(st_data_t key, st_data_t val, st_data_t data)
|
|
{
|
|
// id -> ractor_queue
|
|
const struct ractor_queue *rq = (struct ractor_queue *)val;
|
|
ractor_queue_mark((const struct rb_gc_mark_ctx *)data, rq);
|
|
return ST_CONTINUE;
|
|
}
|
|
|
|
static void
|
|
ractor_sync_mark(const struct rb_gc_mark_ctx *ctx, rb_ractor_t *r)
|
|
{
|
|
/* The owner rewrites the queues, the port table and the monitor list under its sync
|
|
* lock, so only the owner itself or the stopped world may walk them. */
|
|
const bool world_stopped = rb_gc_during_global_gc_p();
|
|
VM_ASSERT(world_stopped || r == rb_current_ractor_raw(false));
|
|
|
|
rb_gc_mark_ctx(ctx, r->sync.default_port_value);
|
|
|
|
/* Until the value is absorbed this is its only reliable root (Qundef while the
|
|
* Ractor still runs); after Ractor#value returns it, the Ruby side roots it. */
|
|
rb_gc_mark_ctx(ctx, r->sync.legacy);
|
|
|
|
/* ractor_sync_init builds the rest, and a root scan reaches the main Ractor before
|
|
* that: ports is what tells the two apart (the lock and the list heads are still
|
|
* zeroed, and walking those crashes). Lock out foreign senders while walking them
|
|
* (self-lock: not recursive, and a held Ractor lock disables malloc-GC, so no GC
|
|
* nests); a stopped world needs no lock. */
|
|
if (r->sync.ports) {
|
|
if (!world_stopped) RACTOR_LOCK_SELF(r);
|
|
{
|
|
ractor_queue_mark(ctx, r->sync.recv_queue);
|
|
st_foreach(r->sync.ports, ractor_mark_ports_i, (st_data_t)ctx);
|
|
ractor_mark_monitors(ctx, r);
|
|
}
|
|
if (!world_stopped) RACTOR_UNLOCK_SELF(r);
|
|
|
|
/* The baskets on no queue: one being built to send, one being materialized.
|
|
* Walked in every collection, like the queues. What they hold is shareable, but
|
|
* "only a global GC frees a shareable" does not hold: pinned_roots_mark, which
|
|
* roots a shareable from its page bit, is skipped once the process is back to a
|
|
* single Ractor (rb_gc_single_objspace_p), and then an ordinary local GC frees
|
|
* one that nothing else names. A payload in flight is named by its basket and
|
|
* nothing else, so this list has to be a root whenever the queues are. No sync
|
|
* lock, though: only the owner touches it (the lock above guards the queues,
|
|
* which a foreign sender writes). */
|
|
ractor_mark_off_queue_baskets(ctx, r);
|
|
}
|
|
}
|
|
|
|
static int
|
|
ractor_sync_free_ports_i(st_data_t _key, st_data_t val, st_data_t _args)
|
|
{
|
|
struct ractor_queue *queue = (struct ractor_queue *)val;
|
|
|
|
ractor_queue_free(queue);
|
|
|
|
return ST_CONTINUE;
|
|
}
|
|
|
|
static void
|
|
ractor_sync_free(rb_ractor_t *r)
|
|
{
|
|
if (r->sync.recv_queue) {
|
|
ractor_queue_free(r->sync.recv_queue);
|
|
}
|
|
|
|
// maybe NULL
|
|
if (r->sync.ports) {
|
|
st_foreach(r->sync.ports, ractor_sync_free_ports_i, 0);
|
|
st_free_table(r->sync.ports);
|
|
r->sync.ports = NULL;
|
|
}
|
|
}
|
|
|
|
static size_t
|
|
ractor_sync_memsize(const rb_ractor_t *r)
|
|
{
|
|
if (r->sync.ports) {
|
|
return st_memsize(r->sync.ports);
|
|
}
|
|
else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
static void
|
|
ractor_sync_init(rb_ractor_t *r)
|
|
{
|
|
// lock
|
|
rb_native_mutex_initialize(&r->sync.lock);
|
|
|
|
// monitors
|
|
ccan_list_head_init(&r->sync.off_queue_baskets);
|
|
ccan_list_head_init(&r->sync.monitors);
|
|
|
|
// waiters
|
|
ccan_list_head_init(&r->sync.waiters);
|
|
|
|
// receiving queue
|
|
r->sync.recv_queue = ractor_queue_new();
|
|
|
|
// ports
|
|
r->sync.ports = st_init_numtable();
|
|
/* ractor_setup_default_port creates it only after the Ractor joins
|
|
* vm->ractor.set, so a global GC cannot free the rootless port in between. */
|
|
r->sync.default_port_value = Qfalse;
|
|
|
|
// legacy
|
|
r->sync.legacy = Qundef;
|
|
|
|
// no receive is rebuilding a payload yet
|
|
|
|
}
|
|
|
|
/* Create the default port. Call only after the Ractor joined vm->ractor.set, so the
|
|
* root scan can mark the shareable port from creation onwards. */
|
|
void
|
|
rb_ractor_setup_default_port(rb_ractor_t *r)
|
|
{
|
|
VM_ASSERT(r->sync.default_port_value == Qfalse);
|
|
r->sync.default_port_value = ractor_port_new(r);
|
|
RB_OBJ_SET_SHAREABLE(r->sync.default_port_value);
|
|
}
|
|
|
|
// Ractor#value
|
|
|
|
static rb_ractor_t *
|
|
ractor_set_successor_once(rb_ractor_t *r, rb_ractor_t *cr)
|
|
{
|
|
if (r->sync.successor == NULL) {
|
|
rb_ractor_t *successor = ATOMIC_PTR_CAS(r->sync.successor, NULL, cr);
|
|
return successor == NULL ? cr : successor;
|
|
}
|
|
|
|
return r->sync.successor;
|
|
}
|
|
|
|
static VALUE
|
|
ractor_make_remote_exception(VALUE cause, VALUE sender)
|
|
{
|
|
VALUE err = rb_exc_new_cstr(rb_eRactorRemoteError, "thrown by remote Ractor.");
|
|
rb_ivar_set(err, rb_intern("@ractor"), sender);
|
|
rb_ec_setup_exception(NULL, err, cause);
|
|
return err;
|
|
}
|
|
|
|
static VALUE
|
|
ractor_value(rb_execution_context_t *ec, VALUE self)
|
|
{
|
|
rb_ractor_t *cr = rb_ec_ractor_ptr(ec);
|
|
rb_ractor_t *r = RACTOR_PTR(self);
|
|
rb_ractor_t *sr = ractor_set_successor_once(r, cr);
|
|
|
|
if (sr == cr) {
|
|
if (r->sync.legacy_taken) {
|
|
rb_raise(rb_eRactorError, "The value was already taken");
|
|
}
|
|
|
|
/* The value is returned by reference: inherit the dead Ractor's objspace first,
|
|
* making it our own object (containment without a copy). Wait for
|
|
* ractor_terminated: a monitor-port wakeup arrives before the dying thread
|
|
* finishes teardown (vm_remove_ractor still touches the objspace). */
|
|
while (!rb_ractor_status_p(r, ractor_terminated)) {
|
|
rb_thread_schedule();
|
|
}
|
|
|
|
/* The wait above yields the GVL, so another thread of this Ractor can take the
|
|
* value first: re-check. */
|
|
if (r->sync.legacy_taken) {
|
|
rb_raise(rb_eRactorError, "The value was already taken");
|
|
}
|
|
|
|
/* Move r's rb_gc_register_mark_object pins to the joiner before the merge
|
|
* below sweeps r's objspace, or the objects pinned there lose their root. */
|
|
rb_ractor_absorb_registered_marks(GET_RACTOR(), r);
|
|
rb_ractor_absorb_registered_addrs_without_gc(GET_RACTOR(), r);
|
|
|
|
rb_gc_objspace_absorb_into_current(&r->objspace);
|
|
|
|
/* Keep legacy alive in a C local until it is returned: after the absorb only
|
|
* the C struct reaches it, so let the conservative machine-stack mark find it. */
|
|
volatile VALUE legacy_keep = r->sync.legacy;
|
|
|
|
/* A dead Ractor's local storage is unreachable from Ruby (Ractor#[] only works
|
|
* from inside), so let the values die and keep ractor_mark and ractor_free from
|
|
* walking a stale table later. */
|
|
ractor_local_storage_free(r);
|
|
r->local_storage = NULL;
|
|
r->idkey_local_storage = NULL;
|
|
|
|
/* The value is returned to the caller and rooted from Ruby afterwards. Drop it
|
|
* from the C struct: keeping it would leave a C-only reference into the
|
|
* successor's objspace, needing marking and a pin against compaction. */
|
|
VALUE legacy = r->sync.legacy;
|
|
r->sync.legacy = Qnil;
|
|
r->sync.legacy_taken = true;
|
|
RB_GC_GUARD(legacy_keep);
|
|
|
|
if (r->sync.legacy_exc) {
|
|
rb_exc_raise(ractor_make_remote_exception(legacy, self));
|
|
}
|
|
return legacy;
|
|
}
|
|
else {
|
|
rb_raise(rb_eRactorError, "Only the successor ractor can take a value");
|
|
}
|
|
}
|
|
|
|
static VALUE
|
|
ractor_marshal_dump_body(VALUE obj)
|
|
{
|
|
return rb_marshal_dump(obj, Qnil);
|
|
}
|
|
|
|
static VALUE
|
|
ractor_marshal_dump_rescue(VALUE obj, VALUE errinfo)
|
|
{
|
|
rb_raise(rb_eRactorError, "can not copy %"PRIsVALUE" object.", rb_class_of(obj));
|
|
UNREACHABLE_RETURN(Qnil);
|
|
}
|
|
|
|
static VALUE
|
|
ractor_prepare_payload(rb_execution_context_t *ec, VALUE obj, enum ractor_basket_type *ptype,
|
|
struct rb_ractor_courier **pcourier)
|
|
{
|
|
switch (*ptype) {
|
|
case basket_type_ref:
|
|
return obj;
|
|
default:
|
|
if (rb_ractor_shareable_p(obj)) {
|
|
*ptype = basket_type_ref;
|
|
return obj;
|
|
}
|
|
else {
|
|
/* Snapshot the object on the sender side without calling the user-visible
|
|
* #clone. The courier is off-heap, so an in-flight payload is never a GC
|
|
* object and needs no pin: nothing of the sender's heap stays alive while
|
|
* the message waits. */
|
|
*ptype = basket_type_copy;
|
|
if (rb_ractor_courier_build_copy(obj, pcourier) == NULL) {
|
|
rb_raise(rb_eRactorError, "can not copy %"PRIsVALUE" object.", rb_class_of(obj));
|
|
}
|
|
return Qundef;
|
|
}
|
|
}
|
|
}
|
|
|
|
#if RBIMPL_COMPILER_IS(GCC) && defined(__OPTIMIZE__)
|
|
/* GCC produces false-positive -Wclobbered warnings after inlining
|
|
* this function into ractor_basket_new(). */
|
|
NOINLINE(static void ractor_basket_build_payload(rb_execution_context_t *ec, struct ractor_basket *b, VALUE obj, enum ractor_basket_type type, bool exc));
|
|
#endif
|
|
static void
|
|
ractor_basket_build_payload(rb_execution_context_t *ec, struct ractor_basket *b, VALUE obj,
|
|
enum ractor_basket_type type, bool exc)
|
|
{
|
|
b->exception = exc;
|
|
|
|
b->p.courier = NULL;
|
|
b->has_courier = true;
|
|
|
|
if (type == basket_type_move) {
|
|
/* Serialize the graph into an off-heap courier; the sources become
|
|
* RactorMovedObject. While in flight there is no GC object left for the
|
|
* sender's GC to mark, sweep or move. The build publishes the courier into
|
|
* the basket as soon as it exists. */
|
|
rb_ractor_courier_build_move(obj, &b->p.courier);
|
|
b->type = type;
|
|
}
|
|
else {
|
|
VALUE v = ractor_prepare_payload(ec, obj, &type, &b->p.courier);
|
|
if (b->p.courier == NULL) {
|
|
b->has_courier = false;
|
|
b->p.v = v;
|
|
}
|
|
b->type = type;
|
|
}
|
|
}
|
|
|
|
static struct ractor_basket *
|
|
ractor_basket_new(rb_execution_context_t *ec, VALUE obj, enum ractor_basket_type type, bool exc)
|
|
{
|
|
rb_ractor_t *cr = rb_ec_ractor_ptr(ec);
|
|
/* Allocate and list the basket before anything is built into it: from here the
|
|
* courier it is about to hold is rooted by this Ractor's in-flight list, even
|
|
* half-built, and every raise below frees it through one path. */
|
|
struct ractor_basket *b = ractor_basket_alloc();
|
|
ractor_off_queue_add(cr, b);
|
|
|
|
enum ruby_tag_type state;
|
|
EC_PUSH_TAG(ec);
|
|
if ((state = EC_EXEC_TAG()) == TAG_NONE) {
|
|
ractor_basket_build_payload(ec, b, obj, type, exc);
|
|
}
|
|
EC_POP_TAG();
|
|
if (state != TAG_NONE) {
|
|
ractor_basket_free(b); /* leaves the list and frees a courier already built */
|
|
EC_JUMP_TAG(ec, state);
|
|
}
|
|
|
|
return b;
|
|
}
|
|
|
|
static VALUE
|
|
ractor_basket_value(struct ractor_basket *b)
|
|
{
|
|
switch (b->type) {
|
|
case basket_type_ref:
|
|
break;
|
|
case basket_type_exit:
|
|
/* Allocated here, in the receiving ractor: a copy, not a shared object. */
|
|
return rb_ary_new_from_args(2, b->sender, b->p.v);
|
|
case basket_type_copy:
|
|
/* An off-heap copy courier rebuilds exactly like a move one; only the sources
|
|
* differ (still alive here, already shells there). */
|
|
case basket_type_move: {
|
|
/* Rebuild the moved graph from the off-heap courier into this Ractor's
|
|
* objspace. The sources are already RactorMovedObject (set when the courier
|
|
* was built), so move's snapshot semantics hold. The courier is xmalloc'd
|
|
* rather than a GC object, so the sender's concurrent local GC never touches
|
|
* it; the shareable VALUEs it carries are marked through this basket, which is
|
|
* on this Ractor's off-queue list until it is freed.
|
|
*
|
|
* Rebuilding can raise here too (rb_hash_aset on a moved key with a custom
|
|
* #hash runs user code, and an async interrupt can arrive). On a raise the
|
|
* courier is still owned by the basket, whose teardown frees it. */
|
|
rb_execution_context_t *ec = rb_current_ec_noinline();
|
|
struct rb_ractor_courier *courier = b->p.courier;
|
|
/* Keep the materialized graph on the machine stack (result): it is the only
|
|
* root until it reaches the caller. courier_free below runs a long loop, and
|
|
* only the malloc'd basket's p.v holding it would give a concurrent global GC a
|
|
* wide window. */
|
|
VALUE result = Qundef;
|
|
enum ruby_tag_type state;
|
|
EC_PUSH_TAG(ec);
|
|
if ((state = EC_EXEC_TAG()) == TAG_NONE) {
|
|
result = rb_ractor_courier_materialize(courier);
|
|
}
|
|
EC_POP_TAG();
|
|
if (state != TAG_NONE) {
|
|
/* An unconsumed courier stays in b->p.courier; basket_free frees it. */
|
|
ractor_basket_free(b);
|
|
EC_JUMP_TAG(ec, state);
|
|
}
|
|
rb_ractor_courier_free(courier);
|
|
b->p.courier = NULL;
|
|
b->has_courier = false;
|
|
b->p.v = result;
|
|
RB_GC_GUARD(result);
|
|
break;
|
|
}
|
|
default:
|
|
VM_ASSERT(0); // unreachable
|
|
}
|
|
|
|
VM_ASSERT(!b->has_courier);
|
|
VM_ASSERT(!RB_TYPE_P(b->p.v, T_NONE));
|
|
return b->p.v;
|
|
}
|
|
|
|
static VALUE
|
|
ractor_basket_accept(struct ractor_basket *b)
|
|
{
|
|
VALUE v = ractor_basket_value(b);
|
|
|
|
if (b->exception) {
|
|
VALUE err = ractor_make_remote_exception(v, b->sender);
|
|
ractor_basket_free(b);
|
|
rb_exc_raise(err);
|
|
}
|
|
|
|
ractor_basket_free(b);
|
|
return v;
|
|
}
|
|
|
|
// Ractor blocking by receive
|
|
|
|
#if VM_CHECK_MODE > 0
|
|
static bool
|
|
ractor_waiter_included(rb_ractor_t *cr, rb_thread_t *th)
|
|
{
|
|
ASSERT_ractor_locking(cr);
|
|
|
|
struct ractor_waiter *w;
|
|
|
|
ccan_list_for_each(&cr->sync.waiters, w, node) {
|
|
if (w->th == th) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
#if USE_RUBY_DEBUG_LOG
|
|
|
|
static const char *
|
|
wakeup_status_str(enum ractor_wakeup_status wakeup_status)
|
|
{
|
|
switch (wakeup_status) {
|
|
case wakeup_none: return "none";
|
|
case wakeup_by_send: return "by_send";
|
|
case wakeup_by_interrupt: return "by_interrupt";
|
|
case wakeup_by_close: return "by_close";
|
|
}
|
|
rb_bug("unreachable");
|
|
}
|
|
|
|
static const char *
|
|
basket_type_name(enum ractor_basket_type type)
|
|
{
|
|
switch (type) {
|
|
case basket_type_none: return "none";
|
|
case basket_type_ref: return "ref";
|
|
case basket_type_copy: return "copy";
|
|
case basket_type_move: return "move";
|
|
case basket_type_exit: return "exit";
|
|
}
|
|
VM_ASSERT(0);
|
|
return NULL;
|
|
}
|
|
|
|
#endif // USE_RUBY_DEBUG_LOG
|
|
|
|
static bool
|
|
ractor_wakeup_all_locked(rb_ractor_t *r, enum ractor_wakeup_status wakeup_status)
|
|
{
|
|
ASSERT_ractor_locking(r);
|
|
|
|
RUBY_DEBUG_LOG("r:%"PRI_SERIALT_PREFIX"u wakeup:%s", rb_ractor_id(r), wakeup_status_str(wakeup_status));
|
|
|
|
bool wakeup_p = false;
|
|
|
|
while (1) {
|
|
struct ractor_waiter *waiter = ccan_list_pop(&r->sync.waiters, struct ractor_waiter, node);
|
|
|
|
if (waiter) {
|
|
VM_ASSERT(waiter->wakeup_status == wakeup_none);
|
|
|
|
waiter->wakeup_status = wakeup_status;
|
|
rb_ractor_sched_wakeup(r, waiter->th);
|
|
|
|
wakeup_p = true;
|
|
}
|
|
else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return wakeup_p;
|
|
}
|
|
|
|
static bool
|
|
ractor_wakeup_all(rb_ractor_t *r, enum ractor_wakeup_status wakeup_status)
|
|
{
|
|
ASSERT_ractor_unlocking(r);
|
|
|
|
bool wakeup_p;
|
|
|
|
RACTOR_LOCK(r);
|
|
{
|
|
wakeup_p = ractor_wakeup_all_locked(r, wakeup_status);
|
|
}
|
|
RACTOR_UNLOCK(r);
|
|
|
|
return wakeup_p;
|
|
}
|
|
|
|
static void
|
|
ubf_ractor_wait(void *ptr)
|
|
{
|
|
struct ractor_waiter *waiter = (struct ractor_waiter *)ptr;
|
|
|
|
rb_thread_t *th = waiter->th;
|
|
rb_ractor_t *r = th->ractor;
|
|
rb_atomic_t event_serial = waiter->event_serial;
|
|
|
|
// clear ubf and nobody can kick UBF
|
|
th->unblock.func = NULL;
|
|
th->unblock.arg = NULL;
|
|
|
|
rb_native_mutex_unlock(&th->interrupt_lock);
|
|
{
|
|
RACTOR_LOCK(r);
|
|
{
|
|
if (RUBY_ATOMIC_LOAD(th->unblock.event_serial) == event_serial && waiter->wakeup_status == wakeup_none) {
|
|
RUBY_DEBUG_LOG("waiter:%p", (void *)waiter);
|
|
|
|
waiter->wakeup_status = wakeup_by_interrupt;
|
|
ccan_list_del(&waiter->node);
|
|
|
|
rb_ractor_sched_wakeup(r, waiter->th);
|
|
}
|
|
}
|
|
RACTOR_UNLOCK(r);
|
|
}
|
|
rb_native_mutex_lock(&th->interrupt_lock);
|
|
}
|
|
|
|
// Waits for an event on cr. `end` is an absolute deadline, NULL to wait forever.
|
|
static enum ractor_wakeup_status
|
|
ractor_wait(rb_execution_context_t *ec, rb_ractor_t *cr, const rb_hrtime_t *end)
|
|
{
|
|
rb_thread_t *th = rb_ec_thread_ptr(ec);
|
|
|
|
struct ractor_waiter waiter = {
|
|
.wakeup_status = wakeup_none,
|
|
.th = th,
|
|
.end = end,
|
|
};
|
|
|
|
RUBY_DEBUG_LOG("wait%s", "");
|
|
|
|
ASSERT_ractor_locking(cr);
|
|
|
|
VM_ASSERT(GET_RACTOR() == cr);
|
|
VM_ASSERT(!ractor_waiter_included(cr, th));
|
|
|
|
ccan_list_add_tail(&cr->sync.waiters, &waiter.node);
|
|
|
|
// resume another ready thread and wait for an event
|
|
rb_ractor_sched_wait(ec, cr, ubf_ractor_wait, &waiter);
|
|
|
|
if (waiter.wakeup_status == wakeup_none) {
|
|
ccan_list_del(&waiter.node);
|
|
}
|
|
|
|
RUBY_DEBUG_LOG("wakeup_status:%s", wakeup_status_str(waiter.wakeup_status));
|
|
|
|
RACTOR_UNLOCK_SELF(cr);
|
|
{
|
|
rb_ec_check_ints(ec);
|
|
}
|
|
RACTOR_LOCK_SELF(cr);
|
|
|
|
VM_ASSERT(!ractor_waiter_included(cr, th));
|
|
return waiter.wakeup_status;
|
|
}
|
|
|
|
static void
|
|
ractor_deliver_incoming_messages(rb_execution_context_t *ec, rb_ractor_t *cr)
|
|
{
|
|
ASSERT_ractor_locking(cr);
|
|
struct ractor_queue *recv_q = cr->sync.recv_queue;
|
|
|
|
struct ractor_basket *b;
|
|
while ((b = ractor_queue_deq(cr, recv_q)) != NULL) {
|
|
ractor_queue_enq(cr, ractor_get_queue(cr, b->port_id, true), b);
|
|
}
|
|
}
|
|
|
|
static bool
|
|
ractor_check_received(rb_ractor_t *cr, struct ractor_queue *messages)
|
|
{
|
|
struct ractor_queue *received_queue = cr->sync.recv_queue;
|
|
bool received = false;
|
|
|
|
ASSERT_ractor_locking(cr);
|
|
|
|
if (ractor_queue_empty_p(cr, received_queue)) {
|
|
RUBY_DEBUG_LOG("empty");
|
|
}
|
|
else {
|
|
received = true;
|
|
|
|
// messages <- incoming
|
|
ractor_queue_init(messages);
|
|
ractor_queue_move(messages, received_queue);
|
|
}
|
|
|
|
VM_ASSERT(ractor_queue_empty_p(cr, received_queue));
|
|
|
|
RUBY_DEBUG_LOG("received:%d", received);
|
|
return received;
|
|
}
|
|
|
|
// Returns false if the deadline `end` passed with nothing to deliver. Incoming
|
|
// messages are delivered even then, so the caller retries its queue once more.
|
|
// A wait can end on a wakeup meant for another port, so the caller keeps the
|
|
// deadline: a stream of them must not hold a timed receive past its time.
|
|
static bool
|
|
ractor_deadline_passed_p(const rb_hrtime_t *end)
|
|
{
|
|
return end != NULL && rb_hrtime_now() >= *end;
|
|
}
|
|
|
|
static bool
|
|
ractor_wait_receive(rb_execution_context_t *ec, rb_ractor_t *cr, const rb_hrtime_t *end)
|
|
{
|
|
struct ractor_queue messages;
|
|
bool deliverred = false;
|
|
bool timedout = false;
|
|
|
|
RACTOR_LOCK_SELF(cr);
|
|
{
|
|
if (ractor_check_received(cr, &messages)) {
|
|
deliverred = true;
|
|
}
|
|
else if (!end) {
|
|
ractor_wait(ec, cr, NULL); // no timeout: wait until a message arrives
|
|
}
|
|
else if (*end == 0) {
|
|
timedout = true; // `timeout: 0`: over without reading any clock
|
|
}
|
|
else {
|
|
// only a wakeup nobody claimed can be the deadline, so only then look at
|
|
// the clock: a send or an interrupt says what woke this thread by itself
|
|
timedout = ractor_wait(ec, cr, end) == wakeup_none && rb_hrtime_now() >= *end;
|
|
}
|
|
}
|
|
RACTOR_UNLOCK_SELF(cr);
|
|
|
|
if (deliverred) {
|
|
VM_ASSERT(!ractor_queue_empty_p(cr, &messages));
|
|
struct ractor_basket *b;
|
|
|
|
while ((b = ractor_queue_deq(cr, &messages)) != NULL) {
|
|
ractor_queue_enq(cr, ractor_get_queue(cr, b->port_id, false), b);
|
|
}
|
|
}
|
|
|
|
return !timedout;
|
|
}
|
|
|
|
static VALUE
|
|
ractor_try_receive(rb_execution_context_t *ec, rb_ractor_t *cr, const struct ractor_port *rp)
|
|
{
|
|
struct ractor_queue *rq = ractor_get_queue(cr, ractor_port_id(rp), false);
|
|
|
|
if (rq == NULL) {
|
|
rb_raise(rb_eRactorClosedError, "The port was already closed");
|
|
}
|
|
|
|
struct ractor_basket *b = ractor_queue_deq(cr, rq);
|
|
/* Off the queue and not yet freed: this Ractor roots it while it materializes. */
|
|
if (b) ractor_off_queue_add(cr, b);
|
|
|
|
if (rq->closed && ractor_queue_empty_p(cr, rq)) {
|
|
ractor_delete_port(cr, ractor_port_id(rp), false);
|
|
}
|
|
|
|
if (b) {
|
|
return ractor_basket_accept(b);
|
|
}
|
|
else {
|
|
return Qundef;
|
|
}
|
|
}
|
|
|
|
// Returns Qundef if the deadline passed first. It bounds how long this blocks; it
|
|
// does not cut delivery off. A message that lands while the timeout is being
|
|
// reported is still returned, as Thread::Queue#pop(timeout:) does. Either way
|
|
// nothing is lost: a basket only leaves the queue when it is returned.
|
|
static VALUE
|
|
ractor_receive(rb_execution_context_t *ec, const struct ractor_port *rp, const rb_hrtime_t *end)
|
|
{
|
|
rb_ractor_t *cr = rb_ec_ractor_ptr(ec);
|
|
VM_ASSERT(cr == rp->r);
|
|
|
|
RUBY_DEBUG_LOG("port:%u", (unsigned int)ractor_port_id(rp));
|
|
|
|
while (1) {
|
|
VALUE v = ractor_try_receive(ec, cr, rp);
|
|
|
|
if (v != Qundef) {
|
|
return v;
|
|
}
|
|
else if (!ractor_wait_receive(ec, cr, end)) {
|
|
return Qundef;
|
|
}
|
|
else if (ractor_deadline_passed_p(end)) {
|
|
// The wait ended on a wakeup meant for another port, which says
|
|
// nothing about the clock. One more look, then the deadline stands.
|
|
return ractor_try_receive(ec, cr, rp);
|
|
}
|
|
}
|
|
}
|
|
|
|
// A timeout argument becomes an absolute deadline, or 0 for `timeout: 0`, which
|
|
// every wait reads as "do not wait". Returns NULL when there is no timeout.
|
|
static const rb_hrtime_t *
|
|
ractor_timeout_deadline(VALUE timeout, rb_hrtime_t *storage)
|
|
{
|
|
if (NIL_P(timeout)) return NULL;
|
|
|
|
if (!(FIXNUM_P(timeout) && FIX2LONG(timeout) == 0)) {
|
|
struct timeval tv = rb_time_interval(timeout); // raises on a negative timeout
|
|
rb_hrtime_t rel = rb_timeval2hrtime(&tv);
|
|
|
|
if (rel > 0) {
|
|
*storage = rb_hrtime_add(rb_hrtime_now(), rel);
|
|
return storage;
|
|
}
|
|
}
|
|
|
|
*storage = 0;
|
|
return storage;
|
|
}
|
|
|
|
// Ractor#send
|
|
|
|
static void
|
|
ractor_send_basket(rb_execution_context_t *ec, const struct ractor_port *rp, struct ractor_basket *b, bool raise_on_error)
|
|
{
|
|
bool closed = false;
|
|
|
|
RUBY_DEBUG_LOG("port:%u@r%"PRI_SERIALT_PREFIX"u b:%s v:%p", (unsigned int)ractor_port_id(rp), rb_ractor_id(rp->r), basket_type_name(b->type), (void *)b->p.v);
|
|
|
|
RACTOR_LOCK(rp->r);
|
|
{
|
|
if (ractor_closed_port_p(ec, rp->r, rp)) {
|
|
closed = true;
|
|
}
|
|
else {
|
|
b->port_id = ractor_port_id(rp);
|
|
/* The receiver's queue roots it from here; drop it from ours. */
|
|
ractor_off_queue_remove(b);
|
|
ractor_queue_enq(rp->r, rp->r->sync.recv_queue, b);
|
|
ractor_wakeup_all_locked(rp->r, wakeup_by_send);
|
|
}
|
|
}
|
|
RACTOR_UNLOCK(rp->r);
|
|
|
|
// NOTE: ref r -> b->p.v is created, but Ractor is unprotected object, so no problem on that.
|
|
|
|
if (closed) {
|
|
RUBY_DEBUG_LOG("closed:%u@r%"PRI_SERIALT_PREFIX"u", (unsigned int)ractor_port_id(rp), rb_ractor_id(rp->r));
|
|
|
|
/* Nothing took the basket: it was not enqueued, so free it whether or not the
|
|
* caller wants the error raised. */
|
|
ractor_basket_free(b);
|
|
|
|
if (raise_on_error) {
|
|
rb_raise(rb_eRactorClosedError, "The port was already closed");
|
|
}
|
|
}
|
|
}
|
|
|
|
/* sender is the ractor the token is about; both it and the token are shareable,
|
|
* so nothing is copied until the receiver builds the pair. It also skips the tag
|
|
* ractor_basket_new pushes: the tokens go out from a thread whose EC has already
|
|
* lost its VM stack, and EC_PUSH_TAG reads ec->cfp under ZJIT. */
|
|
static struct ractor_basket *
|
|
ractor_basket_new_exit(VALUE sender, VALUE token)
|
|
{
|
|
struct ractor_basket *b = ractor_basket_alloc();
|
|
|
|
b->type = basket_type_exit;
|
|
b->sender = sender;
|
|
b->p.v = token;
|
|
|
|
return b;
|
|
}
|
|
|
|
static VALUE
|
|
ractor_send0(rb_execution_context_t *ec, const struct ractor_port *rp, VALUE obj, VALUE move, bool raise_on_error)
|
|
{
|
|
struct ractor_basket *b = ractor_basket_new(ec, obj, RTEST(move) ? basket_type_move : basket_type_none, false);
|
|
ractor_send_basket(ec, rp, b, raise_on_error);
|
|
RB_GC_GUARD(obj);
|
|
return rp->r->pub.self;
|
|
}
|
|
|
|
static VALUE
|
|
ractor_send(rb_execution_context_t *ec, const struct ractor_port *rp, VALUE obj, VALUE move)
|
|
{
|
|
return ractor_send0(ec, rp, obj, move, true);
|
|
}
|
|
|
|
// Ractor::Selector
|
|
|
|
struct ractor_selector {
|
|
struct st_table *ports; // rpv -> rp
|
|
|
|
};
|
|
|
|
static int
|
|
ractor_selector_mark_i(st_data_t key, st_data_t val, st_data_t dmy)
|
|
{
|
|
rb_gc_mark((VALUE)key); // rpv
|
|
|
|
return ST_CONTINUE;
|
|
}
|
|
|
|
static void
|
|
ractor_selector_mark(void *ptr)
|
|
{
|
|
struct ractor_selector *s = ptr;
|
|
|
|
if (s->ports) {
|
|
st_foreach(s->ports, ractor_selector_mark_i, 0);
|
|
}
|
|
}
|
|
|
|
static void
|
|
ractor_selector_free(void *ptr)
|
|
{
|
|
struct ractor_selector *s = ptr;
|
|
st_free_table(s->ports);
|
|
SIZED_FREE(s);
|
|
}
|
|
|
|
static size_t
|
|
ractor_selector_memsize(const void *ptr)
|
|
{
|
|
const struct ractor_selector *s = ptr;
|
|
size_t size = sizeof(struct ractor_selector);
|
|
if (s->ports) {
|
|
size += st_memsize(s->ports);
|
|
}
|
|
return size;
|
|
}
|
|
|
|
static const rb_data_type_t ractor_selector_data_type = {
|
|
"ractor/selector",
|
|
{
|
|
ractor_selector_mark,
|
|
ractor_selector_free,
|
|
ractor_selector_memsize,
|
|
NULL, // update
|
|
},
|
|
0, 0, RUBY_TYPED_THREAD_SAFE_FREE | RUBY_TYPED_WB_PROTECTED,
|
|
};
|
|
|
|
static struct ractor_selector *
|
|
RACTOR_SELECTOR_PTR(VALUE selv)
|
|
{
|
|
VM_ASSERT(rb_typeddata_is_kind_of(selv, &ractor_selector_data_type));
|
|
return (struct ractor_selector *)DATA_PTR(selv);
|
|
}
|
|
|
|
// Ractor::Selector.new
|
|
|
|
static VALUE
|
|
ractor_selector_create(VALUE klass)
|
|
{
|
|
struct ractor_selector *s;
|
|
VALUE selv = TypedData_Make_Struct(klass, struct ractor_selector, &ractor_selector_data_type, s);
|
|
s->ports = st_init_numtable(); // TODO
|
|
return selv;
|
|
}
|
|
|
|
// Ractor::Selector#add(r)
|
|
|
|
/*
|
|
* call-seq:
|
|
* add(ractor) -> ractor
|
|
*
|
|
* Adds _ractor_ to +self+. Raises an exception if _ractor_ is already added.
|
|
* Returns _ractor_.
|
|
*/
|
|
static VALUE
|
|
ractor_selector_add(VALUE selv, VALUE rpv)
|
|
{
|
|
if (!ractor_port_p(rpv)) {
|
|
rb_raise(rb_eArgError, "Not a Ractor::Port object");
|
|
}
|
|
|
|
struct ractor_selector *s = RACTOR_SELECTOR_PTR(selv);
|
|
const struct ractor_port *rp = ractor_port_ptr_check(rpv);
|
|
|
|
if (st_lookup(s->ports, (st_data_t)rpv, NULL)) {
|
|
rb_raise(rb_eArgError, "already added");
|
|
}
|
|
|
|
st_insert(s->ports, (st_data_t)rpv, (st_data_t)rp);
|
|
RB_OBJ_WRITTEN(selv, Qundef, rpv);
|
|
|
|
return selv;
|
|
}
|
|
|
|
// Ractor::Selector#remove(r)
|
|
|
|
/* call-seq:
|
|
* remove(ractor) -> ractor
|
|
*
|
|
* Removes _ractor_ from +self+. Raises an exception if _ractor_ is not added.
|
|
* Returns the removed _ractor_.
|
|
*/
|
|
static VALUE
|
|
ractor_selector_remove(VALUE selv, VALUE rpv)
|
|
{
|
|
if (!ractor_port_p(rpv)) {
|
|
rb_raise(rb_eArgError, "Not a Ractor::Port object");
|
|
}
|
|
|
|
struct ractor_selector *s = RACTOR_SELECTOR_PTR(selv);
|
|
|
|
if (!st_lookup(s->ports, (st_data_t)rpv, NULL)) {
|
|
rb_raise(rb_eArgError, "not added yet");
|
|
}
|
|
|
|
st_delete(s->ports, (st_data_t *)&rpv, NULL);
|
|
|
|
return selv;
|
|
}
|
|
|
|
// Ractor::Selector#clear
|
|
|
|
/*
|
|
* call-seq:
|
|
* clear -> self
|
|
*
|
|
* Removes all ractors from +self+. Raises +self+.
|
|
*/
|
|
static VALUE
|
|
ractor_selector_clear(VALUE selv)
|
|
{
|
|
struct ractor_selector *s = RACTOR_SELECTOR_PTR(selv);
|
|
st_clear(s->ports);
|
|
return selv;
|
|
}
|
|
|
|
/*
|
|
* call-seq:
|
|
* empty? -> true or false
|
|
*
|
|
* Returns +true+ if no ractor is added.
|
|
*/
|
|
static VALUE
|
|
ractor_selector_empty_p(VALUE selv)
|
|
{
|
|
struct ractor_selector *s = RACTOR_SELECTOR_PTR(selv);
|
|
return s->ports->num_entries == 0 ? Qtrue : Qfalse;
|
|
}
|
|
|
|
// Ractor::Selector#wait
|
|
|
|
struct ractor_selector_wait_data {
|
|
rb_ractor_t *cr;
|
|
rb_execution_context_t *ec;
|
|
bool found;
|
|
VALUE v;
|
|
VALUE rpv;
|
|
};
|
|
|
|
static int
|
|
ractor_selector_wait_i(st_data_t key, st_data_t val, st_data_t data)
|
|
{
|
|
struct ractor_selector_wait_data *p = (struct ractor_selector_wait_data *)data;
|
|
const struct ractor_port *rp = (const struct ractor_port *)val;
|
|
|
|
VALUE v = ractor_try_receive(p->ec, p->cr, rp);
|
|
|
|
if (v != Qundef) {
|
|
p->found = true;
|
|
p->v = v;
|
|
p->rpv = (VALUE)key;
|
|
return ST_STOP;
|
|
}
|
|
else {
|
|
return ST_CONTINUE;
|
|
}
|
|
}
|
|
|
|
static VALUE
|
|
ractor_selector__wait(rb_execution_context_t *ec, VALUE selector, const rb_hrtime_t *end)
|
|
{
|
|
rb_ractor_t *cr = rb_ec_ractor_ptr(ec);
|
|
struct ractor_selector *s = RACTOR_SELECTOR_PTR(selector);
|
|
|
|
struct ractor_selector_wait_data data = {
|
|
.ec = ec,
|
|
.cr = cr,
|
|
.found = false,
|
|
};
|
|
|
|
while (1) {
|
|
st_foreach(s->ports, ractor_selector_wait_i, (st_data_t)&data);
|
|
|
|
if (data.found) {
|
|
return rb_ary_new_from_args(2, data.rpv, data.v);
|
|
}
|
|
else if (!ractor_wait_receive(ec, cr, end)) {
|
|
return Qnil;
|
|
}
|
|
else if (ractor_deadline_passed_p(end)) {
|
|
st_foreach(s->ports, ractor_selector_wait_i, (st_data_t)&data);
|
|
return data.found ? rb_ary_new_from_args(2, data.rpv, data.v) : Qnil;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* call-seq:
|
|
* wait(receive: false, yield_value: undef, move: false) -> [ractor, value]
|
|
*
|
|
* Waits until any ractor in _selector_ can be active.
|
|
*/
|
|
static VALUE
|
|
ractor_selector_wait(VALUE selector)
|
|
{
|
|
return ractor_selector__wait(GET_EC(), selector, NULL);
|
|
}
|
|
|
|
static VALUE
|
|
ractor_selector_new(int argc, VALUE *ractors, VALUE klass)
|
|
{
|
|
VALUE selector = ractor_selector_create(klass);
|
|
|
|
for (int i=0; i<argc; i++) {
|
|
ractor_selector_add(selector, ractors[i]);
|
|
}
|
|
|
|
return selector;
|
|
}
|
|
|
|
static VALUE
|
|
ractor_select_internal(rb_execution_context_t *ec, VALUE self, VALUE ports, VALUE timeout)
|
|
{
|
|
rb_hrtime_t deadline;
|
|
const rb_hrtime_t *end = ractor_timeout_deadline(timeout, &deadline);
|
|
|
|
VALUE selector = ractor_selector_new(RARRAY_LENINT(ports), (VALUE *)RARRAY_CONST_PTR(ports), rb_cRactorSelector);
|
|
VALUE result = ractor_selector__wait(ec, selector, end);
|
|
|
|
RB_GC_GUARD(selector);
|
|
RB_GC_GUARD(ports);
|
|
return result;
|
|
}
|
|
|
|
#ifndef USE_RACTOR_SELECTOR
|
|
#define USE_RACTOR_SELECTOR 0
|
|
#endif
|
|
|
|
RUBY_SYMBOL_EXPORT_BEGIN
|
|
void rb_init_ractor_selector(void);
|
|
RUBY_SYMBOL_EXPORT_END
|
|
|
|
/*
|
|
* Document-class: Ractor::Selector
|
|
* :nodoc: currently
|
|
*
|
|
* Selects multiple Ractors to be activated.
|
|
*/
|
|
void
|
|
rb_init_ractor_selector(void)
|
|
{
|
|
rb_cRactorSelector = rb_define_class_under(rb_cRactor, "Selector", rb_cObject);
|
|
rb_undef_alloc_func(rb_cRactorSelector);
|
|
|
|
rb_define_singleton_method(rb_cRactorSelector, "new", ractor_selector_new , -1);
|
|
rb_define_method(rb_cRactorSelector, "add", ractor_selector_add, 1);
|
|
rb_define_method(rb_cRactorSelector, "remove", ractor_selector_remove, 1);
|
|
rb_define_method(rb_cRactorSelector, "clear", ractor_selector_clear, 0);
|
|
rb_define_method(rb_cRactorSelector, "empty?", ractor_selector_empty_p, 0);
|
|
rb_define_method(rb_cRactorSelector, "wait", ractor_selector_wait, 0);
|
|
}
|
|
|
|
static void
|
|
Init_RactorPort(void)
|
|
{
|
|
rb_cRactorPort = rb_define_class_under(rb_cRactor, "Port", rb_cObject);
|
|
rb_define_alloc_func(rb_cRactorPort, ractor_port_alloc);
|
|
rb_define_method(rb_cRactorPort, "initialize", ractor_port_initialize, 0);
|
|
rb_define_method(rb_cRactorPort, "initialize_copy", ractor_port_initialize_copy, 1);
|
|
|
|
#if USE_RACTOR_SELECTOR
|
|
rb_init_ractor_selector();
|
|
#endif
|
|
}
|