There are a few ways we can accomplish inter-thread communication in Julia. In this article, we will look into two ways - via channel and via wait & notify.
Via Channel...
Let's try to dissect the above code.
The Key Idea: Channels are Iterable Streams
In Julia, a Channel is not just a queue—it implements the iteration protocol.
So when we write:
for val in ch
println("Consuming $val on thread $(threadid())")
end
this is conceptually equivalent to:
while true
val = take!(ch) # blocks if empty
println("Consuming $val on thread $(threadid())")
end
…but with one crucial addition:
The loop automatically stops when the channel is closed.
What Actually Happens Internally
When Julia executes:
for val in ch
it translates roughly into:
state = iterate(ch)
while state !== nothing
(val, next_state) = state
println(...)
state = iterate(ch, next_state)
end
For Channel, iterate(ch) is defined such that:
It internally calls
take!(ch)If data is available → returns
(value, state)If the channel is:
empty but open → it blocks (yields the task)
closed and empty → returns
nothing→ loop ends
Why This is Perfect for Concurrency
Let’s break down the behavior in your example:
Producer Thread
put!(ch, i)
Pushes data into the channel
If the channel buffer is full → producer blocks
Consumer Thread
for val in ch
If data is available → consumes immediately
If empty → consumer blocks (non-busy wait!)
If channel is closed → loop exits cleanly
Important: This is NOT Busy Waiting
A common mistake in other languages:
while(queue.empty()) { /* spin */ }
But Julia does this instead:
The consumer yields control
The scheduler runs another task
When
put!happens → consumer is resumed
This is cooperative scheduling, not CPU spinning.
Why for val in ch is Better Than take! Loop
We could write:
while true
val = take!(ch)
println(val)
end
But then we must manually handle termination:
How do we know when to stop?
We'd need a sentinel value or extra signaling
With:
for val in ch
we get:
Automatic blocking
Automatic wake-up
Automatic termination on
close(ch)Cleaner, declarative code
The Role of close(ch)
This line in our producer is critical:
close(ch)
Without it:
The consumer will wait forever
Because it assumes more data might come
With it:
The iteration ends naturally
forloop exits → task completes
Subtle but Important Detail
Even though we used:
Threads.@spawn
The channel itself is thread-safe, meaning:
Multiple producers/consumers can safely operate
Synchronization is handled internally
Final Insight
Consumer code:
for val in ch
is not just syntactic sugar—it encodes three things at once:
Blocking synchronization (wait for data)
Data flow semantics (consume stream)
Termination protocol (stop on close)
That’s why it’s considered idiomatic Julia concurrency.
Via Event/Condition
Core Idea
A Condition in Julia is a wait queue tied to a lock.
Threads can:
- wait(cond) → sleep until signaled
- notify(cond) → wake waiting thread(s)
What happens in the code?
-
Prints numbers
1 → 5 -
Acquires lock and calls:
wait(cond) -
Internally:
-
Releases
mtx - Goes to sleep
-
Gets queued on
cond
-
Releases
The thread is now blocked without consuming CPU
Main Thread (Producer / Notifier)
sleep(10)
lock(mtx) do
notify(cond)
end
What happens:
-
After 10 seconds, main thread:
-
Acquires the same lock (
mtx) -
Calls
notify(cond)
-
Acquires the same lock (
-
This:
- Wakes the waiting thread
- That thread re-acquires the lock
- Continues execution
Execution Timeline
Worker Thread Main Thread
-------------- -------------
1 → 5 printed
Now Waiting...
(wait → sleep)
sleep(10)
notify(cond)
Resuming!
6 → 10 printed
Critical Rules
1. Lock must be held
Both must be inside:
lock(mtx) do ... end
Otherwise:
ConcurrencyViolationError("lock must be held")
Same lock everywhere
cond = GenericCondition(mtx)
👉 You must use this exact mtx for:
-
wait -
notify
wait is atomic
When calling:
wait(cond)
Julia:
- Releases lock
- Sleeps
- On notify → wakes up
- Re-acquires lock
This avoids race conditions
Conceptual Model
Think of it like:
Condition = Lock + Queue of waiting threads
-
wait→ join queue -
notify→ wake one (or all)
When to use this?
Use Condition variables when:
- You need pure signaling
- No data needs to be transferred
Use Channel when:
- You need data + synchronization
No comments:
Post a Comment