Threads and Thread-Specific Data |
This section covers os_thread
, os_this_thread , os_future_thread
,
os_future , os_trap and discusses thread-specific data.
A thread is a program that is
executing. Like processes, a thread can access most of its own attributes, but
only a small subset of the attributes for another thread. Therefore, thread
objects are modeled using two classes: os_this_thread
, which represents the current thread, and os_thread
, which represents other threads. The sections in this
chapter describe the behaviors of all thread-related classes.
Thread-specific data can be used to create a unique key and then make an association between each thread and this key.
The class os_this_thread
is comprised of static member functions that
perform operations on the current thread, including facilities for sleeping
and waiting for a thread. Instances of os_this_thread
cannot be created.
To
create a new thread, construct an os_thread with
the address of a function, and a pointer to an optional argument. This creates
a thread that executes the specified function with the supplied pointer as its
argument. The function must be able to take a void*
argument and return a void* .
Once constructed, an os_thread
object contains functions for suspending, resuming, terminating, and changing
the priority of the thread object. The new thread executes concurrently with
other threads in the process and terminates when it reaches the end of the
specified function.
The following example spawns two threads and controls them using a variety of intuitive functions.
#include <iostream>
#include <ospace/thread.h>
void*
f1( void* args )
{
for ( int i = 0; i < 7; i++ )
{
cout << "tid " << os_this_thread::tid() << ", loop " << i << endl;
os_this_thread::sleep( 1 ); // Sleep for 1 second.
}
return 0; // Ignored in this example.
}
void
main()
{
os_thread_toolkit intialize;
cout << "Main thread has tid " << os_this_thread::tid() << endl;
os_thread t1( f1 ); // Spawn new thread.
cout << "thread t1 has tid " << t1.tid() << endl;
os_thread t2( f1 ); // Spawn new thread.
cout << "thread t2 has tid " << t2.tid() << endl;
cout << "sleep for 3 seconds" << endl;
os_this_thread::sleep( 3 );
cout << "suspend thread t1" << endl;
t1.suspend();
os_this_thread::sleep( 3 );
cout << "resume thread t1" << endl;
t1.resume();
cout << "wait for threads" << endl;
os_this_thread::wait_for_thread( t1 );
os_this_thread::wait_for_thread( t2 );
cout << "main thread terminates" << endl;
}
Main thread has tid 1
tid 4, loop 0
thread t1 has tid 4
tid 6, loop 0
thread t2 has tid 6
sleep for 3 seconds
tid 6, loop 1
tid 4, loop 1
tid 6, loop 2
tid 4, loop 2
tid 6, loop 3
suspend thread t1
tid 6, loop 4
tid 6, loop 5
tid 6, loop 6
resume thread t1
tid 4, loop 3
wait for threads
tid 4, loop 4
tid 4, loop 5
tid 4, loop 6
main thread terminates
Thread-specific data can be used to create a unique key with an association between each thread and this key.
To create a unique key of type
os_thread_key_t , use allocate()
. Then, to associate the key with a void
pointer for a particular thread, use set()
. Once the association is made, you can use get()
to access the void
pointer associated with the key.
The following example uses
thread-specific data to associate a name with each thread. The main program
creates a unique key for name associations, which is then associated with a
particular thread by f1() . The f2()
function uses the key to retrieve the name of the
thread.
#include <iostream>
#include <string>
#include <ospace/thread.h>
os_thread_key_t string_key; // Handle for string description.
void
f2()
{
cout << "f2() is called by ";
// Retrieve association.
cout << * (string*) os_this_thread::get( string_key ) << endl;
}
void*
f1( void* args )
{
os_this_thread::set( string_key, args ); // Add association.
f2();
return 0;
}
void
main()
{
os_thread_toolkit initialize;
string_key = os_this_thread::allocate(); // Allocate key.
// Spawn two threads.
os_thread t1( f1, (void*) new string( "thread 1" ) );
os_thread t2( f1, (void*) new string( "thread 2" ) );
cout << "wait for threads" << endl;
os_this_thread::wait_for_thread( t1 );
os_this_thread::wait_for_thread( t2 );
}
f2() is called by thread 1
f2() is called by thread 2
wait for threads
The class os_future_thread
can be used to process information and store the value for later use when it
is done processing. This permits the application to be independent of when the
thread actually will finish processing and to retrieve the return value
anytime after it is available. This feature makes asynchronous programming
easier by encapsulating the responsibility to synchronize on the availability
and validity of the result.
To create a new future thread, construct an os_future_thread
with the address of a function, a pointer to an optional argument, and the
address of a callback function. This creates a thread that executes the
specified function with the supplied pointer as its argument and the specified
callback function with the pointer to the return value as its argument. The
functions must be able to take a void* argument
and return a void* .
Once constructed, an os_future_thread
object contains functions for starting, changing the user functions, and
retrieving the value. The new thread executes concurrently with other threads
in the process and can be restarted again.
The following example spawns a future thread and blocks at
the method invocation result() until the value is
available.
#include <ospace/thread.h>
#include <iostream>
void*
do_action( void* )
{
os_this_thread::sleep( 1 );
cout << "Exiting do_action" << endl;
return (void*)123;
}
int
main()
{
os_thread_toolkit init_thread;
os_future_thread async_thread( (os_future_thread_func_t) do_action, 0, 0 );
async_thread.start();
os_future result = async_thread.result();
long data = (long)result.redeem();
cout << "Result of async thread = " << data << endl;
return 0;
}
Exiting do_action
Result of async thread = 123
An os_future holds the future
result of a user supplied function. When the redeem()
member is called, two possible actions occur. If the user function has
finished and returned a value, the redeem function returns the value. If the
user function is still executing, the redeem function will block until the
return value can be obtained.
The following example spawns two future threads and four os_future
objects, a pair for each os_future_thread result.
#include <ospace/thread.h>
#include <iostream>
static int value = 2;
os_mutex value_mutex;
void*
do_action( void* )
{
int j;
value_mutex.lock();
j = value;
value++;
value_mutex.unlock();
os_this_thread::sleep( j );
cout << "Exiting do_action" << endl;
return (void*)j;
}
int
main()
{
os_thread_toolkit init_thread;
os_future_thread async_thread1( (os_future_thread_func_t)do_action, 0 );
os_future_thread async_thread2( (os_future_thread_func_t)do_action, 0 );
async_thread1.start();
async_thread2.start();
os_future result1 = async_thread1.result();
os_future result2 = async_thread1.result();
os_future result3 = async_thread2.result();
os_future result4 = async_thread2.result();
long data = (long)result1.redeem();
cout << "Thread 1, Future 1 value = " << data << endl;
data = (long)result2.redeem();
cout << "Thread 1, Future 2 value = " << data << endl;
data = (long)result3.redeem();
cout << "Thread 2, Future 3 value = " << data << endl;
data = (long)result4.redeem();
cout << "Thread 2, Future 4 value = " << data << endl;
return 0;
}
Exiting do_action
Thread 1, Future 1 value = 2
Thread 1, Future 2 value = 2
Exiting do_action
Thread 2, Future 3 value = 3
Thread 2, Future 4 value = 3
The class os_trap can contain
multiple os_future objects and returns the next
future in the order of the time they were received. This reduces the wait time
between future thread processing.
To add futures to the os_trap
object, use the method add_to_trap() passing the os_future
object as the argument, and conversely to remove futures, use remove_from_trap()
passing the os_future object as the argument.
Retrieve the next available future by using get_next().
The following example uses traps to get the next readily-available result.
#include <ospace/thread.h>
#include <iostream>
static int value = 2;
os_mutex value_mutex;
void*
do_action( void* )
{
int j;
value_mutex.lock();
j = value;
value++;
value_mutex.unlock();
os_this_thread::sleep( j );
cout << "Exiting do_action" << endl;
return (void*)j;
}
int
main()
{
os_thread_toolkit init_thread;
os_future_thread async_thread1( (os_future_thread_func_t) do_action, 0 );
os_future_thread async_thread2( (os_future_thread_func_t) do_action, 0 );
async_thread1.start();
async_thread2.start();
os_future result1 = async_thread1.result();
os_future result2 = async_thread1.result();
os_future result3 = async_thread2.result();
os_future result4 = async_thread2.result();
os_trap trap;
trap.add_to_trap( result1 );
trap.add_to_trap( result2 );
trap.add_to_trap( result3 );
trap.add_to_trap( result4 );
for ( int i = 0; i < 4; i ++ )
{
os_future future_from_trap;
future_from_trap = trap.get_next();
cout << "From trap, data = " << (long)future_from_trap.redeem() << endl;
}
return 0;
}
Exiting do_action
From trap, data = 2
From trap, data = 2
Exiting do_action
From trap, data = 3
From trap, data = 3
A thread pool is a collection of reusable threads that are used repeatedly to run small tasks. The benefits of reusable threads are the gains from using system resources for things other than thread creation, which has a noticeable impact on performance when the number of tasks and the frequency of task execution is significant.
An os_thread_pool is
configurable by allowing adjustments of the maximum pool size, the maximum
number of idle threads, and the keep alive time through its constructor at
creation time or its methods. Information about the thread pool's state can be
obtained through various methods. Tasks can be added to the thread pool for
execution using the execute() method passing a
pointer to the task as an argument. Each task is an object that implements a
runnable interface, i.e. the void* run() method
is called when it is executed.
Use of member template feature in the execute methods gives the flexibility of not restricting the thread pool instance to a specific task class.
In the following example, the print_jobs()
function in the os_printer class uses a thread
pool object to execute the print jobs. Initially, a thread pool is created
with a maximum of 5 total threads and a maximim of 3 idle threads. The thread
pool object is opened and the os_print_jobs
runnable objects are executed for execution by idle threads. Later, more print
jobs are executed and the thread pool is resized. The thread pool size, idle
threads, and the pending tasks are repeated displayed to show how threads are
created and destroyed when calls to resize are made.
#include <iostream>
#include <vector>
#include <ospace/thread.h>
class os_print_job
{
public:
os_print_job( int id = 0 ) : id_( id ) {}
void* run()
{
cout << "\tPrinter executing job # " << id_ << endl;
os_this_thread::sleep( id_ % 3 );
return 0;
}
private:
int id_;
};
class os_printer
{
public:
os_printer( int num_jobs );
void print_jobs();
private:
static void display_stats( const os_thread_pool& tpool );
os_vector< os_print_job > jobs_;
};
os_printer::os_printer( int num_jobs )
{
for ( int i = 0; i < num_jobs; ++i )
jobs_.push_back( os_print_job( i ) );
}
/* static */ void
os_printer::display_stats( const os_thread_pool& tpool )
{
cout << "\n========Thread pool statistics========";
cout << "\n Max pool size = " << tpool.max_pool_size();
cout << "\n Current pool size = " << tpool.pool_size();
cout << "\n Max idle threads = " << tpool.max_idle_threads();
cout << "\n Idle threads = " << tpool.idle_threads();
cout << "\n Pending tasks = " << tpool.pending_tasks();
cout << "\n======================================" << endl;
}
void
os_printer::print_jobs()
{
os_thread_pool tpool( 5, 3 );
cout << "\nCreating a thread pool of (max_pool = 5, max_idle = 3)" << endl;
display_stats( tpool );
cout << "\nScheduling tasks for thread pool execution" << endl;
for ( int i= 0; i < jobs_.size(); ++i )
tpool.execute( &jobs_[ i ] );
os_this_thread::sleep( 2 );
display_stats( tpool );
cout << "\nResizing the thread pool to (max_pool =7, max_idle = 5)" << endl;
tpool.max_idle_threads( 5 );
tpool.max_pool_size( 7 );
os_this_thread::sleep( 1 );
display_stats( tpool );
cout << "\nScheduling more tasks for thread pool execution" << endl;
for ( int j= 0; j < jobs_.size(); ++j )
tpool.execute( &jobs_[ j ] );
cout << "\nResizing the thread pool to (max_pool =10, max_idle = 2)" << endl;
tpool.max_idle_threads( 2 );
tpool.max_pool_size( 10 );
os_this_thread::sleep( 1 );
display_stats( tpool );
os_this_thread::sleep( 2 );
display_stats( tpool );
cout << "\nResizing the thread pool to (max_pool =5, max_idle = 1)" << endl;
tpool.max_idle_threads( 1 );
tpool.max_pool_size( 5 );
os_this_thread::sleep( 2 );
display_stats( tpool );
cout << "\nWaiting for task execution completion and pool exit" << endl;
tpool.wait_for_completion();
display_stats( tpool );
}
int
main()
{
os_thread_toolkit initialize;
os_printer printer( 25 ); // Create 25 jobs.
printer.print_jobs();
return 0;
}
Creating a thread pool of (max_pool = 5, max_idle = 3)
========Thread pool statistics========
Max pool size = 5
Current pool size = 0
Max idle threads = 3
Idle threads = 0
Pending tasks = 0
======================================
Scheduling tasks for thread pool execution
Printer executing job # 0
Printer executing job # 1
Printer executing job # 2
Printer executing job # 3
Printer executing job # 4
Printer executing job # 5
Printer executing job # 6
Printer executing job # 7
Printer executing job # 8
Printer executing job # 9
Printer executing job # 10
Printer executing job # 11
========Thread pool statistics========
Max pool size = 5
Current pool size = 5
Max idle threads = 3
Idle threads = 0
Pending tasks = 13
======================================
Resizing the thread pool to (max_pool =7, max_idle = 5)
Printer executing job # 12
Printer executing job # 13
Printer executing job # 14
Printer executing job # 15
Printer executing job # 16
Printer executing job # 17
Printer executing job # 18
Printer executing job # 19
Printer executing job # 20
Printer executing job # 21
Printer executing job # 22
========Thread pool statistics========
Max pool size = 7
Current pool size = 7
Max idle threads = 5
Idle threads = 0
Pending tasks = 2
======================================
Scheduling more tasks for thread pool execution
Resizing the thread pool to (max_pool =10, max_idle = 2)
Printer executing job # 23
Printer executing job # 24
Printer executing job # 0
Printer executing job # 1
Printer executing job # 2
Printer executing job # 3
Printer executing job # 4
Printer executing job # 5
Printer executing job # 6
Printer executing job # 7
Printer executing job # 8
Printer executing job # 9
Printer executing job # 10
Printer executing job # 11
========Thread pool statistics========
Max pool size = 10
Current pool size = 10
Max idle threads = 2
Idle threads = 0
Pending tasks = 13
======================================
Printer executing job # 12
Printer executing job # 13
Printer executing job # 14
Printer executing job # 15
Printer executing job # 16
Printer executing job # 17
Printer executing job # 18
Printer executing job # 19
Printer executing job # 20
Printer executing job # 21
Printer executing job # 22
Printer executing job # 23
Printer executing job # 24
========Thread pool statistics========
Max pool size = 10
Current pool size = 7
Max idle threads = 2
Idle threads = 2
Pending tasks = 0
======================================
Resizing the thread pool to (max_pool =5, max_idle = 1)
========Thread pool statistics========
Max pool size = 5
Current pool size = 1
Max idle threads = 1
Idle threads = 1
Pending tasks = 0
======================================
Waiting for task execution completion
========Thread pool statistics========
Max pool size = 5
Current pool size = 0
Max idle threads = 1
Idle threads = 0
Pending tasks = 0
======================================
Copyright©1994-2026 Recursion
Software LLC
All Rights Reserved - For use by licensed users only.