-
Notifications
You must be signed in to change notification settings - Fork 989
Implement race-free signals using futexes #4586
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
package futex | ||
|
||
// Cross platform futex implementation. | ||
// Futexes are supported on all major operating systems and on WebAssembly. | ||
// | ||
// For more information, see: https://outerproduct.net/futex-dictionary.html | ||
|
||
import ( | ||
"sync/atomic" | ||
"unsafe" | ||
) | ||
|
||
// A futex is a way for userspace to wait with the pointer as the key, and for | ||
// another thread to wake one or all waiting threads keyed on the same pointer. | ||
// | ||
// A futex does not change the underlying value, it only reads it before going | ||
// to sleep (atomically) to prevent lost wake-ups. | ||
type Futex struct { | ||
atomic.Uint32 | ||
} | ||
|
||
// Atomically check for cmp to still be equal to the futex value and if so, go | ||
// to sleep. Return true if we were definitely awoken by a call to Wake or | ||
// WakeAll, and false if we can't be sure of that. | ||
func (f *Futex) Wait(cmp uint32) bool { | ||
tinygo_futex_wait((*uint32)(unsafe.Pointer(&f.Uint32)), cmp) | ||
|
||
// We *could* detect a zero return value from the futex system call which | ||
// would indicate we got awoken by a Wake or WakeAll call. However, this is | ||
// what the manual page has to say: | ||
// | ||
// > Note that a wake-up can also be caused by common futex usage patterns | ||
// > in unrelated code that happened to have previously used the futex | ||
// > word's memory location (e.g., typical futex-based implementations of | ||
// > Pthreads mutexes can cause this under some conditions). Therefore, | ||
// > callers should always conservatively assume that a return value of 0 | ||
// > can mean a spurious wake-up, and use the futex word's value (i.e., the | ||
// > user-space synchronization scheme) to decide whether to continue to | ||
// > block or not. | ||
// | ||
// I'm not sure whether we do anything like pthread does, so to be on the | ||
// safe side we say we don't know whether the wakeup was spurious or not and | ||
// return false. | ||
return false | ||
} | ||
|
||
// Like Wait, but times out after the number of nanoseconds in timeout. | ||
func (f *Futex) WaitUntil(cmp uint32, timeout uint64) { | ||
tinygo_futex_wait_timeout((*uint32)(unsafe.Pointer(&f.Uint32)), cmp, timeout) | ||
} | ||
|
||
// Wake a single waiter. | ||
func (f *Futex) Wake() { | ||
tinygo_futex_wake((*uint32)(unsafe.Pointer(&f.Uint32))) | ||
} | ||
|
||
// Wake all waiters. | ||
func (f *Futex) WakeAll() { | ||
tinygo_futex_wake_all((*uint32)(unsafe.Pointer(&f.Uint32))) | ||
} | ||
|
||
//export tinygo_futex_wait | ||
func tinygo_futex_wait(addr *uint32, cmp uint32) | ||
|
||
//export tinygo_futex_wait_timeout | ||
func tinygo_futex_wait_timeout(addr *uint32, cmp uint32, timeout uint64) | ||
|
||
//export tinygo_futex_wake | ||
func tinygo_futex_wake(addr *uint32) | ||
|
||
//export tinygo_futex_wake_all | ||
func tinygo_futex_wake_all(addr *uint32) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
//go:build none | ||
|
||
// This file is manually included, to avoid CGo which would cause a circular | ||
// import. | ||
|
||
#include <stdint.h> | ||
|
||
// This API isn't documented by Apple, but it is used by LLVM libc++ (so should | ||
// be stable) and has been documented extensively here: | ||
// https://outerproduct.net/futex-dictionary.html | ||
|
||
int __ulock_wait(uint32_t operation, void *addr, uint64_t value, uint32_t timeout_us); | ||
int __ulock_wait2(uint32_t operation, void *addr, uint64_t value, uint64_t timeout_ns, uint64_t value2); | ||
int __ulock_wake(uint32_t operation, void *addr, uint64_t wake_value); | ||
Comment on lines
+12
to
+14
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Link to documentation, if it exists somewhere (semi-)stable. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. There's no real documentation, but someone has figured out these calls: https://outerproduct.net/futex-dictionary.html |
||
|
||
// Operation code. | ||
#define UL_COMPARE_AND_WAIT 1 | ||
|
||
// Flags to the operation value. | ||
#define ULF_WAKE_ALL 0x00000100 | ||
#define ULF_NO_ERRNO 0x01000000 | ||
|
||
void tinygo_futex_wait(uint32_t *addr, uint32_t cmp) { | ||
__ulock_wait(UL_COMPARE_AND_WAIT|ULF_NO_ERRNO, addr, (uint64_t)cmp, 0); | ||
} | ||
|
||
void tinygo_futex_wait_timeout(uint32_t *addr, uint32_t cmp, uint64_t timeout) { | ||
// Make sure that an accidental use of a zero timeout is not treated as an | ||
// infinite timeout. Return if it's zero since it wouldn't be waiting for | ||
// any significant time anyway. | ||
// Probably unnecessary, but guards against potential bugs. | ||
if (timeout == 0) { | ||
return; | ||
} | ||
|
||
// Note: __ulock_wait2 is available since MacOS 11. | ||
// I think that's fine, since the version before that (MacOS 10.15) is EOL | ||
// since 2022. Though if needed, we could certainly use __ulock_wait instead | ||
// and deal with the smaller timeout value. | ||
Comment on lines
+36
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: I would put (most of) this note in the commit message. Go 1.23 also requires macOS 11 and it doesn't seem reasonable to do better in TinyGo. |
||
__ulock_wait2(UL_COMPARE_AND_WAIT|ULF_NO_ERRNO, addr, (uint64_t)cmp, timeout, 0); | ||
} | ||
|
||
void tinygo_futex_wake(uint32_t *addr) { | ||
__ulock_wake(UL_COMPARE_AND_WAIT|ULF_NO_ERRNO, addr, 0); | ||
} | ||
|
||
void tinygo_futex_wake_all(uint32_t *addr) { | ||
__ulock_wake(UL_COMPARE_AND_WAIT|ULF_NO_ERRNO|ULF_WAKE_ALL, addr, 0); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
//go:build none | ||
|
||
// This file is manually included, to avoid CGo which would cause a circular | ||
// import. | ||
|
||
#include <limits.h> | ||
#include <stdint.h> | ||
#include <sys/syscall.h> | ||
#include <time.h> | ||
#include <unistd.h> | ||
|
||
#define FUTEX_WAIT 0 | ||
#define FUTEX_WAKE 1 | ||
#define FUTEX_PRIVATE_FLAG 128 | ||
Comment on lines
+12
to
+14
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because this header file is not available in musl, which we use. |
||
|
||
void tinygo_futex_wait(uint32_t *addr, uint32_t cmp) { | ||
syscall(SYS_futex, addr, FUTEX_WAIT|FUTEX_PRIVATE_FLAG, cmp, NULL, NULL, 0); | ||
} | ||
|
||
void tinygo_futex_wait_timeout(uint32_t *addr, uint32_t cmp, uint64_t timeout) { | ||
struct timespec ts = {0}; | ||
ts.tv_sec = timeout / 1000000000; | ||
ts.tv_nsec = timeout % 1000000000; | ||
syscall(SYS_futex, addr, FUTEX_WAIT|FUTEX_PRIVATE_FLAG, cmp, &ts, NULL, 0); | ||
} | ||
|
||
void tinygo_futex_wake(uint32_t *addr) { | ||
syscall(SYS_futex, addr, FUTEX_WAKE|FUTEX_PRIVATE_FLAG, 1, NULL, NULL, 0); | ||
} | ||
|
||
void tinygo_futex_wake_all(uint32_t *addr) { | ||
syscall(SYS_futex, addr, FUTEX_WAKE|FUTEX_PRIVATE_FLAG, INT_MAX, NULL, NULL, 0); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's probably ok to assume the layout of
atomic.Uint32
here and below, but it seems to me you can avoid it byand adding methods that forwards to atomic.(Load|And|Store...)Uint32 functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer to keep using
atomic.Uint32
. I don't expectatomic.Uint32
to ever change to something else, and if it does we can always update this code.