Condition Variables
Condition variables allow one or more coroutines 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 coroutine, while the Broadcast method is used to signal all waiting coroutines. 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"
async fn main() {
mut wg := sync::WaitGroup.New()
cond := sync::Cond.New(new(sync::Mutex))
wg.Add(2)
co async fn() {
cond.Lock().await
println("Coroutine 1 waits...")
cond.Wait().await
println("Coroutine 1 notified!")
cond.Unlock()
wg.Done()
}()
co async fn() {
time::Sleep(2 * time::Second).await
cond.Lock().await
println("Coroutine 2 notifies coroutine 1!")
cond.Signal()
cond.Unlock()
wg.Done()
}()
wg.Wait().await
}In the example code above, a WaitGroup and a condition variable are used to wait and signal for coroutines. 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 coroutine 1 waits for a signal with the Wait call, while coroutine 2 sends this signal with the Signal call. As a result, coroutine 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"
let cond = sync::Cond.New(new(sync::Mutex))
let mut ready = false
async fn waitForCondition(mut wg: &sync::WaitGroup) {
cond.Lock().await
for !ready {
println("Waiting for the condition...")
cond.Wait().await
}
println("Condition met!")
cond.Unlock()
wg.Done()
}
async fn signalCondition() {
time::Sleep(time::Second * 3).await
cond.Lock().await
ready = true
cond.Signal()
cond.Unlock()
}
async fn main() {
mut wg := sync::WaitGroup.New()
wg.Add(1)
co waitForCondition(wg)
signalCondition().await
wg.Wait().await
}In the above code example, the waitForCondition function works with a coroutine 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 coroutine.