Posts Tagged ‘threads’
The case for synchronous concurrency
The point of this post is to show that it is wrong to rely on timing issues in preemptive multithreading for activites that require a synchronized behavior. I compare the same program specification in three different implementations.
The example program is supposed to blink two leds with different frequencies (400ms and 1000ms) on an Arduino board. They must blink together every 2 seconds.
The first two implementations use different RTOSes with preemptive multithreading. The third implementation uses the synchronous language Céu.
(UPDATE) The complete source files can be found here.
The first implementation uses the ChibiOS RTOS. I omitted all code related to creating and starting the threads (which run with the same priority).
Follows the code and video for the implementation in ChibiOS:
static msg_t Thread1(void *arg) {
while (TRUE) {
digitalWrite(11, HIGH);
chThdSleepMilliseconds(400);
digitalWrite(11, LOW);
chThdSleepMilliseconds(400);
}
}
static msg_t Thread2(void *arg) {
while (TRUE) {
digitalWrite(12, HIGH);
chThdSleepMilliseconds(1000);
digitalWrite(12, LOW);
chThdSleepMilliseconds(1000);
}
}
You can see that around 1:00 the leds loose synchronism among them, and also with the metronome.
The second implementation uses the DuinOS RTOS. I also omitted all code related to creating and starting the threads (which run with the same priority).
In this example the leds were well synchronized, so I included another task that uses the serial port with a different frequency.
Follows the code and video for the implementation in DuinOS:
taskLoop (T1) {
digitalWrite(11, HIGH);
delay(400);
digitalWrite(11, LOW);
delay(400);
}
taskLoop (T2) {
digitalWrite(12, HIGH);
delay(1000);
digitalWrite(12, LOW);
delay(1000);
}
int c = 0;
taskLoop (T3) {
delay(77);
Serial.print(c++);
}
Since the beginning you can see that the leds loose synchronism among them, and also with the metronome.
The third implementation uses the synchronous language Céu.
In this example the leds were well synchronized, even with the third activity that uses the serial port.
Follows the code and video for the implementation in Céu:
par do
loop do
_digitalWrite(11, _HIGH);
await 400ms;
_digitalWrite(11, _LOW);
await 400ms;
end
with
loop do
_digitalWrite(12, _HIGH);
await 1s;
_digitalWrite(12, _LOW);
await 1s;
end
with
int c = 0;
loop do
await 77ms;
c = c + 1;
_Serial.print(c);
end
end
Conclusion:
The execution model of preemptive multithreading does not ensure implicit synchronization among threads.
There’s nothing wrong with the RTOSes and the example implementations: the behavior shown in the videos is perfectly valid.
The problem is that usually the very first examples for these systems use blinking leds (supposely synchronized) to show how easy is to write multithreaded code. This is not true!
Preemptive multithreading should not be used as is to write this kind of highly synchronized applications. Adding semaphores or other synchronization primitives to these codes won’t help alone, they require a single thread to handle timers that is responsible to dispatching others.
I used timers in the example, but any kind of high frequency input would also behave nondeterministically in multithreaded systems.
In synchronous languages like Céu, the execution model enforces that all activities are synchronized all the time.
About Determinism
Current approaches for concurrent systems, such as multi-threading and message-passing are inherently non-deterministic, leading to unpredicted execution.
In multi-threaded systems, wherein memory is shared among threads, even if critical sections of code are protected, one is still subject to bugs due to non-determinism.
Suppose one writes the following code:
thread1 {
... // do some processing
lock {
a = a + 2
}
}
thread2 {
... // do some processing
lock {
a = a * 2
}
}
a = 1
start(thread1)
start(thread2)
wait(thread1, thread2)
print(a)
Depending on which thread assigns to `a` first, the value printed might be 6 or 4.
Moreover, each time the program is executed, the other result may be printed, as thread scheduling isn’t deterministic.
By using message-passing concurrency, non-determinism is also an issue.
In the code below, the value 6 or 4 might also be printed.
myChannel = new Channel()
cspA {
... // do some processing
send(myChannel, 4)
}
cspB {
... // do some processing
send(myChannel, 6)
}
cspC {
... // do long processing
a = receive(myChannel)
a = receive(myChannel)
print(a)
}
The characteristic that makes such systems non-deterministic is that each command in the language takes an unbounded time to execute.
As each thread or process run in asynchrony with each other, we (or the compiler) can’t predict where each thread will be at anytime, being impossible to detect simultaneous accesses to system resources.
Synchronous Concurrency, in the other hand, is deterministic.
Each command is conceptually instantaneous or takes exactly the time it says so.
For instance, in LuaGravity all commands but AWAIT are instantaneous:
_a = _a + 2 -- instantaneous SPAWN(reactor) -- instantaneous AWAIT(reactor) -- waits for `reactor` to finish AWAIT(2) -- waits 2 seconds
In the code below, we can predict simultaneous access to _a that would lead to non-deterministic behavior, and raise an error.
SPAWN(function() _a = _a + 2 -- line 2: simultaneous access to `_a` with line 8 end) SPAWN(function() AWAIT(keyboard.press) _a = 10 -- deterministic access to `_a` end) _a = _a * 2 -- line 8: simultaneous access to `_a` with line 2
The execution of this program yields an error when the second simultaneous access to _a happens.
The prediction of simultaneous access could be even static (if LuGravity had a compiling phase), raising a compile-time error.