Stroika Library 3.0d16
 
Loading...
Searching...
No Matches
Stroika::Foundation::Execution::Synchronized< T, TRAITS > Class Template Reference

Wrap any object with Synchronized<> and it can be used similarly to the base type, but safely in a thread safe manner, from multiple threads. This is similar to std::atomic. More...

#include <Synchronized.h>

Inherits std::conditional_t< TRAITS::kDbgTraceLockUnlockIfNameSet, Private_::DbgTraceNameObj_, Common::Empty >.

Classes

class  ReadableReference
 

Public Member Functions

template<typename... ARGUMENT_TYPES>
 Synchronized (ARGUMENT_TYPES &&... args)
 
nonvirtual operator T () const
 
nonvirtual T load () const
 
nonvirtual void store (const T &v)
 
nonvirtual ReadableReference cget () const
 get a read-only smart pointer to the underlying Synchronized<> object, holding the readlock the whole time the return (often temporary) ReadableReference exists.
 
nonvirtual WritableReference rwget ()
 get a read-write smart pointer to the underlying Synchronized<> object, holding the full lock the whole time the (often temporary) WritableReference exists.
 
nonvirtual void lock_shared () const
 
nonvirtual void unlock_shared () const
 
nonvirtual void lock () const
 
nonvirtual bool try_lock () const
 
nonvirtual bool try_lock_for (const chrono::duration< double > &tryFor) const
 
nonvirtual void unlock () const
 
nonvirtual bool UpgradeLockNonAtomicallyQuietly (ReadableReference *lockBeingUpgraded, const function< void(WritableReference &&)> &doWithWriteLock, Time::DurationSeconds timeout=Time::kInfinity)
 Upgrade a shared_lock (ReadableReference) to a (WritableReference) full lock temporarily in the context of argument function; return true if succeeds, and false if fails (timeout trying to full-lock) or argument doWithWriteLock () returns false.
 
nonvirtual void UpgradeLockNonAtomically (ReadableReference *lockBeingUpgraded, const function< void(WritableReference &&)> &doWithWriteLock, Time::DurationSeconds timeout=Time::kInfinity)
 Same as UpgradeLockNonAtomicallyQuietly, but throws on failure (either timeout or if argument function returns false)
 

Detailed Description

template<typename T, typename TRAITS = Synchronized_Traits<>>
class Stroika::Foundation::Execution::Synchronized< T, TRAITS >

Wrap any object with Synchronized<> and it can be used similarly to the base type, but safely in a thread safe manner, from multiple threads. This is similar to std::atomic.

The idea behind any of these synchronized classes is that they can be used freely from different threads without worry of data corruption. It is almost as if each operation were preceeded with a mutex lock (on that object) and followed by an unlock.

If one thread does a Read operation on Synchronized<T> while another does a write (modification) operation on Synchronized<T>, the Read will always return a consistent reasonable value, from before the modification or afterwards, but never a distorted invalid value.

This is very much closely logically the java 'synchronized' attribute, except that its not a language extension/attribute here, but rather a class wrapper. Its also implemented in the library, not the programming language.

Note
LIKE JAVA SYNCHRONIZED This is SIMPLE to use like the Java (and .net) synchronized attribute(lock) mechanism. But why does it not suffer from the same performance defect?

Because with Java - you mix up exceptions and assertions. With Stroika, we have builtin checking for races (Debug::AssertExternallySynchronizedMutex) in most objects, so you only use Synchronized<> (or some other more performant mechanism) in the few places you need it.

Note
Synchronized<> is similar to std::atomic, except that
  • You can use it as a mutex with lock_guard and lock for an extended period of time.
  • This supports read/write locks.
  • This supports locking objects and updated bits of them - not just replacing all at one go
Example Usage
Wrap any object with Synchronized<> and it can be used similarly to the base type,...

or slightly faster, but possibly slower or less safe (depending on usage)

or to allow timed locks

or to read-modify-update in place

//nb: lock lasts til end of enclosing scope
auto lockedConfigData = fConfig_.rwget ();
lockedConfigData->SomeValueChanged = 1;

or to read-modify-update with explicit temporary

//nb: lock lasts til end of enclosing scope
auto lockedConfigData = fConfig_.rwget ();
T value = lockedConfigData;
value.SomeValueChanged = 1;
lockedConfigData.store (value);

or, because Synchronized<> has lock/unlock methods, it can be used with a lock_guard (if associated mutex recursive), as in:

auto&& critSec = lock_guard{n};
// lock to make sure someone else doesn't change n after we made sure it looked good
if (looks_good (n)) {
String a = n;
a = a + a;
n.store (a);
}
String is like std::u32string, except it is much easier to use, often much more space efficient,...
Definition String.h:201
Note
We consider supporting operator-> for Synchronized<> - and overloading on const to see if we use a Read Lock or a Write lock. The problem is - that IF its called through a non-const object, it will select the non-const (write lock) even though all that was needed was the read lock! So this paradigm - though more terse and clear - just encourages inefficient coding (so we have no read locks - all locks write locks).

So ONLY support operator-> const overload (brevity and more common than for write). To write - use rwget().

Note
Upgrading a shared_lock to a full lock We experimented with using boost upgrade_lock code to allow for a full upgrade capability, but this intrinsically can (easily) yield deadlocks (e.g. thread A holds read lock and tries to upgrade, while thread B holds shared_lock and waits on something from thread A), and so I decided to abandon this approach.

Instead, just have upgradeLock release the shared_lock, and re-acquire the mutex as a full lock. BUT - this has problems too. Typically - you compute something with the shared_lock and notice you want to commit a change, and so upgrade to get the full lock. But when you do the upgrade, someone else could sneak in and do the same thing invalidating your earlier computation.

So - the Upgrade lock APIS have the word "NON_ATOMICALLY" in the name to emphasize this issue, and either return a boolean indicating failure, or take a callback that gets notified of the need to recompute the cached value/data.

Note
Comparisons: o static_assert (equality_comparable<T> and TRAITS::kIsRecursiveReadMutex ==> equality_comparable<Synchronized<T>>); o static_assert (totally_ordered<T> and TRAITS::kIsRecursiveReadMutex ==> totally_ordered<Synchronized<T>>);

Definition at line 241 of file Synchronized.h.

Constructor & Destructor Documentation

◆ Synchronized()

template<typename T , typename TRAITS >
template<typename... ARGUMENT_TYPES>
Stroika::Foundation::Execution::Synchronized< T, TRAITS >::Synchronized ( ARGUMENT_TYPES &&...  args)

Create a Synchronized with any argument type the underlying type will be constructed with the same (perfectly) forwarded arguments.

And plain copy constructor.

FOR NOW, avoid MOVE constructor (cuz synchronized is a combination of original data with lock, and not sure what moving the lock means).

Definition at line 30 of file Synchronized.inl.

Member Function Documentation

◆ operator T()

template<typename T , typename TRAITS >
requires (TRAITS::kIsRecursiveReadMutex)
Stroika::Foundation::Execution::Synchronized< T, TRAITS >::operator T ( ) const

Synonym for load ()

Note
Tentatively (as of v2.0a155) - decided to do with non-explicit. This makes usage more terse for cases like: Synchronized<T> sX_; T Accessor () { return sX_; } and so far has caused no obvious problems.
This works only for 'recursive' mutexes (the default, except for RWSynchronized). To avoid the absence of this feature (say with RWSynchronized) - use cget ().load (); The reason this is only defined for recursive mutexes is so that it can be used in a context where this thread already has a lock (e.g. called rwget ()).

Definition at line 73 of file Synchronized.inl.

◆ load()

template<typename T , typename TRAITS >
requires (TRAITS::kIsRecursiveReadMutex)
T Stroika::Foundation::Execution::Synchronized< T, TRAITS >::load ( ) const

Note - unlike operator->, load () returns a copy of the internal data, and only locks while fetching it, so that the lock does not persist while using the result.

Note
Using load () can be most efficient (least lock contention) with read/write locks (
See also
RWSynchronized), since it just uses a read lock and releases it immediately.
Example Usage
sharedData.load ().AbortAndWaitTilDone (); // copies Thread::Ptr and doesn't maintain lock during wait
sharedData->AbortAndWaitTilDone (); // works off internal copy of thread object, and maintains the lock while accessing
Note
This works only for 'recursive' mutexes (the default, except for RWSynchronized). To avoid the absence of this feature (e.g. with RWSynchronized<T>) - use cget ().load (), or existingLock.load (); The reason this is only defined for recursive mutexes is so that it can be used in a context where this thread already has a lock (e.g. called rwget ()).

Definition at line 79 of file Synchronized.inl.

◆ store()

template<typename T , typename TRAITS >
requires (TRAITS::kIsRecursiveLockMutex)
void Stroika::Foundation::Execution::Synchronized< T, TRAITS >::store ( const T &  v)
See also
load ()

Save the given value into this synchronized object, acquiring the needed write lock first.

Note
This works only for 'recursive' mutexes (the default, except for RWSynchronized). To avoid the absence of this feature (say with RWSynchronized) - use rwget ().store (); The reason this is only defined for recursive mutexes is so that it can be used in a context where this thread already has a lock (e.g. called rwget ()).

Definition at line 96 of file Synchronized.inl.

◆ cget()

template<typename T , typename TRAITS >
auto Stroika::Foundation::Execution::Synchronized< T, TRAITS >::cget ( ) const

get a read-only smart pointer to the underlying Synchronized<> object, holding the readlock the whole time the return (often temporary) ReadableReference exists.

Note
this supports multiple readers/single writer, iff the mutex used with Synchronized<> supports it (
See also
Synchronized)
Example Usage
auto lockedConfigData = fConfig_.cget ();
fCurrentCell_ = lockedConfigData->fCell.Value (Cell::Short);
fCurrentPressure_ = lockedConfigData->fPressure.Value (Pressure::Low);

This is roughly equivalent (if using a recursive mutex) to (COUNTER_EXAMPLE):

lock_guard<Synchronized<T,TRAITS>> critSec{fConfig_};
fCurrentCell_ = fConfig_->fCell.Value (Cell::Short);
fCurrentPressure_ = fConfig_->fPressure.Value (Pressure::Low);

NOTE - THIS EXAMPLE USAGE IS UNSAFE - DONT DO!

auto danglingCRef = fConfig_.cget ().cref(); // the cref() is reference is only valid til the end of the full expression
use (danglingCRef); // BAD cref dangles after end of previous expression;

Except that this works whether using a shared_mutex or regular mutex. Also - this provides only read-only access (use rwget for read-write access).

Note
- this creates a lock, so be sure TRAITS::kIsRecursiveReadMutex if using this in a place where the same thread may have a lock.

Definition at line 142 of file Synchronized.inl.

◆ rwget()

template<typename T , typename TRAITS >
auto Stroika::Foundation::Execution::Synchronized< T, TRAITS >::rwget ( )

get a read-write smart pointer to the underlying Synchronized<> object, holding the full lock the whole time the (often temporary) WritableReference exists.

Note
- this creates a lock, so be sure TRAITS::kIsRecursiveLockMutex if using this in a place where the same thread may have a lock.

Definition at line 157 of file Synchronized.inl.

◆ lock_shared()

template<typename T , typename TRAITS >
requires (TRAITS::kIsRecursiveReadMutex and TRAITS::kSupportSharedLocks)
void Stroika::Foundation::Execution::Synchronized< T, TRAITS >::lock_shared ( ) const
Note
lock_shared () only works for 'recursive' mutexes which supported 'shared lock'. To avoid the absence of this feature (say with RWSynchronized) - use rwget () or cget ();
- This is only usable with TRAITS::kIsRecursiveReadMutex, because there would be no way to access the underlying value otherwise.

Definition at line 177 of file Synchronized.inl.

◆ unlock_shared()

template<typename T , typename TRAITS >
requires (TRAITS::kIsRecursiveReadMutex and TRAITS::kSupportSharedLocks)
void Stroika::Foundation::Execution::Synchronized< T, TRAITS >::unlock_shared ( ) const
Note
unlock_shared () only works for 'recursive' mutexes which supported 'shared lock'. To avoid the absence of this feature (say with RWSynchronized) - use rwget () or cget ();
- This is only usable with TRAITS::kIsRecursiveReadMutex, because there would be no way to access the underlying value otherwise.

Definition at line 187 of file Synchronized.inl.

◆ lock()

template<typename T , typename TRAITS >
requires (TRAITS::kIsRecursiveReadMutex)
void Stroika::Foundation::Execution::Synchronized< T, TRAITS >::lock ( ) const
Note
This works only for 'recursive' mutexes (the default, except for RWSynchronized). To avoid the absence of this feature (e.g. with RWSynchronized<T>) - use cget (), or rwget ().
- This is only usable with TRAITS::kIsRecursiveLockMutex, because there would be no way to access the underlying value otherwise.

Definition at line 197 of file Synchronized.inl.

◆ try_lock()

template<typename T , typename TRAITS >
requires (TRAITS::kIsRecursiveReadMutex)
bool Stroika::Foundation::Execution::Synchronized< T, TRAITS >::try_lock ( ) const
Note
This works only for 'recursive' mutexes (the default, except for RWSynchronized). To avoid the absence of this feature (e.g. with RWSynchronized<T>) - use cget (), or rwget ().
- This is only usable with TRAITS::kIsRecursiveLockMutex, because there would be no way to access the underlying value otherwise.

Definition at line 208 of file Synchronized.inl.

◆ try_lock_for()

template<typename T , typename TRAITS >
requires (TRAITS::kIsRecursiveReadMutex and TRAITS::kSupportsTimedLocks)
bool Stroika::Foundation::Execution::Synchronized< T, TRAITS >::try_lock_for ( const chrono::duration< double > &  tryFor) const
Note
This works only for 'recursive' mutexes (the default, except for RWSynchronized). To avoid the absence of this feature (e.g. with RWSynchronized<T>) - use cget (), or rwget ().
- This is only usable with TRAITS::kIsRecursiveLockMutex, because there would be no way to access the underlying value otherwise.

Definition at line 222 of file Synchronized.inl.

◆ unlock()

template<typename T , typename TRAITS >
requires (TRAITS::kIsRecursiveReadMutex)
void Stroika::Foundation::Execution::Synchronized< T, TRAITS >::unlock ( ) const
Note
This works only for 'recursive' mutexes (the default, except for RWSynchronized). To avoid the absence of this feature (e.g. with RWSynchronized<T>) - use cget (), or rwget ().
- This is only usable with TRAITS::kIsRecursiveLockMutex, because there would be no way to access the underlying value otherwise.

Definition at line 236 of file Synchronized.inl.

◆ UpgradeLockNonAtomicallyQuietly()

template<typename T , typename TRAITS >
requires (TRAITS::kSupportSharedLocks and TRAITS::kSupportsTimedLocks)
bool Stroika::Foundation::Execution::Synchronized< T, TRAITS >::UpgradeLockNonAtomicallyQuietly ( ReadableReference lockBeingUpgraded,
const function< void(WritableReference &&)> &  doWithWriteLock,
Time::DurationSeconds  timeout = Time::kInfinity 
)

Upgrade a shared_lock (ReadableReference) to a (WritableReference) full lock temporarily in the context of argument function; return true if succeeds, and false if fails (timeout trying to full-lock) or argument doWithWriteLock () returns false.

See also
UpgradeLockNonAtomically - to just calls UpgradeLockNonAtomicallyQuietly () and throws timeout on timeout intervening WriteLock or doWithWriteLock returns false

A DEFEFCT with (this) UpgradeLockNonAtomically API, is that you cannot count on values computed with the read lock to remain valid in the upgrade lock (since we unlock and then re-lock). We resolve this by having two versions of UpgradeLockNonAtomically, one where the callback gets notified there was an intervening writelock, and one where the entire call fails and you have to re-run.

Note
optional 'bool interveningWriteLock' parameter - if present, intervening locks are flagged with this parameter, and if the parameter is NOT present, intervening locks are treated as timeouts (even if infinite timeout specified)
- also returns false on intervening lock IFF doWithWriteLock/1 passed in has no intervening WriteLock parameter.
optional 'bool interveningWriteLock' parameter - if present, intervening locks are flagged with this parameter, and if the parameter is NOT present, intervening locks are treated as timeouts (even if infinite timeout specified)
- This does NOT require the mutex be recursive - just supporting both lock_shared and lock ()
- This function takes as argument an existing ReadableReference, which MUST come from a cget on this Synchronized object (and therefore must be locked) and DURING the context of this function call that becomes invalid, but when this call returns it will still be locked READONLY. This does NOT change the lock to writable (after the call) - but ONLY during the call of the argument function.
Example Usage
...
again:
auto lockedStatus = fStatus_.cget ();
// do a bunch of code that only needs read access
if (some rare event) {
if (not fStatus_.UpgradeLockNonAtomicallyQuietly (&lockedStatus, [=](auto&& writeLock) {
writeLock.rwref ().fCompletedScans.Add (scan);
}
)) {
goto again; // important to goto before we acquire readlock to avoid deadlock when multiple threads do this
}
}
nonvirtual bool UpgradeLockNonAtomicallyQuietly(ReadableReference *lockBeingUpgraded, const function< void(WritableReference &&)> &doWithWriteLock, Time::DurationSeconds timeout=Time::kInfinity)
Upgrade a shared_lock (ReadableReference) to a (WritableReference) full lock temporarily in the conte...
nonvirtual ReadableReference cget() const
get a read-only smart pointer to the underlying Synchronized<> object, holding the readlock the whole...
void Sleep(Time::Duration seconds2Wait)
Definition Sleep.cpp:18

Definition at line 270 of file Synchronized.inl.

◆ UpgradeLockNonAtomically()

template<typename T , typename TRAITS >
requires (TRAITS::kSupportSharedLocks and TRAITS::kSupportsTimedLocks)
void Stroika::Foundation::Execution::Synchronized< T, TRAITS >::UpgradeLockNonAtomically ( ReadableReference lockBeingUpgraded,
const function< void(WritableReference &&)> &  doWithWriteLock,
Time::DurationSeconds  timeout = Time::kInfinity 
)

Same as UpgradeLockNonAtomicallyQuietly, but throws on failure (either timeout or if argument function returns false)

Note
- the 'ReadableReference' must be shared_locked coming in, and will be identically shared_locked on return.
- throws on timeout OR if interveningWriteLock and doWithWriteLock/1 passed, or if doWithWriteLock returns false
- This does NOT require the mutex be recursive - just supporting both lock_shared and lock ()
- the timeout refers ONLY the acquiring the upgrade - not the time it takes to re-acquire the shared lock or perform the argument operation
Example Usage
auto lockedStatus = fStatus_.cget ();
// do a bunch of code that only needs read access
if (some rare event) {
// This COULD fail/throw only if called from intervening lock
fStatus_.UpgradeLockNonAtomically ([=](auto&& writeLock) {
writeLock.rwref ().fCompletedScans.Add (scan);
});
}
nonvirtual void UpgradeLockNonAtomically(ReadableReference *lockBeingUpgraded, const function< void(WritableReference &&)> &doWithWriteLock, Time::DurationSeconds timeout=Time::kInfinity)
Same as UpgradeLockNonAtomicallyQuietly, but throws on failure (either timeout or if argument functio...

Definition at line 327 of file Synchronized.inl.


The documentation for this class was generated from the following files: