Condition Variables
Condition variables allow one or more threads to wait until a specific condition is met. They are a concurrency primitive provided by the Jule standard library via sync::Cond
. A condition variable must be created using Cond.New
and associated with a Locker
during creation. This Locker
is often a primitive like,sync::Mutex
. Once a condition variable is used, it should not be copied again, as this could lead to inconsistencies and unexpected outcomes.
The Signal
method is used to send a signal to a single waiting thread, while the Broadcast
method is used to signal all waiting threads. To wait for a signal, the Wait
method is used.
Additionally, sys::Cond
provides the Lock
and Unlock
methods for the associated Locker
. For consistency, it is recommended to use these methods to manage the locking mechanism, even if the mechanism is stored in a separate variable. Furthermore, unless absolutely necessary—which is rare—it is advised not to store the locking mechanism associated with the condition variable separately.
For example:
use "std/sync"
use "std/time"
fn main() {
mut wg := sync::WaitGroup.New()
cond := sync::Cond.New(new(sync::Mutex))
wg.Add(2)
co fn() {
cond.Lock()
println("Thread 1 waits...")
cond.Wait()
println("Thread 1 notified!")
cond.Unlock()
wg.Done()
}()
co fn() {
time::Sleep(2 * time::Second)
cond.Lock()
println("Thread 2 notifies thread 1!")
cond.Signal()
cond.Unlock()
wg.Done()
}()
wg.Wait()
}
In the example code above, a WaitGroup
and a condition variable are used to wait and signal for threads. As recommended, the locking mechanism to be used with the condition variable (in this case, a sync::Mutex
) is directly associated and used through the condition variable. The thread 1 waits for a signal with the Wait
call, while thread 2 sends this signal with the Signal
call. As a result, thread 1 is awakened and continues execution.
When waiting for a condition, the Wait
method should called within a loop which checks the condition. This ensures that unexpected wakeups or notifications are handled correctly (see: Spurious Wakeups). A Wait
call, considers the associated Locker
is locked. Releases the locking mechanism and waits until it is notified by a signal. After being notified, it tries to reacquire the lock.
For example:
use "std/sync"
use "std/time"
static cond = sync::Cond.New(new(sync::Mutex))
static mut ready = false
fn waitForCondition(mut wg: &sync::WaitGroup) {
cond.Lock()
for !ready {
println("Waiting for the condition...")
cond.Wait()
}
println("Condition met!")
cond.Unlock()
wg.Done()
}
fn signalCondition() {
time::Sleep(time::Second * 3)
cond.Lock()
ready = true
cond.Signal()
cond.Unlock()
}
fn main() {
mut wg := sync::WaitGroup.New()
wg.Add(1)
co waitForCondition(wg)
signalCondition()
wg.Wait()
}
In the above code example, the waitForCondition
function works with a thread and waits for the ready
variable to become true
. As mentioned, the waiting is done within a loop. The signalCondition
function, on the other hand, waits for 3 seconds before setting the required condition and waking up the thread.