aboutsummaryrefslogtreecommitdiff
path: root/core/sync
diff options
context:
space:
mode:
authorJack Mordaunt <jackmordaunt.dev@gmail.com>2025-06-12 12:39:57 -0300
committerJack Mordaunt <jackmordaunt.dev@gmail.com>2025-06-12 17:35:48 -0300
commitc29168f76f05e98e7532c65eda253e14992f8ddf (patch)
treedf2809f788e8767976d6e240e0bac3440b231cc0 /core/sync
parent52d38f1788f11dfb0b7e55ec694258cdce012374 (diff)
core/sync/chan.try_send: avoid blocking if no reader is available
This changes the semantics of try_send to be consistently non-blocking. That is, if the buffered is full OR there are no readers it returns false. The previous behaviour was such that it would block in the latter case of no reader, and it would wait for a reader. That is problematic because it produces inconsistent behaviour between buffered and unbuffered channels which is astonishing and adds complexity to the caller. To illustrate the problem with the old behaviour, consider the try_select operation: if a send-channel happens to be unbuffered the try_select (which wants to never block) can now block, that unbuffered send channel is selected (at random) and there is no reader on the other side. Thus we have unpredictable blocking behaviour, which breaks the guarantee that try_select never blocks. If you want a blocking send you can just call "send" (the blocking variant). In addition, there is some reader/writer math done inside can_{send,recv} such that they only report true if there is sufficient reader/writer capacity. If there is contention we need to ensure that each reader is paired to exactly one writer. Consider try_send: if there is a single reader we can send. If there is a single reader and a single writer, then we cannot send, as that reader will be paired with the existing writer. Therefore can_send is only true if there are more readers than writers at the time of check. NOTE: The original tests don't need to use wait-looping with thread.yield() or heuristic sleep. Instead we can just use blocking channel operations rather than non-blocking operations.
Diffstat (limited to 'core/sync')
-rw-r--r--core/sync/chan/chan.odin15
1 files changed, 8 insertions, 7 deletions
diff --git a/core/sync/chan/chan.odin b/core/sync/chan/chan.odin
index c5a4cf317..1d91556b5 100644
--- a/core/sync/chan/chan.odin
+++ b/core/sync/chan/chan.odin
@@ -779,7 +779,7 @@ try_send_raw :: proc "contextless" (c: ^Raw_Chan, msg_in: rawptr) -> (ok: bool)
} else if c.unbuffered_data != nil { // unbuffered
sync.guard(&c.mutex)
- if c.closed {
+ if c.closed || c.r_waiting - c.w_waiting <= 0 {
return false
}
@@ -843,7 +843,7 @@ try_recv_raw :: proc "contextless" (c: ^Raw_Chan, msg_out: rawptr) -> bool {
} else if c.unbuffered_data != nil { // unbuffered
sync.guard(&c.mutex)
- if c.closed || c.w_waiting == 0 {
+ if c.closed || c.w_waiting - c.r_waiting <= 0 {
return false
}
@@ -1046,8 +1046,9 @@ is_closed :: proc "contextless" (c: ^Raw_Chan) -> bool {
}
/*
-Returns whether a message is ready to be read, i.e.,
-if a call to `recv` or `recv_raw` would block
+Returns whether a message can be read without blocking the current
+thread. Specifically, it checks if the channel is buffered and not full,
+or if there is already a writer attempting to send a message.
**Inputs**
- `c`: The channel
@@ -1075,7 +1076,7 @@ can_recv :: proc "contextless" (c: ^Raw_Chan) -> bool {
if is_buffered(c) {
return c.queue.len > 0
}
- return c.w_waiting > 0
+ return c.w_waiting - c.r_waiting > 0
}
@@ -1088,7 +1089,7 @@ or if there is already a reader waiting for a message.
- `c`: The channel
**Returns**
-- `true` if a message can be send, `false` otherwise
+- `true` if a message can be sent, `false` otherwise
Example:
@@ -1110,7 +1111,7 @@ can_send :: proc "contextless" (c: ^Raw_Chan) -> bool {
if is_buffered(c) {
return c.queue.len < c.queue.cap
}
- return c.w_waiting == 0
+ return c.r_waiting - c.w_waiting > 0
}
/*