diff options
| author | The Miri Cronjob Bot <miri@cron.bot> | 2024-03-14 05:01:33 +0000 |
|---|---|---|
| committer | The Miri Cronjob Bot <miri@cron.bot> | 2024-03-14 05:01:33 +0000 |
| commit | 06ca3abc5ab06894fd2e15f78140eacccca3a5e9 (patch) | |
| tree | ca240256a0323f4bbf16f86e7de5c1c78e47c98a /library/std/src/sys/sync/rwlock | |
| parent | f5bb34f4605bc83c02c2c0a7a128d706d6031f11 (diff) | |
| parent | ac1b8575c017b6cc99cf389ceffe853d7b53a694 (diff) | |
| download | rust-06ca3abc5ab06894fd2e15f78140eacccca3a5e9.tar.gz rust-06ca3abc5ab06894fd2e15f78140eacccca3a5e9.zip | |
Merge from rustc
Diffstat (limited to 'library/std/src/sys/sync/rwlock')
| -rw-r--r-- | library/std/src/sys/sync/rwlock/futex.rs | 320 | ||||
| -rw-r--r-- | library/std/src/sys/sync/rwlock/mod.rs | 37 | ||||
| -rw-r--r-- | library/std/src/sys/sync/rwlock/no_threads.rs | 65 | ||||
| -rw-r--r-- | library/std/src/sys/sync/rwlock/queue.rs | 557 | ||||
| -rw-r--r-- | library/std/src/sys/sync/rwlock/sgx.rs | 219 | ||||
| -rw-r--r-- | library/std/src/sys/sync/rwlock/sgx/tests.rs | 21 | ||||
| -rw-r--r-- | library/std/src/sys/sync/rwlock/solid.rs | 93 | ||||
| -rw-r--r-- | library/std/src/sys/sync/rwlock/teeos.rs | 44 | ||||
| -rw-r--r-- | library/std/src/sys/sync/rwlock/windows7.rs | 40 | ||||
| -rw-r--r-- | library/std/src/sys/sync/rwlock/xous.rs | 74 |
10 files changed, 1470 insertions, 0 deletions
diff --git a/library/std/src/sys/sync/rwlock/futex.rs b/library/std/src/sys/sync/rwlock/futex.rs new file mode 100644 index 00000000000..aa0de900238 --- /dev/null +++ b/library/std/src/sys/sync/rwlock/futex.rs @@ -0,0 +1,320 @@ +use crate::sync::atomic::{ + AtomicU32, + Ordering::{Acquire, Relaxed, Release}, +}; +use crate::sys::futex::{futex_wait, futex_wake, futex_wake_all}; + +pub struct RwLock { + // The state consists of a 30-bit reader counter, a 'readers waiting' flag, and a 'writers waiting' flag. + // Bits 0..30: + // 0: Unlocked + // 1..=0x3FFF_FFFE: Locked by N readers + // 0x3FFF_FFFF: Write locked + // Bit 30: Readers are waiting on this futex. + // Bit 31: Writers are waiting on the writer_notify futex. + state: AtomicU32, + // The 'condition variable' to notify writers through. + // Incremented on every signal. + writer_notify: AtomicU32, +} + +const READ_LOCKED: u32 = 1; +const MASK: u32 = (1 << 30) - 1; +const WRITE_LOCKED: u32 = MASK; +const MAX_READERS: u32 = MASK - 1; +const READERS_WAITING: u32 = 1 << 30; +const WRITERS_WAITING: u32 = 1 << 31; + +#[inline] +fn is_unlocked(state: u32) -> bool { + state & MASK == 0 +} + +#[inline] +fn is_write_locked(state: u32) -> bool { + state & MASK == WRITE_LOCKED +} + +#[inline] +fn has_readers_waiting(state: u32) -> bool { + state & READERS_WAITING != 0 +} + +#[inline] +fn has_writers_waiting(state: u32) -> bool { + state & WRITERS_WAITING != 0 +} + +#[inline] +fn is_read_lockable(state: u32) -> bool { + // This also returns false if the counter could overflow if we tried to read lock it. + // + // We don't allow read-locking if there's readers waiting, even if the lock is unlocked + // and there's no writers waiting. The only situation when this happens is after unlocking, + // at which point the unlocking thread might be waking up writers, which have priority over readers. + // The unlocking thread will clear the readers waiting bit and wake up readers, if necessary. + state & MASK < MAX_READERS && !has_readers_waiting(state) && !has_writers_waiting(state) +} + +#[inline] +fn has_reached_max_readers(state: u32) -> bool { + state & MASK == MAX_READERS +} + +impl RwLock { + #[inline] + pub const fn new() -> Self { + Self { state: AtomicU32::new(0), writer_notify: AtomicU32::new(0) } + } + + #[inline] + pub fn try_read(&self) -> bool { + self.state + .fetch_update(Acquire, Relaxed, |s| is_read_lockable(s).then(|| s + READ_LOCKED)) + .is_ok() + } + + #[inline] + pub fn read(&self) { + let state = self.state.load(Relaxed); + if !is_read_lockable(state) + || self + .state + .compare_exchange_weak(state, state + READ_LOCKED, Acquire, Relaxed) + .is_err() + { + self.read_contended(); + } + } + + #[inline] + pub unsafe fn read_unlock(&self) { + let state = self.state.fetch_sub(READ_LOCKED, Release) - READ_LOCKED; + + // It's impossible for a reader to be waiting on a read-locked RwLock, + // except if there is also a writer waiting. + debug_assert!(!has_readers_waiting(state) || has_writers_waiting(state)); + + // Wake up a writer if we were the last reader and there's a writer waiting. + if is_unlocked(state) && has_writers_waiting(state) { + self.wake_writer_or_readers(state); + } + } + + #[cold] + fn read_contended(&self) { + let mut state = self.spin_read(); + + loop { + // If we can lock it, lock it. + if is_read_lockable(state) { + match self.state.compare_exchange_weak(state, state + READ_LOCKED, Acquire, Relaxed) + { + Ok(_) => return, // Locked! + Err(s) => { + state = s; + continue; + } + } + } + + // Check for overflow. + if has_reached_max_readers(state) { + panic!("too many active read locks on RwLock"); + } + + // Make sure the readers waiting bit is set before we go to sleep. + if !has_readers_waiting(state) { + if let Err(s) = + self.state.compare_exchange(state, state | READERS_WAITING, Relaxed, Relaxed) + { + state = s; + continue; + } + } + + // Wait for the state to change. + futex_wait(&self.state, state | READERS_WAITING, None); + + // Spin again after waking up. + state = self.spin_read(); + } + } + + #[inline] + pub fn try_write(&self) -> bool { + self.state + .fetch_update(Acquire, Relaxed, |s| is_unlocked(s).then(|| s + WRITE_LOCKED)) + .is_ok() + } + + #[inline] + pub fn write(&self) { + if self.state.compare_exchange_weak(0, WRITE_LOCKED, Acquire, Relaxed).is_err() { + self.write_contended(); + } + } + + #[inline] + pub unsafe fn write_unlock(&self) { + let state = self.state.fetch_sub(WRITE_LOCKED, Release) - WRITE_LOCKED; + + debug_assert!(is_unlocked(state)); + + if has_writers_waiting(state) || has_readers_waiting(state) { + self.wake_writer_or_readers(state); + } + } + + #[cold] + fn write_contended(&self) { + let mut state = self.spin_write(); + + let mut other_writers_waiting = 0; + + loop { + // If it's unlocked, we try to lock it. + if is_unlocked(state) { + match self.state.compare_exchange_weak( + state, + state | WRITE_LOCKED | other_writers_waiting, + Acquire, + Relaxed, + ) { + Ok(_) => return, // Locked! + Err(s) => { + state = s; + continue; + } + } + } + + // Set the waiting bit indicating that we're waiting on it. + if !has_writers_waiting(state) { + if let Err(s) = + self.state.compare_exchange(state, state | WRITERS_WAITING, Relaxed, Relaxed) + { + state = s; + continue; + } + } + + // Other writers might be waiting now too, so we should make sure + // we keep that bit on once we manage lock it. + other_writers_waiting = WRITERS_WAITING; + + // Examine the notification counter before we check if `state` has changed, + // to make sure we don't miss any notifications. + let seq = self.writer_notify.load(Acquire); + + // Don't go to sleep if the lock has become available, + // or if the writers waiting bit is no longer set. + state = self.state.load(Relaxed); + if is_unlocked(state) || !has_writers_waiting(state) { + continue; + } + + // Wait for the state to change. + futex_wait(&self.writer_notify, seq, None); + + // Spin again after waking up. + state = self.spin_write(); + } + } + + /// Wake up waiting threads after unlocking. + /// + /// If both are waiting, this will wake up only one writer, but will fall + /// back to waking up readers if there was no writer to wake up. + #[cold] + fn wake_writer_or_readers(&self, mut state: u32) { + assert!(is_unlocked(state)); + + // The readers waiting bit might be turned on at any point now, + // since readers will block when there's anything waiting. + // Writers will just lock the lock though, regardless of the waiting bits, + // so we don't have to worry about the writer waiting bit. + // + // If the lock gets locked in the meantime, we don't have to do + // anything, because then the thread that locked the lock will take + // care of waking up waiters when it unlocks. + + // If only writers are waiting, wake one of them up. + if state == WRITERS_WAITING { + match self.state.compare_exchange(state, 0, Relaxed, Relaxed) { + Ok(_) => { + self.wake_writer(); + return; + } + Err(s) => { + // Maybe some readers are now waiting too. So, continue to the next `if`. + state = s; + } + } + } + + // If both writers and readers are waiting, leave the readers waiting + // and only wake up one writer. + if state == READERS_WAITING + WRITERS_WAITING { + if self.state.compare_exchange(state, READERS_WAITING, Relaxed, Relaxed).is_err() { + // The lock got locked. Not our problem anymore. + return; + } + if self.wake_writer() { + return; + } + // No writers were actually blocked on futex_wait, so we continue + // to wake up readers instead, since we can't be sure if we notified a writer. + state = READERS_WAITING; + } + + // If readers are waiting, wake them all up. + if state == READERS_WAITING { + if self.state.compare_exchange(state, 0, Relaxed, Relaxed).is_ok() { + futex_wake_all(&self.state); + } + } + } + + /// This wakes one writer and returns true if we woke up a writer that was + /// blocked on futex_wait. + /// + /// If this returns false, it might still be the case that we notified a + /// writer that was about to go to sleep. + fn wake_writer(&self) -> bool { + self.writer_notify.fetch_add(1, Release); + futex_wake(&self.writer_notify) + // Note that FreeBSD and DragonFlyBSD don't tell us whether they woke + // up any threads or not, and always return `false` here. That still + // results in correct behaviour: it just means readers get woken up as + // well in case both readers and writers were waiting. + } + + /// Spin for a while, but stop directly at the given condition. + #[inline] + fn spin_until(&self, f: impl Fn(u32) -> bool) -> u32 { + let mut spin = 100; // Chosen by fair dice roll. + loop { + let state = self.state.load(Relaxed); + if f(state) || spin == 0 { + return state; + } + crate::hint::spin_loop(); + spin -= 1; + } + } + + #[inline] + fn spin_write(&self) -> u32 { + // Stop spinning when it's unlocked or when there's waiting writers, to keep things somewhat fair. + self.spin_until(|state| is_unlocked(state) || has_writers_waiting(state)) + } + + #[inline] + fn spin_read(&self) -> u32 { + // Stop spinning when it's unlocked or read locked, or when there's waiting threads. + self.spin_until(|state| { + !is_write_locked(state) || has_readers_waiting(state) || has_writers_waiting(state) + }) + } +} diff --git a/library/std/src/sys/sync/rwlock/mod.rs b/library/std/src/sys/sync/rwlock/mod.rs new file mode 100644 index 00000000000..675931c64bd --- /dev/null +++ b/library/std/src/sys/sync/rwlock/mod.rs @@ -0,0 +1,37 @@ +cfg_if::cfg_if! { + if #[cfg(any( + all(target_os = "windows", not(target_vendor = "win7")), + target_os = "linux", + target_os = "android", + target_os = "freebsd", + target_os = "openbsd", + target_os = "dragonfly", + target_os = "fuchsia", + all(target_family = "wasm", target_feature = "atomics"), + target_os = "hermit", + ))] { + mod futex; + pub use futex::RwLock; + } else if #[cfg(target_family = "unix")] { + mod queue; + pub use queue::RwLock; + } else if #[cfg(all(target_os = "windows", target_vendor = "win7"))] { + mod windows7; + pub use windows7::RwLock; + } else if #[cfg(all(target_vendor = "fortanix", target_env = "sgx"))] { + mod sgx; + pub use sgx::RwLock; + } else if #[cfg(target_os = "solid_asp3")] { + mod solid; + pub use solid::RwLock; + } else if #[cfg(target_os = "teeos")] { + mod teeos; + pub use teeos::RwLock; + } else if #[cfg(target_os = "xous")] { + mod xous; + pub use xous::RwLock; + } else { + mod no_threads; + pub use no_threads::RwLock; + } +} diff --git a/library/std/src/sys/sync/rwlock/no_threads.rs b/library/std/src/sys/sync/rwlock/no_threads.rs new file mode 100644 index 00000000000..789ef9b29e5 --- /dev/null +++ b/library/std/src/sys/sync/rwlock/no_threads.rs @@ -0,0 +1,65 @@ +use crate::cell::Cell; + +pub struct RwLock { + // This platform has no threads, so we can use a Cell here. + mode: Cell<isize>, +} + +unsafe impl Send for RwLock {} +unsafe impl Sync for RwLock {} // no threads on this platform + +impl RwLock { + #[inline] + #[rustc_const_stable(feature = "const_locks", since = "1.63.0")] + pub const fn new() -> RwLock { + RwLock { mode: Cell::new(0) } + } + + #[inline] + pub fn read(&self) { + let m = self.mode.get(); + if m >= 0 { + self.mode.set(m + 1); + } else { + rtabort!("rwlock locked for writing"); + } + } + + #[inline] + pub fn try_read(&self) -> bool { + let m = self.mode.get(); + if m >= 0 { + self.mode.set(m + 1); + true + } else { + false + } + } + + #[inline] + pub fn write(&self) { + if self.mode.replace(-1) != 0 { + rtabort!("rwlock locked for reading") + } + } + + #[inline] + pub fn try_write(&self) -> bool { + if self.mode.get() == 0 { + self.mode.set(-1); + true + } else { + false + } + } + + #[inline] + pub unsafe fn read_unlock(&self) { + self.mode.set(self.mode.get() - 1); + } + + #[inline] + pub unsafe fn write_unlock(&self) { + assert_eq!(self.mode.replace(0), -1); + } +} diff --git a/library/std/src/sys/sync/rwlock/queue.rs b/library/std/src/sys/sync/rwlock/queue.rs new file mode 100644 index 00000000000..dce966086b8 --- /dev/null +++ b/library/std/src/sys/sync/rwlock/queue.rs @@ -0,0 +1,557 @@ +//! Efficient read-write locking without `pthread_rwlock_t`. +//! +//! The readers-writer lock provided by the `pthread` library has a number of +//! problems which make it a suboptimal choice for `std`: +//! +//! * It is non-movable, so it needs to be allocated (lazily, to make the +//! constructor `const`). +//! * `pthread` is an external library, meaning the fast path of acquiring an +//! uncontended lock cannot be inlined. +//! * Some platforms (at least glibc before version 2.25) have buggy implementations +//! that can easily lead to undefined behaviour in safe Rust code when not properly +//! guarded against. +//! * On some platforms (e.g. macOS), the lock is very slow. +//! +//! Therefore, we implement our own `RwLock`! Naively, one might reach for a +//! spinlock, but those [can be quite problematic] when the lock is contended. +//! Instead, this readers-writer lock copies its implementation strategy from +//! the Windows [SRWLOCK] and the [usync] library. Spinning is still used for the +//! fast path, but it is bounded: after spinning fails, threads will locklessly +//! add an information structure containing a [`Thread`] handle into a queue of +//! waiters associated with the lock. The lock owner, upon releasing the lock, +//! will scan through the queue and wake up threads as appropriate, which will +//! then again try to acquire the lock. The resulting [`RwLock`] is: +//! +//! * adaptive, since it spins before doing any heavywheight parking operations +//! * allocation-free, modulo the per-thread [`Thread`] handle, which is +//! allocated regardless when using threads created by `std` +//! * writer-preferring, even if some readers may still slip through +//! * unfair, which reduces context-switching and thus drastically improves +//! performance +//! +//! and also quite fast in most cases. +//! +//! [can be quite problematic]: https://matklad.github.io/2020/01/02/spinlocks-considered-harmful.html +//! [SRWLOCK]: https://learn.microsoft.com/en-us/windows/win32/sync/slim-reader-writer--srw--locks +//! [usync]: https://crates.io/crates/usync +//! +//! # Implementation +//! +//! ## State +//! +//! A single [`AtomicPtr`] is used as state variable. The lowest three bits are used +//! to indicate the meaning of the remaining bits: +//! +//! | [`LOCKED`] | [`QUEUED`] | [`QUEUE_LOCKED`] | Remaining | | +//! |:-----------|:-----------|:-----------------|:-------------|:----------------------------------------------------------------------------------------------------------------------------| +//! | 0 | 0 | 0 | 0 | The lock is unlocked, no threads are waiting | +//! | 1 | 0 | 0 | 0 | The lock is write-locked, no threads waiting | +//! | 1 | 0 | 0 | n > 0 | The lock is read-locked with n readers | +//! | 0 | 1 | * | `*mut Node` | The lock is unlocked, but some threads are waiting. Only writers may lock the lock | +//! | 1 | 1 | * | `*mut Node` | The lock is locked, but some threads are waiting. If the lock is read-locked, the last queue node contains the reader count | +//! +//! ## Waiter queue +//! +//! When threads are waiting on the lock (`QUEUE` is set), the lock state +//! points to a queue of waiters, which is implemented as a linked list of +//! nodes stored on the stack to avoid memory allocation. To enable lockless +//! enqueuing of new nodes to the queue, the linked list is single-linked upon +//! creation. Since when the lock is read-locked, the lock count is stored in +//! the last link of the queue, threads have to traverse the queue to find the +//! last element upon releasing the lock. To avoid having to traverse the whole +//! list again and again, a pointer to the found tail is cached in the (current) +//! first element of the queue. +//! +//! Also, while the lock is unfair for performance reasons, it is still best to +//! wake the tail node first, which requires backlinks to previous nodes to be +//! created. This is done at the same time as finding the tail, and thus a set +//! tail field indicates the remaining portion of the queue is initialized. +//! +//! TLDR: Here's a diagram of what the queue looks like: +//! +//! ```text +//! state +//! │ +//! ▼ +//! ╭───────╮ next ╭───────╮ next ╭───────╮ next ╭───────╮ +//! │ ├─────►│ ├─────►│ ├─────►│ count │ +//! │ │ │ │ │ │ │ │ +//! │ │ │ │◄─────┤ │◄─────┤ │ +//! ╰───────╯ ╰───────╯ prev ╰───────╯ prev ╰───────╯ +//! │ ▲ +//! └───────────────────────────┘ +//! tail +//! ``` +//! +//! Invariants: +//! 1. At least one node must contain a non-null, current `tail` field. +//! 2. The first non-null `tail` field must be valid and current. +//! 3. All nodes preceding this node must have a correct, non-null `next` field. +//! 4. All nodes following this node must have a correct, non-null `prev` field. +//! +//! Access to the queue is controlled by the `QUEUE_LOCKED` bit, which threads +//! try to set both after enqueuing themselves to eagerly add backlinks to the +//! queue, which drastically improves performance, and after unlocking the lock +//! to wake the next waiter(s). This is done atomically at the same time as the +//! enqueuing/unlocking operation. The thread releasing the `QUEUE_LOCK` bit +//! will check the state of the lock and wake up waiters as appropriate. This +//! guarantees forward-progress even if the unlocking thread could not acquire +//! the queue lock. +//! +//! ## Memory orderings +//! +//! To properly synchronize changes to the data protected by the lock, the lock +//! is acquired and released with [`Acquire`] and [`Release`] ordering, respectively. +//! To propagate the initialization of nodes, changes to the queue lock are also +//! performed using these orderings. + +#![forbid(unsafe_op_in_unsafe_fn)] + +use crate::cell::OnceCell; +use crate::hint::spin_loop; +use crate::mem; +use crate::ptr::{self, null_mut, without_provenance_mut, NonNull}; +use crate::sync::atomic::{ + AtomicBool, AtomicPtr, + Ordering::{AcqRel, Acquire, Relaxed, Release}, +}; +use crate::sys_common::thread_info; +use crate::thread::Thread; + +// Locking uses exponential backoff. `SPIN_COUNT` indicates how many times the +// locking operation will be retried. +// `spin_loop` will be called `2.pow(SPIN_COUNT) - 1` times. +const SPIN_COUNT: usize = 7; + +type State = *mut (); +type AtomicState = AtomicPtr<()>; + +const UNLOCKED: State = without_provenance_mut(0); +const LOCKED: usize = 1; +const QUEUED: usize = 2; +const QUEUE_LOCKED: usize = 4; +const SINGLE: usize = 8; +const MASK: usize = !(QUEUE_LOCKED | QUEUED | LOCKED); + +/// Marks the state as write-locked, if possible. +#[inline] +fn write_lock(state: State) -> Option<State> { + let state = state.wrapping_byte_add(LOCKED); + if state.addr() & LOCKED == LOCKED { Some(state) } else { None } +} + +/// Marks the state as read-locked, if possible. +#[inline] +fn read_lock(state: State) -> Option<State> { + if state.addr() & QUEUED == 0 && state.addr() != LOCKED { + Some(without_provenance_mut(state.addr().checked_add(SINGLE)? | LOCKED)) + } else { + None + } +} + +/// Masks the state, assuming it points to a queue node. +/// +/// # Safety +/// The state must contain a valid pointer to a queue node. +#[inline] +unsafe fn to_node(state: State) -> NonNull<Node> { + unsafe { NonNull::new_unchecked(state.mask(MASK)).cast() } +} + +/// An atomic node pointer with relaxed operations. +struct AtomicLink(AtomicPtr<Node>); + +impl AtomicLink { + fn new(v: Option<NonNull<Node>>) -> AtomicLink { + AtomicLink(AtomicPtr::new(v.map_or(null_mut(), NonNull::as_ptr))) + } + + fn get(&self) -> Option<NonNull<Node>> { + NonNull::new(self.0.load(Relaxed)) + } + + fn set(&self, v: Option<NonNull<Node>>) { + self.0.store(v.map_or(null_mut(), NonNull::as_ptr), Relaxed); + } +} + +#[repr(align(8))] +struct Node { + next: AtomicLink, + prev: AtomicLink, + tail: AtomicLink, + write: bool, + thread: OnceCell<Thread>, + completed: AtomicBool, +} + +impl Node { + /// Create a new queue node. + fn new(write: bool) -> Node { + Node { + next: AtomicLink::new(None), + prev: AtomicLink::new(None), + tail: AtomicLink::new(None), + write, + thread: OnceCell::new(), + completed: AtomicBool::new(false), + } + } + + /// Prepare this node for waiting. + fn prepare(&mut self) { + // Fall back to creating an unnamed `Thread` handle to allow locking in + // TLS destructors. + self.thread + .get_or_init(|| thread_info::current_thread().unwrap_or_else(|| Thread::new(None))); + self.completed = AtomicBool::new(false); + } + + /// Wait until this node is marked as completed. + /// + /// # Safety + /// May only be called from the thread that created the node. + unsafe fn wait(&self) { + while !self.completed.load(Acquire) { + unsafe { + self.thread.get().unwrap().park(); + } + } + } + + /// Atomically mark this node as completed. The node may not outlive this call. + unsafe fn complete(this: NonNull<Node>) { + // Since the node may be destroyed immediately after the completed flag + // is set, clone the thread handle before that. + let thread = unsafe { this.as_ref().thread.get().unwrap().clone() }; + unsafe { + this.as_ref().completed.store(true, Release); + } + thread.unpark(); + } +} + +struct PanicGuard; + +impl Drop for PanicGuard { + fn drop(&mut self) { + rtabort!("tried to drop node in intrusive list."); + } +} + +/// Add backlinks to the queue, returning the tail. +/// +/// May be called from multiple threads at the same time, while the queue is not +/// modified (this happens when unlocking multiple readers). +/// +/// # Safety +/// * `head` must point to a node in a valid queue. +/// * `head` must be or be in front of the head of the queue at the time of the +/// last removal. +/// * The part of the queue starting with `head` must not be modified during this +/// call. +unsafe fn add_backlinks_and_find_tail(head: NonNull<Node>) -> NonNull<Node> { + let mut current = head; + let tail = loop { + let c = unsafe { current.as_ref() }; + match c.tail.get() { + Some(tail) => break tail, + // SAFETY: + // All `next` fields before the first node with a `set` tail are + // non-null and valid (invariant 3). + None => unsafe { + let next = c.next.get().unwrap_unchecked(); + next.as_ref().prev.set(Some(current)); + current = next; + }, + } + }; + + unsafe { + head.as_ref().tail.set(Some(tail)); + tail + } +} + +pub struct RwLock { + state: AtomicState, +} + +impl RwLock { + #[inline] + pub const fn new() -> RwLock { + RwLock { state: AtomicPtr::new(UNLOCKED) } + } + + #[inline] + pub fn try_read(&self) -> bool { + self.state.fetch_update(Acquire, Relaxed, read_lock).is_ok() + } + + #[inline] + pub fn read(&self) { + if !self.try_read() { + self.lock_contended(false) + } + } + + #[inline] + pub fn try_write(&self) -> bool { + // Atomically set the `LOCKED` bit. This is lowered to a single atomic + // instruction on most modern processors (e.g. "lock bts" on x86 and + // "ldseta" on modern AArch64), and therefore is more efficient than + // `fetch_update(lock(true))`, which can spuriously fail if a new node + // is appended to the queue. + self.state.fetch_or(LOCKED, Acquire).addr() & LOCKED == 0 + } + + #[inline] + pub fn write(&self) { + if !self.try_write() { + self.lock_contended(true) + } + } + + #[cold] + fn lock_contended(&self, write: bool) { + let update = if write { write_lock } else { read_lock }; + let mut node = Node::new(write); + let mut state = self.state.load(Relaxed); + let mut count = 0; + loop { + if let Some(next) = update(state) { + // The lock is available, try locking it. + match self.state.compare_exchange_weak(state, next, Acquire, Relaxed) { + Ok(_) => return, + Err(new) => state = new, + } + } else if state.addr() & QUEUED == 0 && count < SPIN_COUNT { + // If the lock is not available and no threads are queued, spin + // for a while, using exponential backoff to decrease cache + // contention. + for _ in 0..(1 << count) { + spin_loop(); + } + state = self.state.load(Relaxed); + count += 1; + } else { + // Fall back to parking. First, prepare the node. + node.prepare(); + + // If there are threads queued, set the `next` field to a + // pointer to the next node in the queue. Otherwise set it to + // the lock count if the state is read-locked or to zero if it + // is write-locked. + node.next.0 = AtomicPtr::new(state.mask(MASK).cast()); + node.prev = AtomicLink::new(None); + let mut next = ptr::from_ref(&node) + .map_addr(|addr| addr | QUEUED | (state.addr() & LOCKED)) + as State; + + if state.addr() & QUEUED == 0 { + // If this is the first node in the queue, set the tail field to + // the node itself to ensure there is a current `tail` field in + // the queue (invariants 1 and 2). This needs to use `set` to + // avoid invalidating the new pointer. + node.tail.set(Some(NonNull::from(&node))); + } else { + // Otherwise, the tail of the queue is not known. + node.tail.set(None); + // Try locking the queue to eagerly add backlinks. + next = next.map_addr(|addr| addr | QUEUE_LOCKED); + } + + // Register the node, using release ordering to propagate our + // changes to the waking thread. + if let Err(new) = self.state.compare_exchange_weak(state, next, AcqRel, Relaxed) { + // The state has changed, just try again. + state = new; + continue; + } + + // The node is registered, so the structure must not be + // mutably accessed or destroyed while other threads may + // be accessing it. Guard against unwinds using a panic + // guard that aborts when dropped. + let guard = PanicGuard; + + // If the current thread locked the queue, unlock it again, + // linking it in the process. + if state.addr() & (QUEUE_LOCKED | QUEUED) == QUEUED { + unsafe { + self.unlock_queue(next); + } + } + + // Wait until the node is removed from the queue. + // SAFETY: the node was created by the current thread. + unsafe { + node.wait(); + } + + // The node was removed from the queue, disarm the guard. + mem::forget(guard); + + // Reload the state and try again. + state = self.state.load(Relaxed); + count = 0; + } + } + } + + #[inline] + pub unsafe fn read_unlock(&self) { + match self.state.fetch_update(Release, Acquire, |state| { + if state.addr() & QUEUED == 0 { + let count = state.addr() - (SINGLE | LOCKED); + Some(if count > 0 { without_provenance_mut(count | LOCKED) } else { UNLOCKED }) + } else { + None + } + }) { + Ok(_) => {} + // There are waiters queued and the lock count was moved to the + // tail of the queue. + Err(state) => unsafe { self.read_unlock_contended(state) }, + } + } + + #[cold] + unsafe fn read_unlock_contended(&self, state: State) { + // The state was observed with acquire ordering above, so the current + // thread will observe all node initializations. + + // SAFETY: + // Because new read-locks cannot be acquired while threads are queued, + // all queue-lock owners will observe the set `LOCKED` bit. Because they + // do not modify the queue while there is a lock owner, the queue will + // not be removed from here. + let tail = unsafe { add_backlinks_and_find_tail(to_node(state)).as_ref() }; + // The lock count is stored in the `next` field of `tail`. + // Decrement it, making sure to observe all changes made to the queue + // by the other lock owners by using acquire-release ordering. + let was_last = tail.next.0.fetch_byte_sub(SINGLE, AcqRel).addr() - SINGLE == 0; + if was_last { + // SAFETY: + // Other threads cannot read-lock while threads are queued. Also, + // the `LOCKED` bit is still set, so there are no writers. Therefore, + // the current thread exclusively owns the lock. + unsafe { self.unlock_contended(state) } + } + } + + #[inline] + pub unsafe fn write_unlock(&self) { + if let Err(state) = + self.state.compare_exchange(without_provenance_mut(LOCKED), UNLOCKED, Release, Relaxed) + { + // SAFETY: + // Since other threads cannot acquire the lock, the state can only + // have changed because there are threads queued on the lock. + unsafe { self.unlock_contended(state) } + } + } + + /// # Safety + /// * The lock must be exclusively owned by this thread. + /// * There must be threads queued on the lock. + #[cold] + unsafe fn unlock_contended(&self, mut state: State) { + loop { + // Atomically release the lock and try to acquire the queue lock. + let next = state.map_addr(|a| (a & !LOCKED) | QUEUE_LOCKED); + match self.state.compare_exchange_weak(state, next, AcqRel, Relaxed) { + // The queue lock was acquired. Release it, waking up the next + // waiter in the process. + Ok(_) if state.addr() & QUEUE_LOCKED == 0 => unsafe { + return self.unlock_queue(next); + }, + // Another thread already holds the queue lock, leave waking up + // waiters to it. + Ok(_) => return, + Err(new) => state = new, + } + } + } + + /// Unlocks the queue. If the lock is unlocked, wakes up the next eligible + /// thread(s). + /// + /// # Safety + /// The queue lock must be held by the current thread. + unsafe fn unlock_queue(&self, mut state: State) { + debug_assert_eq!(state.addr() & (QUEUED | QUEUE_LOCKED), QUEUED | QUEUE_LOCKED); + + loop { + let tail = unsafe { add_backlinks_and_find_tail(to_node(state)) }; + + if state.addr() & LOCKED == LOCKED { + // Another thread has locked the lock. Leave waking up waiters + // to them by releasing the queue lock. + match self.state.compare_exchange_weak( + state, + state.mask(!QUEUE_LOCKED), + Release, + Acquire, + ) { + Ok(_) => return, + Err(new) => { + state = new; + continue; + } + } + } + + let is_writer = unsafe { tail.as_ref().write }; + if is_writer && let Some(prev) = unsafe { tail.as_ref().prev.get() } { + // `tail` is a writer and there is a node before `tail`. + // Split off `tail`. + + // There are no set `tail` links before the node pointed to by + // `state`, so the first non-null tail field will be current + // (invariant 2). Invariant 4 is fullfilled since `find_tail` + // was called on this node, which ensures all backlinks are set. + unsafe { + to_node(state).as_ref().tail.set(Some(prev)); + } + + // Release the queue lock. Doing this by subtraction is more + // efficient on modern processors since it is a single instruction + // instead of an update loop, which will fail if new threads are + // added to the list. + self.state.fetch_byte_sub(QUEUE_LOCKED, Release); + + // The tail was split off and the lock released. Mark the node as + // completed. + unsafe { + return Node::complete(tail); + } + } else { + // The next waiter is a reader or the queue only consists of one + // waiter. Just wake all threads. + + // The lock cannot be locked (checked above), so mark it as + // unlocked to reset the queue. + if let Err(new) = + self.state.compare_exchange_weak(state, UNLOCKED, Release, Acquire) + { + state = new; + continue; + } + + let mut current = tail; + loop { + let prev = unsafe { current.as_ref().prev.get() }; + unsafe { + Node::complete(current); + } + match prev { + Some(prev) => current = prev, + None => return, + } + } + } + } + } +} diff --git a/library/std/src/sys/sync/rwlock/sgx.rs b/library/std/src/sys/sync/rwlock/sgx.rs new file mode 100644 index 00000000000..136dea597bb --- /dev/null +++ b/library/std/src/sys/sync/rwlock/sgx.rs @@ -0,0 +1,219 @@ +#[cfg(test)] +mod tests; + +use crate::alloc::Layout; +use crate::num::NonZero; +use crate::sys::pal::waitqueue::{ + try_lock_or_false, NotifiedTcs, SpinMutex, SpinMutexGuard, WaitQueue, WaitVariable, +}; +use crate::sys_common::lazy_box::{LazyBox, LazyInit}; + +struct AllocatedRwLock { + readers: SpinMutex<WaitVariable<Option<NonZero<usize>>>>, + writer: SpinMutex<WaitVariable<bool>>, +} + +pub struct RwLock { + inner: LazyBox<AllocatedRwLock>, +} + +impl LazyInit for AllocatedRwLock { + fn init() -> Box<Self> { + Box::new(AllocatedRwLock { + readers: SpinMutex::new(WaitVariable::new(None)), + writer: SpinMutex::new(WaitVariable::new(false)), + }) + } +} + +// Check at compile time that RwLock's size and alignment matches the C definition +// in libunwind (see also `test_c_rwlock_initializer` in `tests`). +const _: () = { + let rust = Layout::new::<RwLock>(); + let c = Layout::new::<*mut ()>(); + assert!(rust.size() == c.size()); + assert!(rust.align() == c.align()); +}; + +impl RwLock { + pub const fn new() -> RwLock { + RwLock { inner: LazyBox::new() } + } + + #[inline] + pub fn read(&self) { + let lock = &*self.inner; + let mut rguard = lock.readers.lock(); + let wguard = lock.writer.lock(); + if *wguard.lock_var() || !wguard.queue_empty() { + // Another thread has or is waiting for the write lock, wait + drop(wguard); + WaitQueue::wait(rguard, || {}); + // Another thread has passed the lock to us + } else { + // No waiting writers, acquire the read lock + *rguard.lock_var_mut() = NonZero::new(rguard.lock_var().map_or(0, |n| n.get()) + 1); + } + } + + #[inline] + pub unsafe fn try_read(&self) -> bool { + let lock = &*self.inner; + let mut rguard = try_lock_or_false!(lock.readers); + let wguard = try_lock_or_false!(lock.writer); + if *wguard.lock_var() || !wguard.queue_empty() { + // Another thread has or is waiting for the write lock + false + } else { + // No waiting writers, acquire the read lock + *rguard.lock_var_mut() = NonZero::new(rguard.lock_var().map_or(0, |n| n.get()) + 1); + true + } + } + + #[inline] + pub fn write(&self) { + let lock = &*self.inner; + let rguard = lock.readers.lock(); + let mut wguard = lock.writer.lock(); + if *wguard.lock_var() || rguard.lock_var().is_some() { + // Another thread has the lock, wait + drop(rguard); + WaitQueue::wait(wguard, || {}); + // Another thread has passed the lock to us + } else { + // We are just now obtaining the lock + *wguard.lock_var_mut() = true; + } + } + + #[inline] + pub fn try_write(&self) -> bool { + let lock = &*self.inner; + let rguard = try_lock_or_false!(lock.readers); + let mut wguard = try_lock_or_false!(lock.writer); + if *wguard.lock_var() || rguard.lock_var().is_some() { + // Another thread has the lock + false + } else { + // We are just now obtaining the lock + *wguard.lock_var_mut() = true; + true + } + } + + #[inline] + unsafe fn __read_unlock( + &self, + mut rguard: SpinMutexGuard<'_, WaitVariable<Option<NonZero<usize>>>>, + wguard: SpinMutexGuard<'_, WaitVariable<bool>>, + ) { + *rguard.lock_var_mut() = NonZero::new(rguard.lock_var().unwrap().get() - 1); + if rguard.lock_var().is_some() { + // There are other active readers + } else { + if let Ok(mut wguard) = WaitQueue::notify_one(wguard) { + // A writer was waiting, pass the lock + *wguard.lock_var_mut() = true; + wguard.drop_after(rguard); + } else { + // No writers were waiting, the lock is released + rtassert!(rguard.queue_empty()); + } + } + } + + #[inline] + pub unsafe fn read_unlock(&self) { + let lock = &*self.inner; + let rguard = lock.readers.lock(); + let wguard = lock.writer.lock(); + unsafe { self.__read_unlock(rguard, wguard) }; + } + + #[inline] + unsafe fn __write_unlock( + &self, + rguard: SpinMutexGuard<'_, WaitVariable<Option<NonZero<usize>>>>, + wguard: SpinMutexGuard<'_, WaitVariable<bool>>, + ) { + match WaitQueue::notify_one(wguard) { + Err(mut wguard) => { + // No writers waiting, release the write lock + *wguard.lock_var_mut() = false; + if let Ok(mut rguard) = WaitQueue::notify_all(rguard) { + // One or more readers were waiting, pass the lock to them + if let NotifiedTcs::All { count } = rguard.notified_tcs() { + *rguard.lock_var_mut() = Some(count) + } else { + unreachable!() // called notify_all + } + rguard.drop_after(wguard); + } else { + // No readers waiting, the lock is released + } + } + Ok(wguard) => { + // There was a thread waiting for write, just pass the lock + wguard.drop_after(rguard); + } + } + } + + #[inline] + pub unsafe fn write_unlock(&self) { + let lock = &*self.inner; + let rguard = lock.readers.lock(); + let wguard = lock.writer.lock(); + unsafe { self.__write_unlock(rguard, wguard) }; + } + + // only used by __rust_rwlock_unlock below + #[inline] + #[cfg_attr(test, allow(dead_code))] + unsafe fn unlock(&self) { + let lock = &*self.inner; + let rguard = lock.readers.lock(); + let wguard = lock.writer.lock(); + if *wguard.lock_var() == true { + unsafe { self.__write_unlock(rguard, wguard) }; + } else { + unsafe { self.__read_unlock(rguard, wguard) }; + } + } +} + +// The following functions are needed by libunwind. These symbols are named +// in pre-link args for the target specification, so keep that in sync. +#[cfg(not(test))] +const EINVAL: i32 = 22; + +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __rust_rwlock_rdlock(p: *mut RwLock) -> i32 { + if p.is_null() { + return EINVAL; + } + unsafe { (*p).read() }; + return 0; +} + +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __rust_rwlock_wrlock(p: *mut RwLock) -> i32 { + if p.is_null() { + return EINVAL; + } + unsafe { (*p).write() }; + return 0; +} + +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __rust_rwlock_unlock(p: *mut RwLock) -> i32 { + if p.is_null() { + return EINVAL; + } + unsafe { (*p).unlock() }; + return 0; +} diff --git a/library/std/src/sys/sync/rwlock/sgx/tests.rs b/library/std/src/sys/sync/rwlock/sgx/tests.rs new file mode 100644 index 00000000000..5fd6670afd4 --- /dev/null +++ b/library/std/src/sys/sync/rwlock/sgx/tests.rs @@ -0,0 +1,21 @@ +use super::*; +use crate::ptr; + +// Verify that the byte pattern libunwind uses to initialize an RwLock is +// equivalent to the value of RwLock::new(). If the value changes, +// `src/UnwindRustSgx.h` in libunwind needs to be changed too. +#[test] +fn test_c_rwlock_initializer() { + const C_RWLOCK_INIT: *mut () = ptr::null_mut(); + + // For the test to work, we need the padding/unused bytes in RwLock to be + // initialized as 0. In practice, this is the case with statics. + static RUST_RWLOCK_INIT: RwLock = RwLock::new(); + + unsafe { + // If the assertion fails, that not necessarily an issue with the value + // of C_RWLOCK_INIT. It might just be an issue with the way padding + // bytes are initialized in the test code. + assert_eq!(crate::mem::transmute_copy::<_, *mut ()>(&RUST_RWLOCK_INIT), C_RWLOCK_INIT); + }; +} diff --git a/library/std/src/sys/sync/rwlock/solid.rs b/library/std/src/sys/sync/rwlock/solid.rs new file mode 100644 index 00000000000..9bf6f5dbb73 --- /dev/null +++ b/library/std/src/sys/sync/rwlock/solid.rs @@ -0,0 +1,93 @@ +//! A readers-writer lock implementation backed by the SOLID kernel extension. +use crate::sys::pal::{ + abi, + itron::{ + error::{expect_success, expect_success_aborting, fail, ItronError}, + spin::SpinIdOnceCell, + }, +}; + +pub struct RwLock { + /// The ID of the underlying mutex object + rwl: SpinIdOnceCell<()>, +} + +// Safety: `num_readers` is protected by `mtx_num_readers` +unsafe impl Send for RwLock {} +unsafe impl Sync for RwLock {} + +fn new_rwl() -> Result<abi::ID, ItronError> { + ItronError::err_if_negative(unsafe { abi::rwl_acre_rwl() }) +} + +impl RwLock { + #[inline] + pub const fn new() -> RwLock { + RwLock { rwl: SpinIdOnceCell::new() } + } + + /// Get the inner mutex's ID, which is lazily created. + fn raw(&self) -> abi::ID { + match self.rwl.get_or_try_init(|| new_rwl().map(|id| (id, ()))) { + Ok((id, ())) => id, + Err(e) => fail(e, &"rwl_acre_rwl"), + } + } + + #[inline] + pub fn read(&self) { + let rwl = self.raw(); + expect_success(unsafe { abi::rwl_loc_rdl(rwl) }, &"rwl_loc_rdl"); + } + + #[inline] + pub fn try_read(&self) -> bool { + let rwl = self.raw(); + match unsafe { abi::rwl_ploc_rdl(rwl) } { + abi::E_TMOUT => false, + er => { + expect_success(er, &"rwl_ploc_rdl"); + true + } + } + } + + #[inline] + pub fn write(&self) { + let rwl = self.raw(); + expect_success(unsafe { abi::rwl_loc_wrl(rwl) }, &"rwl_loc_wrl"); + } + + #[inline] + pub fn try_write(&self) -> bool { + let rwl = self.raw(); + match unsafe { abi::rwl_ploc_wrl(rwl) } { + abi::E_TMOUT => false, + er => { + expect_success(er, &"rwl_ploc_wrl"); + true + } + } + } + + #[inline] + pub unsafe fn read_unlock(&self) { + let rwl = self.raw(); + expect_success_aborting(unsafe { abi::rwl_unl_rwl(rwl) }, &"rwl_unl_rwl"); + } + + #[inline] + pub unsafe fn write_unlock(&self) { + let rwl = self.raw(); + expect_success_aborting(unsafe { abi::rwl_unl_rwl(rwl) }, &"rwl_unl_rwl"); + } +} + +impl Drop for RwLock { + #[inline] + fn drop(&mut self) { + if let Some(rwl) = self.rwl.get().map(|x| x.0) { + expect_success_aborting(unsafe { abi::rwl_del_rwl(rwl) }, &"rwl_del_rwl"); + } + } +} diff --git a/library/std/src/sys/sync/rwlock/teeos.rs b/library/std/src/sys/sync/rwlock/teeos.rs new file mode 100644 index 00000000000..ef9b1ab5154 --- /dev/null +++ b/library/std/src/sys/sync/rwlock/teeos.rs @@ -0,0 +1,44 @@ +use crate::sys::sync::mutex::Mutex; + +/// we do not supported rwlock, so use mutex to simulate rwlock. +/// it's useful because so many code in std will use rwlock. +pub struct RwLock { + inner: Mutex, +} + +impl RwLock { + #[inline] + pub const fn new() -> RwLock { + RwLock { inner: Mutex::new() } + } + + #[inline] + pub fn read(&self) { + unsafe { self.inner.lock() }; + } + + #[inline] + pub fn try_read(&self) -> bool { + unsafe { self.inner.try_lock() } + } + + #[inline] + pub fn write(&self) { + unsafe { self.inner.lock() }; + } + + #[inline] + pub unsafe fn try_write(&self) -> bool { + unsafe { self.inner.try_lock() } + } + + #[inline] + pub unsafe fn read_unlock(&self) { + unsafe { self.inner.unlock() }; + } + + #[inline] + pub unsafe fn write_unlock(&self) { + unsafe { self.inner.unlock() }; + } +} diff --git a/library/std/src/sys/sync/rwlock/windows7.rs b/library/std/src/sys/sync/rwlock/windows7.rs new file mode 100644 index 00000000000..e69415baac4 --- /dev/null +++ b/library/std/src/sys/sync/rwlock/windows7.rs @@ -0,0 +1,40 @@ +use crate::cell::UnsafeCell; +use crate::sys::c; + +pub struct RwLock { + inner: UnsafeCell<c::SRWLOCK>, +} + +unsafe impl Send for RwLock {} +unsafe impl Sync for RwLock {} + +impl RwLock { + #[inline] + pub const fn new() -> RwLock { + RwLock { inner: UnsafeCell::new(c::SRWLOCK_INIT) } + } + #[inline] + pub fn read(&self) { + unsafe { c::AcquireSRWLockShared(self.inner.get()) } + } + #[inline] + pub fn try_read(&self) -> bool { + unsafe { c::TryAcquireSRWLockShared(self.inner.get()) != 0 } + } + #[inline] + pub fn write(&self) { + unsafe { c::AcquireSRWLockExclusive(self.inner.get()) } + } + #[inline] + pub fn try_write(&self) -> bool { + unsafe { c::TryAcquireSRWLockExclusive(self.inner.get()) != 0 } + } + #[inline] + pub unsafe fn read_unlock(&self) { + c::ReleaseSRWLockShared(self.inner.get()) + } + #[inline] + pub unsafe fn write_unlock(&self) { + c::ReleaseSRWLockExclusive(self.inner.get()) + } +} diff --git a/library/std/src/sys/sync/rwlock/xous.rs b/library/std/src/sys/sync/rwlock/xous.rs new file mode 100644 index 00000000000..ab45b33e1f6 --- /dev/null +++ b/library/std/src/sys/sync/rwlock/xous.rs @@ -0,0 +1,74 @@ +use crate::sync::atomic::{AtomicIsize, Ordering::Acquire}; +use crate::thread::yield_now; + +pub struct RwLock { + /// The "mode" value indicates how many threads are waiting on this + /// Mutex. Possible values are: + /// -1: The lock is locked for writing + /// 0: The lock is unlocked + /// >=1: The lock is locked for reading + /// + /// This currently spins waiting for the lock to be freed. An + /// optimization would be to involve the ticktimer server to + /// coordinate unlocks. + mode: AtomicIsize, +} + +const RWLOCK_WRITING: isize = -1; +const RWLOCK_FREE: isize = 0; + +unsafe impl Send for RwLock {} +unsafe impl Sync for RwLock {} + +impl RwLock { + #[inline] + #[rustc_const_stable(feature = "const_locks", since = "1.63.0")] + pub const fn new() -> RwLock { + RwLock { mode: AtomicIsize::new(RWLOCK_FREE) } + } + + #[inline] + pub unsafe fn read(&self) { + while !unsafe { self.try_read() } { + yield_now(); + } + } + + #[inline] + pub unsafe fn try_read(&self) -> bool { + self.mode + .fetch_update( + Acquire, + Acquire, + |v| if v == RWLOCK_WRITING { None } else { Some(v + 1) }, + ) + .is_ok() + } + + #[inline] + pub unsafe fn write(&self) { + while !unsafe { self.try_write() } { + yield_now(); + } + } + + #[inline] + pub unsafe fn try_write(&self) -> bool { + self.mode.compare_exchange(RWLOCK_FREE, RWLOCK_WRITING, Acquire, Acquire).is_ok() + } + + #[inline] + pub unsafe fn read_unlock(&self) { + let previous = self.mode.fetch_sub(1, Acquire); + assert!(previous != RWLOCK_FREE); + assert!(previous != RWLOCK_WRITING); + } + + #[inline] + pub unsafe fn write_unlock(&self) { + assert_eq!( + self.mode.compare_exchange(RWLOCK_WRITING, RWLOCK_FREE, Acquire, Acquire), + Ok(RWLOCK_WRITING) + ); + } +} |
