Semaphore Helpers |
Five types of semaphore helpers are discussed in this section.
The account
class contains several examples in which a
semaphore is locked when a scope is entered and unlocked when the scope is
exited. Because this is a common operation, the os_sem_lock
family of classes is supplied to conveniently and
automatically perform the scoped locking and unlocking.
The constructor of an os_sem_lock
locks its parameter; its destructor unlocks its
parameter. The os_sem_lock is templatized on the
semaphore type, so it can be used to control any kind of semaphore. Following
is a list of the functions each kind of lock executes on its parameter during
construction and destruction.
| Class | Construction | Destruction |
|---|---|---|
In the following example, the account
class uses automatic locking and unlocking.
#include <iostream>
#include <ospace/thread.h>
class account
{
public:
account() :
balance_( 0 )
{
}
int balance() const
{
os_sem_read_lock lock( lock_ );
cout << "read locked by thread " << os_this_thread::tid() << endl;
int tmp = balance_;
os_this_thread::sleep( 2 );
cout << "read auto-unlock account" << endl;
return tmp;
}
void deposit( int n )
{
os_sem_write_lock lock( lock_ );
cout << "write locked by thread " << os_this_thread::tid()
<< endl;
cout << "increment balance of " << balance_ << " by " << n << endl;
balance_ += n;
cout << "Balance is now " << balance_ << endl;
os_this_thread::sleep( 2 );
cout << "write auto-unlock account" << endl;
}
int withdraw( int n )
{
os_sem_write_lock lock( lock_ );
cout << "write locked by thread " << os_this_thread::tid()
<< endl;
cout << "attempt to withdraw " << n;
cout << " from balance of " << balance_ << endl;
int result = 0;
if ( balance_ >= n )
{
balance_ -= n;
cout << "success: balance is now " << balance_ << endl;
result = n;
}
else
{
cout << "failure: balance left at " << balance_ << endl;
}
os_this_thread::sleep( 2 );
cout << "write auto-unlock account" << endl;
return result;
}
private:
os_read_write_semaphore lock_;
int balance_;
};
account shared_account; // Global shared account.
void*
writer( void* args )
{
shared_account.deposit( 50 );
os_this_thread::sleep( 1 );
shared_account.withdraw( 80 );
return 0;
}
void*
reader( void* args )
{
int balance = shared_account.balance();
cout << "balance is " << balance << endl;
return 0;
}
void
main()
{
os_thread_toolkit initialize;
os_thread a( writer ); // Spawn new thread.
os_thread b( reader ); // Spawn new thread.
os_thread c( writer ); // Spawn new thread.
os_thread d( reader ); // Spawn new thread.
os_this_thread::wait_for_thread( a );
os_this_thread::wait_for_thread( b );
}
write locked by thread 4
increment balance of 0 by 50
Balance is now 50
write auto-unlock account
read locked by thread 6
read locked by thread 8
read auto-unlock account
balance is 50
read auto-unlock account
balance is 50
write locked by thread 7
increment balance of 50 by 50
Balance is now 100
write auto-unlock account
write locked by thread 4
attempt to withdraw 80 from balance of 100
success: balance is now 20
write auto-unlock account
write locked by thread 7
attempt to withdraw 80 from balance of 20
failure: balance left at 20
Making a class thread-safe using the previously described synchronization approach is invasive; this approach requires inserting special synchronization code into the class. There are at least three disadvantages to this strategy.
An alternative approach is to use a monitor to read- or write-lock an object for an arbitrary period of time, without having to modify the object's class in any way. The following steps lock an object using a monitor.
os_monitor
with the type of the object to be locked as its
template parameter and the object to be locked as its single constructor
argument.write_lock()
to obtain a write lock on the monitor's object.
Only one thread can have a write lock at any given time. Use write_unlock()
to release the write lock.read_lock()
to obtain a read lock on the monitor's object.
Any number of threads can have a read lock at any given time. Use read_unlock()
to release a read lock.*
or the -> operator to access the monitor's
object. The * operator returns a reference to
the object, whereas the -> operator
returns a pointer to the object. In the following example, the account
class uses monitors. In this case, the account
class contains no synchronization code.
#include <iostream>
#include <ospace/thread.h>
class account
{
public:
account() :
balance_( 0 )
{
}
int balance() const
{
return balance_;
}
void deposit( int n )
{
cout << "Account accessed by thread " << os_this_thread::tid();
cout << endl;
cout << "Increment balance of " << balance_ << " by " << n << endl;
balance_ += n;
cout << "Balance is now " << balance_ << endl;
}
int withdraw( int n )
{
cout << "Account accessed by thread " << os_this_thread::tid();
cout << endl;
cout << "Attempt to withdraw " << n;
cout << " from balance of " << balance_ << endl;
int result = 0;
if ( balance_ >= n )
{
balance_ -= n;
cout << "Success: balance is now " << balance_ << endl;
result = n;
}
else
{
cout << "Failure: balance left at " << balance_ << endl;
}
return result;
}
private:
int balance_;
};
account account1; // Global shared account.
os_monitor< account > shared_account( account1 ); // Monitored account.
void*
writer( void* args )
{
shared_account.write_lock();
cout << "thread " << os_this_thread::tid() << " enters write" << endl;
shared_account->deposit( 50 );
os_this_thread::sleep( 1 );
shared_account->withdraw( 80 );
cout << "thread " << os_this_thread::tid() << " exits write" << endl;
shared_account.write_unlock();
return 0;
}
void*
reader( void* args )
{
shared_account.read_lock();
cout << "thread " << os_this_thread::tid() << " enters read" << endl;
cout << "balance is currently " << shared_account->balance() << endl;
os_this_thread::sleep( 1 );
cout << "thread " << os_this_thread::tid() << " exits read" << endl;
shared_account.read_unlock();
return 0;
}
void
main()
{
os_thread_toolkit initialize;
os_thread a( writer ); // Spawn writer.
os_thread b( writer ); // Spawn writer.
os_thread c( reader ); // Spawn reader.
os_thread d( reader ); // Spawn reader.
os_this_thread::wait_for_thread( a );
os_this_thread::wait_for_thread( b );
os_this_thread::wait_for_thread( c );
os_this_thread::wait_for_thread( d );
}
thread 4 enters write
Account accessed by thread 4
Increment balance of 0 by 50
Balance is now 50
Account accessed by thread 4
Attempt to withdraw 80 from balance of 50
Failure: balance left at 50
thread 4 exits write
thread 7 enters read
balance is currently 50
thread 8 enters read
balance is currently 50
thread 7 exits read
thread 8 exits read
thread 6 enters write
Account accessed by thread 6
Increment balance of 50 by 50
Balance is now 100
Account accessed by thread 6
Attempt to withdraw 80 from balance of 100
Success: balance is now 20
thread 6 exits write
An os_sentinel
uses the same approach to object synchronization as
the os_monitor class.
Additionally, an os_sentinel provides
notification when the guarded object is modified. Multiple threads can observe()
the guarded object in a blocked, idle state. Each
time the sentinel's lock is released, all blocked threads are awakened.
In this example, a sentinel is used by three thread agents to guard a shared string. Every time the string is modified, each agent is notified of the change and inspects the string for a termination value. If the string value does not match, the agent returns to an idle state until the string is modified again.
#include <iostream>
#include <string>
#include <ospace/thread.h>
os_string data = "one";
os_sentinel< os_string > object( data );
void*
agent( void* arg )
{
int id = (long) arg;
os_string value;
do
{
cout << "\tagent " << id << ": monitoring object" << endl;
object.lock();
object.observe();
value = *object;
object.unlock();
cout << "\tagent " << id << ": object has value: " << value << endl;
}
while ( value != "done" );
cout << "\tagent " << id << ": terminating" << endl;
return 0;
}
int
main()
{
os_thread_toolkit initialize;
// Create agents to monitor data
cout << "data has value " << data << endl;
os_thread agent1( agent, (void*) 1 );
os_thread agent2( agent, (void*) 2 );
os_thread agent3( agent, (void*) 3 );
// Update object value
os_this_thread::sleep( 3 );
object.lock();
*object = "two";
cout << "object updated to have value: " << *object << endl;
object.notify_unlock();
os_this_thread::sleep( 3 );
object.lock();
*object = "three";
cout << "object updated to have value: " << *object << endl;
object.notify_unlock();
os_this_thread::sleep( 3 );
object.lock();
*object = "done";
cout << "object updated to have value: " << *object << endl;
object.notify_unlock();
os_this_thread::sleep( 3 );
return 0;
}
data has value one
agent 1: monitoring object
agent 2: monitoring object
agent 3: monitoring object
object updated to have value: two
agent 1: object has value: two
agent 1: monitoring object
agent 2: object has value: two
agent 2: monitoring object
agent 3: object has value: two
agent 3: monitoring object
object updated to have value: three
agent 1: object has value: three
agent 1: monitoring object
agent 2: object has value: three
agent 2: monitoring object
agent 3: object has value: three
agent 3: monitoring object
object updated to have value: done
agent 1: object has value: done
agent 1: terminating
agent 2: object has value: done
agent 2: terminating
agent 3: object has value: done
agent 3: terminating
An os_barrier is
synchronization primitive that allows multiple threads to reach a specific
point and wait before they all can proceed. For example, threads completing a
matrix computation can wait at a barrier until all threads have finished the
current phase of the parallel computation, before they all can proceed to the
next phase of the computation. An os_barrier
object contains a counter which is initialized with a thread count when the
barrier is created. The thread count represents the number of waiting threads
that can wait in the barrier group. When each thread reaches the barrier it
calls the barrier's wait() method. The wait()
method decrements the thread counter. When the last thread reaches the barrier
and calls the wait() method, the count goes to
zero. The barrier object then unblocks all the waiting threads in the barrier
group to proceeed.
The example spawns four threads passing a pointer to a function that blocks until all thread have finished processing total. Finally, the last thread sends a message to the console indicating completion of all threads.
#include <iostream>
#include <ospace/thread.h>
os_mutex mutex;
os_barrier barrier( 4 );
unsigned int total = 0;
void*
add( void* args )
{
unsigned long amount = (unsigned long)(long*)args;
mutex.lock();
cout << "thread " << os_this_thread::tid() << " adds amount" << amount <<endl;
total += amount;
mutex.unlock();
cout << "thread " << os_this_thread::tid() << " waiting on barrier" << endl;
bool serial = barrier.wait();
if ( serial )
cout << "thread " << os_this_thread::tid()
<< " completes the barrier" << endl;
cout << "thread " << os_this_thread::tid() << " total " << total <<endl;
return 0;
}
int
main()
{
os_thread_toolkit init_thread;
os_thread t1( add, (void*) 10 ); // Spawn new thread.
os_thread t2( add, (void*) 20 ); // Spawn new thread.
os_thread t3( add, (void*) 30 ); // Spawn new thread.
os_thread t4( add, (void*) 40 ); // Spawn new thread.
os_this_thread::wait_for_thread( t1 );
os_this_thread::wait_for_thread( t2 );
os_this_thread::wait_for_thread( t3 );
os_this_thread::wait_for_thread( t4 );
return 0;
}
thread 4 adds amount10
thread 4 waiting on barrier
thread 5 adds amount20
thread 5 waiting on barrier
thread 6 adds amount30
thread 6 waiting on barrier
thread 7 adds amount40
thread 7 waiting on barrier
thread 7 completes the barrier
thread 7 total 100
thread 4 total 100
thread 5 total 100
thread 6 total 100
An os_condition is a
synchronization object that synchronizes on an arbitrary boolean condition
whose state is protected by a mutex.
A condition variable enables threads to test an arbitrary condition under the
protection of a mutex and block until the condition is satisfied. If the
condition is false, a thread blocks on a condition variable and atomically
releases the mutex that is waiting for the condition to change. If another
thread changes the condition, it may wake up waiting threads by signaling the
associated condition variable. The waiting threads, upon awakening, reacquire
the mutex and re-evaluate the condition.
#include <iostream>
#include <ospace/thread.h>
enum status { open = 0, closed = 1 };
status gate = closed;
os_condition_mutex gate_mutex;
os_condition gate_cond( gate_mutex );
void*
horse( void* )
{
cout << "horse " << os_this_thread::tid() << " trots to gate" << endl;
os_this_thread::sleep( 1 );
cout << "horse " << os_this_thread::tid() << " waits at gate" << endl;
gate_mutex.lock();
// Loop and check the condition. There could be spurious wakeups.
while ( gate != open )
gate_cond.wait();
gate_mutex.unlock();
cout << "horse " << os_this_thread::tid() << " races" << endl;
return 0;
}
int
main()
{
os_thread_toolkit initialize;
// Spawn new threads. Each thread represents a horse.
os_thread t1( horse );
os_thread t2( horse );
os_thread t3( horse );
os_this_thread::sleep( 3 );
// Open the gate and signal all the horses.
gate_mutex.lock();
gate = open;
gate_cond.signal_all();
gate_mutex.unlock();
os_this_thread::wait_for_thread( t1 );
os_this_thread::wait_for_thread( t2 );
os_this_thread::wait_for_thread( t3 );
cout << "race is over" << endl;
return 0;
}
horse 4 trots to gate
horse 6 trots to gate
horse 7 trots to gate
horse 4 waits at gate
horse 6 waits at gate
horse 7 waits at gate
horse 4 races
horse 6 races
horse 7 races
race is over
An os_ward is a
synchronization mechanism that allows threads to wait on a group of handles or
one handle out of the group to become posted before proceeding. A thread can
wait on handles by calling any of the three wait methods. Wards are useful in
situations in which more than one event or condition must be satisfied before
proceeding.
Instances of the handle type, os_handle_t
, must be obtained using os_ward::handle_for()
static method passing a descriptor as the argument. The handle type works very
much the same as an os_event_semaphore . Handles
are tracked by the ward and are used to notify waiting threads of occurrences
of events or changes in conditions.
#include <iostream>
#include <ospace/thread.h>
void* wait_single( void* arg )
{
cout << "tid " << os_this_thread::tid() << ", waiting for handle 5" << endl;
os_ward::wait_for_single_object( ((os_handle_t*)arg)[ 4 ] );
cout << "tid " << os_this_thread::tid() << ", signaled by handle 5" << endl;
return 0; // Ignored in this example.
}
void* wait_any( void* arg )
{
cout << "tid " << os_this_thread::tid() << ", waiting for any handle " << endl;
int handle_id = 1 + os_ward::wait_for_multiple_objects( 5, (os_handle_t*)arg, false );
cout << "tid " << os_this_thread::tid() << ", signaled by handle " << handle_id << endl;
return 0; // Ignored in this example.
}
void* wait_all( void* arg )
{
cout << "tid " << os_this_thread::tid() << ", waiting for all handles" << endl;
os_ward::wait_for_multiple_objects( 5, (os_handle_t*)arg, true );
cout << "tid " << os_this_thread::tid() << ", signaled by all handles" << endl;
return 0; // Ignored in this example.
}
int
main()
{
os_thread_toolkit initialize;
os_desc_t desc[ 5 ] = { 1, 2, 3, 4, 5 };
os_handle_t handle[ 5 ] =
{
os_ward::handle_for( desc[ 0 ] ),
os_ward::handle_for( desc[ 1 ] ),
os_ward::handle_for( desc[ 2 ] ),
os_ward::handle_for( desc[ 3 ] ),
os_ward::handle_for( desc[ 4 ] )
};
cout << "running" << endl;
cout << "main thread has tid " << os_this_thread::tid() << endl;
// Create thread.
os_thread t1( &wait_single, (void*)handle );
cout << "thread t1 has tid " << t1.tid() << endl;
os_thread t2( &wait_any, (void*)handle );
cout << "thread t2 has tid " << t2.tid() << endl;
os_thread t3( &wait_all, (void*)handle );
cout << "thread t3 has tid " << t3.tid() << endl;
os_this_thread::sleep( 1 );
for ( int i = 0; i < 5; i++ )
{
cout << "main thread signaling with handle " << i + 1 << endl;
os_ward::handle_for( desc[ i ] ).post();
os_this_thread::sleep( 1 );
}
os_this_thread::wait_for_thread( t1 );
os_this_thread::wait_for_thread( t2 );
os_this_thread::wait_for_thread( t3 );
cout << "main thread terminates" << endl;
return 0;
}
running
main thread has tid 1
thread t1 has tid 2
thread t2 has tid 3
thread t3 has tid 4
tid 2, waiting for handle 5
tid 3, waiting for any handle
tid 4, waiting for all handles
main thread signaling with handle 1
tid 3, signaled by handle 1
main thread signaling with handle 2
main thread signaling with handle 3
main thread signaling with handle 4
main thread signaling with handle 5
tid 2, signaled by handle 5
tid 4, signaled by all handles
main thread terminates
Copyright©1994-2026 Recursion
Software LLC
All Rights Reserved - For use by licensed users only.