Bee Concurrence
Multi threading
Bee has capability to run in parallel multiple instances of a rule. For this we need to start a rule asynchronously and run other rules or the same rule in a parallel thread. This is called multi-threading application. For this we introduce new kwyeords:
| keyword | description |
|---|---|
| begin | call a rule asynchronously and create a new thread in a private Region Arena |
| wait | synchronization barrier — block the parent thread until all active begin workers complete |
| yield | suspend a coroutine task and return control (and optionally a value) to the consumer |
Example 1:
Asynchronous call can be done using a control cycle and keyword "begin". This is significant because a rule need parameters. By using keyword "begin" we initiate one rule "test" 4 times but this rule does absolutely nothing, just wait about 30 seconds.
-- a worker rule that returns its argument
rule test(n ∈ Z) => (r ∈ Z):
let r := n * 2;
return;
-- spawn 4 concurrent workers, then synchronize
rule main:
for ∀ i ∈ (1..4) do
begin test(i); -- each call runs on its own thread
done;
wait; -- synchronization barrier: block until all workers finish
return;
Example 2:
A big problem in asynchronous call is to capture the results of a rule that run in parallel mode. In the next example we use map-reduce design pattern to create sum of numbers in parallel. The results are captured into a list. Then we reduce the results by making sum of sums and create the output.
Map-Reduce Pattern
-- sum the integers in a range (runs on one worker thread)
rule sum(a, b ∈ Z) => (r ∈ Z):
cycle:
new i := 0
for ∀ i ∈ (a..b) do
let r += i;
done;
return;
-- prepare for execution on 4 threads
rule main:
new results <: List; -- partial sum collection
-- use a mapping technigue to split data in 4 equal batches
cycle:
new i ∈ N;
for ∀ i ∈ (1..<101)(25) do -- stepped range: 1, 26, 51, 76 (D13)
begin sum(i,i+24) +> results; -- dispatch disjoint batches into the reduction queue
done;
wait; -- wait for other threads to finish;
new output ∈ Z;
-- reduce results into output
for ∀ partial ∈ results do
let output += partial;
done;
print output; -- 5050
expect output = 5050;
return;
Note: There is a smarter way of doing this sum using a formula: (1+100)*50 = 5050. But of course we pretend we don't know this and do it in the hard way. The point is we have done it in parallel on a multithread application.
Bee Coroutines
Coroutines
Coroutines are rules that can be suspended with all states ready then resumed on demand multiple times. Each time we resume a coroutine this can produce one result that we capture. So a coroutine can produce multiple results until finishes.
Example:
Coroutines can be used as a branch of main thread. This is not yet running in parallel but is an example for how a rule can play as a coroutine.
- you need only one coroutine to create a side branch,
- call a branch using "begin" and suspended using "yield",
- resume a coroutine using yield plus routine name.
rule test(n ∈ N) => (result ∈ N):
-- can generate n numbers
cycle:
new i ∈ N;
for ∀ i ∈ (1..n) do
let result := i;
-- suspend execution
yield; -- wait for the main thread
done;
result = 0; -- finalization signal
return;
rule main:
-- start secondary process
begin test(9) -- initialize coroutine
cycle:
new r := 1 ∈ N; -- result reference (seeded to enter loop)
while r > 0 do
yield r << test; -- extract next yielded value from the 'test' coroutine
write (r, ",");
done;
print; -- 0,1,2,3,4,5,6,7,8,9,
return
Note:&In the example above, the program is still working in serial mode, except that has a branch running on a separated thread. When the coroutine function the main thread is on hold, and when the main thread function the coroutine is waiting.
Producer consumer
Producer consumer pattern is using coroutines that can work in parallel in asynchronous mode. That means we start coroutines and do not wait to finish but let the main thread to continue. A producer is preparing the work and the consumer is doing the work.
If you do not know how this works, take a look to the diagram below. We have one rule called "producer" one list that act as queue and a routine called "consumer". The queue has a limited capacity. We can use same consumer twice, if we do then the application is also multi-threading.
Producer Consumer
Diagram
Notes:
- for this you need two rules: one is producer and other is consumer,
- the main thread is starting threads using begin ,
- producer is a dispatcher that distribute the work,
- producer is usually working on a single thread;
- consumer is a worker that resolve a task given by the producer;
- consumer is usually working on multiple threads;
Example:
Worker Exception Isolation
An uncaught exception or panic inside a worker thread does not corrupt the parent or sibling Region Arenas (spec/12 §5). The failure is captured in the worker's thread-local $trial handle.
When the parent executes wait;, any captured worker exceptions are aggregated and re-raised at the barrier call site, so a surrounding trial block can handle them. This surfaces as E1204 WorkerPanicReRaised.
Diagnostic error codes
Diagnostic codes raised by the Concurrency module (spec/12 §7):
| Code | Violation | Mitigation / diagnostic action |
|---|---|---|
| E1201 | UnsynchronizedWorker: Scope exited with pending begin threads prior to a wait barrier. | Add a wait barrier before scope exit. (Canonical working code; twin of E0406 in structure.) |
| E1202 | DataRaceAttempt: Direct cross-thread mutation without a +> reduction channel. | Use the +> reduction channel. |
| E1203 | CoroutinesChannelClosed: yield var << task on a closed or finished coroutine. | Re-open the channel or check coroutine state. |
| E1204 | WorkerPanicReRaised: Unhandled exception in a worker thread surfaced at the wait barrier. | Inspect the $trial handle and handle the re-raise. |
| E1205 | YieldOutsideCoroutine: yield used inside a non-coroutine static rule. | Declare the rule as a coroutine or remove yield. |