use core::hint::spin_loop;
#[repr(C, align(16))]
struct PerCpuLock {
lock: libc::pthread_mutex_t,
}
pub(crate) struct PerCpuLocks {
locks: Box<[PerCpuLock]>,
}
unsafe impl Sync for PerCpuLocks {}
unsafe impl Send for PerCpuLocks {}
impl PerCpuLocks {
pub(crate) fn new(num_cpus: usize) -> Self {
assert!(num_cpus > 0, "PerCpuLocks needs at least one CPU");
let mut locks: Box<[PerCpuLock]> = (0..num_cpus)
.map(|_| PerCpuLock {
lock: unsafe { core::mem::zeroed() },
})
.collect();
for slot in locks.iter_mut() {
let rc = unsafe { libc::pthread_mutex_init(&mut slot.lock, core::ptr::null()) };
assert_eq!(rc, 0, "pthread_mutex_init failed: {rc}");
}
Self { locks }
}
#[allow(dead_code)]
pub(crate) fn len(&self) -> usize {
self.locks.len()
}
pub(crate) fn lock_this_cpu(&self) -> PerCpuLockGuard<'_> {
loop {
let cpu = current_cpu(self.locks.len());
let lock = core::ptr::addr_of!(self.locks[cpu].lock) as *mut libc::pthread_mutex_t;
while unsafe { libc::pthread_mutex_trylock(lock) } != 0 {
spin_loop();
}
if current_cpu(self.locks.len()) == cpu {
return PerCpuLockGuard {
lock,
_marker: core::marker::PhantomData,
};
}
unsafe { libc::pthread_mutex_unlock(lock) };
}
}
}
impl Drop for PerCpuLocks {
fn drop(&mut self) {
for slot in self.locks.iter_mut() {
unsafe { libc::pthread_mutex_destroy(&mut slot.lock) };
}
}
}
fn current_cpu(len: usize) -> usize {
let cpu = unsafe { libc::sched_getcpu() };
if cpu < 0 {
0
} else {
(cpu as usize).min(len - 1)
}
}
pub(crate) struct PerCpuLockGuard<'a> {
lock: *mut libc::pthread_mutex_t,
_marker: core::marker::PhantomData<&'a PerCpuLocks>,
}
impl Drop for PerCpuLockGuard<'_> {
fn drop(&mut self) {
unsafe { libc::pthread_mutex_unlock(self.lock) };
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicBool, Ordering};
struct RawCounter(core::cell::UnsafeCell<u64>);
unsafe impl Sync for RawCounter {}
impl RawCounter {
unsafe fn incr(&self) {
let p = self.0.get();
unsafe { *p += 1 };
}
fn get(&self) -> u64 {
unsafe { *self.0.get() }
}
}
fn pin_to_cpu(cpu: usize) -> bool {
unsafe {
let mut set: libc::cpu_set_t = core::mem::zeroed();
libc::CPU_ZERO(&mut set);
libc::CPU_SET(cpu, &mut set);
libc::sched_setaffinity(0, core::mem::size_of::<libc::cpu_set_t>(), &set) == 0
}
}
#[test]
fn new_initializes_requested_lock_count() {
let locks = PerCpuLocks::new(4);
assert_eq!(locks.len(), 4);
}
#[test]
fn lock_then_drop_allows_reacquire_same_thread() {
let locks = PerCpuLocks::new(2);
{
let _g = locks.lock_this_cpu();
} let _g2 = locks.lock_this_cpu();
}
#[test]
fn same_cpu_threads_get_mutual_exclusion() {
const THREADS: usize = 4;
const ITERS: u64 = 20_000;
let locks = PerCpuLocks::new(8);
let counter = RawCounter(core::cell::UnsafeCell::new(0));
let pinned_ok = AtomicBool::new(true);
std::thread::scope(|s| {
for _ in 0..THREADS {
s.spawn(|| {
if !pin_to_cpu(0) {
pinned_ok.store(false, Ordering::Relaxed);
return;
}
for _ in 0..ITERS {
let _g = locks.lock_this_cpu();
unsafe { counter.incr() };
}
});
}
});
if pinned_ok.load(Ordering::Relaxed) {
let total = counter.get();
assert_eq!(
total,
THREADS as u64 * ITERS,
"lost updates => the per-CPU lock did not provide mutual exclusion"
);
}
}
}