-
-
Notifications
You must be signed in to change notification settings - Fork 44
Using RTL condition variables in the RTL
Felix has C++ type
namespace flx { namespace pthread {
class PTHREAD_EXTERN flx_condv_t : public world_stop_notifier_t
{
::std::mutex m;
::std::condition_variable_any cv;
void notify_world_stop() override;
thread_control_base_t *tc;
public:
flx_condv_t (thread_control_base_t *);
void lock();
void unlock();
void wait();
void timed_wait(double seconds);
void signal();
void broadcast();
~flx_condv_t();
};
which is an implementation of a condition variable. To use it first you construct a value
of the type passing it a pointer to the thread_control_base
. The reason for this will be
explained later.
The condition variable is used to test some volatile condition. To do so, you first
have to call the lock()
method. Next, you can check the condition.
Now you have two choices. If the condition is satisfied, you must release the lock and proceed with the work that was waiting on the condition.
If the condition is not satisfied, you must call the wait()
method instead.
The lock must be set when you call the wait()
or timed_wait()
method.
The wait()
method delays until either a timeout of 1 second expires or a signal
has been sent to the condition variable. The timed_wait()
method is similar
except you can set the delay (using a double float value in seconds).
The delay ensures will eventually be checked again,
even if a signal is not received.
On entry to the wait()
or timed_wait()
methods, the lock is released.
When the wait is complete, the lock is re-acquired.
On exit from the wait()
or timed_wait()
method the lock is held.
The usual action at this point is to jump back to the code which
tests the condition again. This is often done in a loop.
Typical code is:
flx_condv cv;
cv.lock();
while (!condition_satisfied()) cv.wait();
// condition is now satisfied
cv.unlock();
When some code changes variables which may cause the condition to become
satisfied, that code should call either the signal()
or broadcast()
method.
Note that flx_condv_t meets the conditions of C++ Basic Lockable and so
you can use a lock_guard
with it.
The signal()
method releases one pthread waiting on the condition
to be released from the wait()
or timed_wait()
. The broadcast()
method release all of the waiting pthreads.
The rules for signalling are as follows:
-
It is safe but inefficient to never issue a signal. This is because Felix condition variable has a timeout.
-
It is safe, but possibly inefficient, to always use
broadcast()
. This may wake up too many threads, but the threads will check the condition in a serialised manner because the condition check is done whilst the lock is held. However the user must modify the state before releasing the lock if other threads should go back to sleep. -
It is not always correct to call
signal()
. Sometimes it is necessary to wake up more than one thread. -
The operating system can spuriously wake up a thread. Therefore the condition must always be checked.
Now we must explain the reason a thread_control_base_t pointer must be
passed. Notice that the condition variable class is derived
from world_stop_notifier
. When the condition variable is
constructed, it registers itself with the thread_control object.
The registration is removed when the condition variable is
destroyed.
Felix has a world-stop garbage collector. In order for the collector to work, the state of the memory to be scanned must be stable. To ensure this, all threads must stop working whilst the garbage collector is running.
Threads can yield control to the garbage collector by calling
the yield()
method of the thread_control object. The yield
method checks if there is a request to perform a garbage collection.
If so, the thread is suspended until the collection is completed.
Otherwise if there is no collection requested, the yield()
method returns.
The yield()
method is called automatically inside the condition variable
wait()
and timed_wait()
methods. There is no need to call it yourself,
although you may.
Now, the reason for the registration is that when a collection is requested, the requesting thread broadcasts a signal to any threads waiting on a registered condition variable. This makes the thread wake up and perform a yield, instead of waiting for a timeout to expire. Without the signal or the timeout, the thread might remain locked up in the condition variable, because other threads that might signal it have already yielded to the collector. In this case we would have a deadlock, because the collector can't run, but there are no threads running to wake up the last one.
For this reason, Felix cannot usually use system condition variables. Note that in addition to suspending on a world-stop request by the collector, thread must also post their current stack pointer to the thread control object. This is because the collector must conservatively scan the machine stacks of all threads for managed pointers, which are treated as GC roots.