Ada's concurrency facility (tasks) reminds me of Erlang's concurrency facility. I am not fluent in any of these two languages but I like them — Ada is a fresh new entry in the list: seen a bit of it ages ago and I thought to dislike it. Now I'm looking into it again and a litte bit deeper, and I started to like it…
Indeed in particular I would be interested in SPARK (and/or only in Ada 2012, having pre/post conditions and invariants natively), because I am recently fascinated by the idea of the correctness of a program (maybe at the beginning something in the Haskell's world contaminated me with seeds which brought me to this) and design by contracts.
About Ada's tasks, experimenting I've just discovered that when an exception is raised in the “master” task1, all the other running tasks are kept going. (It must be a well known fact, but like I've said, Ada is mostly new for me.)
This is odd if you think about exceptions in the C++ (or other languages) way. In a language like C++ you create threads from a process and these threads die if the “master” process aborts because of an uncaught exception.
What happened
I was writing silly programs. In one of these extendend hello world programs I have a simple statement which writes to standard output the first argument given to the program from the command line:
Put_Line (Ada.Command_Line.Argument (1));
When I invoke this program without arguments an exception is raised and I see it as output (because there isn't an exception
block):
raised CONSTRAINT_ERROR : a-comlin.adb:65 explicit raise
(a-comlin.adb
should be the body of the Ada.Command_Line
package).
It sounds ok because I can't index the first argument if there isn't any argument. This is just bad programming…
Then I've decided to use the same “hello world” program to experiment with tasks. Something like this (with
s and use
s must be deduced if you want a compilable program):
procedure Hello2 is
task Waitress is
entry Go;
end Waitress;
task body Waitress is
begin
accept Go;
Put_Line ("Exiting task");
end Waitress;
begin
Put_Line (Ada.Command_Line.Argument (1));
Put_Line ("Everything's ok");
end Hello2;
Now if you run the program without arguments… You don't see Everything's ok in the console because of the exception raised by Ada.Command_Line.Argument (1)
, but the program doesn't exit: it seems to hung.
This is because the Waitress
task is waiting on accept Go
. Hence, uncaught exceptions affect the task where they are raised, but not all the others. Apparently.
Task termination
Unless… Unless the task is waiting also on terminate
.
task body Waitress is
begin
select
accept Go;
or
terminate;
end select;
Put_Line ("Exiting task");
end Waitress;
Now the uncaught exception which makes the Hello2
procedure to terminate, makes the task to terminate
too.
Let's add a delay to the procedure, so that it doesn't reach its end too soon:
procedure Hello2 is
-- ...
begin
Put_Line (Ada.Command_Line.Argument (1));
Put_Line ("ok");
delay 5.0;
end Hello2;
Now run the program with an argument, so to avoid the exception. After five seconds the program exits and the output is:
whatever you gave as first argument
Everything's ok
No Exiting task
. Let's change the code like this:
procedure Hello2 is
-- ...
begin
Put_Line ("Before putting the first arg");
Put_Line (Ada.Command_Line.Argument (1));
Put_Line ("Everything's ok");
delay 2.5;
Waitress.Go;
delay 2.5;
end Hello2;
Run the program with an argument (to avoid the exception). First we see two lines of output:
Before putting the first arg
whatever you gave as first argument
Everything's ok
After 2.5 seconds we see
Exiting task
and 2.5 seconds after this, the program exits.
If you run the program without arguments you see
Before exception
raised CONSTRAINT_ERROR : a-comlin.adb:65 explicit raise
as expected (at this point).
Except if I like the exception
Let's pretend to handle the exception but our task is without or terminate
again:
procedure Hello2 is
-- ...
task body Waitress is
begin
accept Go;
Put_Line ("Exiting task");
end Waitress;
begin
Put_Line ("Before exception");
Put_Line (Ada.Command_Line.Argument (1));
Put_Line ("Everything's ok");
Waitress.Go;
delay 2.5;
exception
when Constraint_Error =>
Put_Line ("What shall I do?");
end Hello2;
The output of a run without arguments will be:
Before exception
What shall I do?
And then the program hungs, waiting for the task to terminate (but it won't… so it will be a long waiting).
We need to explicitly stop every task without an or terminate
.
-- ...
exception
when Constraint_Error =>
Put_Line ("What shall I do?");
abort Waitress;
-- ...
I don't think this is considered a good practice for all the situations. What if the task is in the middle of something? The or terminate
alternative should be better (maybe the abort
doesn't force a really abrupt termination).
Kill me and everyone will follow
A rude alternative using Ada.Task_Identification
2 could be this:
-- ...
exception
when Constraint_Error =>
Put_Line ("What shall I do?");
Abort_Task (Current_Task);
end Hello2;
In this case the Current_Task
is the environment task. The program exits and the output is something like this3:
Before exception
What shall I do?
Execution terminated by abort of environment task
This is ugly.
I need to do something before I go
Let us suppose a task need to do something before it terminates. It seems like it can't be done with an or terminate
, or at least I wasn't able to find a way.
Then we need to handle it with a specific entry.
-- ...
task Waitress is
entry Go;
entry Stop;
end Waitress;
task body Waitress is
begin
select
accept Go;
or
accept Stop; -- do something special
end select;
Put_Line ("Exiting task");
end Waitress;
-- ...
exception
when Constraint_Error =>
Put_Line ("What shall I do?");
Waitress.Stop;
In this example Stop
behaves exactly like Go
, but you must imagine it does something differently.4
I don't like this approach: what if I have several tasks? I need to call Stop
on every task in every exception block which can terminate the whole program. It doesn't seem right.
RTFM
I am sure this is all trivial to anyone who has read the relevant Ada specifications or a tutorial like those you can find online. Likely there you can find details on task life cycle, termination and all that matters.
The AdaCore has interesting free resources, this wikibook covers several topics, though it seems there isn't too much of Ada 2012, there is also Ada Information Clearinghouse, and in general it seems to me somebody wants to push Ada more than it was in the past, but it could be a biased impression due to my current interests.
The ISO standard isn't yet an option (198 CHF!).
Contrast C++11
C++11 introduced threading facility in the standard; it is mostly pthreads-like with some goodies, plus futures, as far as I know. There isn't anything like what you have in Ada or Erlang5.
From the Ada example above you can see that Ada tasks don't need to be started: they start at the begin
of the procedure or function.
In C++11 you create a thread explicitly (and it starts just after that) and it isn't that easy to wait on a “message” from another thread. To imitate Ada you need to write more code, using mutexes and conditions.
In this post I'm not going to try to do anything of this. The only thing I need is a thread waiting for something long enough, and an exception in the parent thread.
This simple:
#include <iostream>
#include <thread>
#include <chrono>
int waitress()
{
std::this_thread::sleep_for(std::chrono::seconds(20));
std::cout << "20 seconds passed" << std::endl;
return 0;
}
int main()
{
std::thread t1(waitress); // thread starts here
return 0;
}
The output is something like this:
terminate called without an active exception
Aborted
The problem is that this isn't Ada: when the main
function executes the return
statement, the process is really on its way to its termination. The thread is still alive (waiting…) but gets killed.
We must explicitly wait for it to finish.
int main()
{
std::thread t1(waitress);
t1.join();
return 0;
}
This is the first big difference with Ada: when the process ends, it doesn't wait on its active threads. In order to wait that t1
, a thread of execution, joins the parent “trunk”, we must call join()
method.
With t1.join()
we have the process waiting for waitress()
to end; waitress()
isn't accepting messages but we can pretend it is exactly what it is doing.
It is easy to imagine that if an exception is thrown and uncaught, the threads die (t1.join()
isn't reached). In fact it is what happens with this code when you run the compiled executable without arguments:
int main(int argc, char **argv)
{
std::thread t1(waitress);
if (argc < 2) {
throw std::out_of_range("I was about to access argv[1]");
}
std::cout << argv[1] << std::endl;
t1.join();
return 0;
}
In Ada bounds check is done by default; in C++ it would be ok to access argv[1]
as long as the address actually accessed can be accessed by the program… This means that such a horrorful error doesn't throw a C++ exception you can catch. This is why I throw the exception explicitly by myself. Another way would be to “wrap” argv
in an object (an instance of std::vector
, for instance) and use it so that bounds check is performed6.
If we catch the exception, we can handle the thread as we need to. If we keep the t1.join()
outside the try block, our program waits the end of the thread, as Ada would do.
std::thread t1(waitress);
try {
if (argc < 2) {
throw std::out_of_range("I was about to access argv[1]");
}
std::cout << argv[1] << std::endl;
} catch (...) {
std::cerr << "hell no" << std::endl;
}
t1.join();
return 0;
We read hell no
on the console, then the program hungs (for only 20 seconds, but only because this 20 seconds sleep replaced the “wait forever something that won't happen” of the Ada code for convenience).
No reason to leave the join()
there if what we want is to terminate the program if an exception occurred. Maybe it can be moved inside the try block7, or we can leave it there and abort the program from inside the catch block.
But… this termination is abrupt and making a threaded program whose threads terminate gracefully is harder than simply adding a Stop
entry or alike.
What you read on the console,
terminate called without an active exception
Aborted
is a clue that there's something done wrong in our code.
Instead of using a function (as waitress
) we can use a class and this would make things easier, but not as easy as in Ada, Erlang, Go…8
Actually it's called environment task in the the Ada parlance.↩
What we want is to abort the current task, something like
abort This_Task
, butThis_Task
must be a task as intask Waitress is …
. For the current task there isn't a “name” likeWaitress
; this is why we must useAda.Task_Identification
which hasCurrent_Task
(it's aTask_Id
, good forAbort_Task
but not forabort
)↩GNAT 4.9.2.↩
In a slightly more realistic task the
select
maybe is inside an infinite loop andGo
andStop
would do something (accept Stop do … end Stop;
).↩Modern languages, or simply different languages, offer cleaner and/or safer approaches to solve many of the typical concurrency problems. Ada, Erlang… But also Go, Perl6, … maybe also “modern” Java, and surely more, have powerful, though easy, mechanisms. But a language like C++11 doesn't offer certain things ready to use. Go-like channels, select/accept (or receive as in Erlang) semantic can be implemented, of course, but it isn't a trivial effort.↩
The class
std::vector
performs bounds check if you access elements usingat()
method, not[]
operator.[]
is for tough programmers who know what they're doing, and C++ programmers must be tough and must know it better… (I daresay that bounds check gives rarely performace issues we should be worried about; said differently, many common applications doesn't need to squeeze every bit of power from the machine, thus optimizing these checks have no value.)↩Of course also in Ada we can “scope” the exception starting another block and associating the
exception
part to that block instead of the one of the procedure. So we can resume from certain kind of errors, if we need to.↩This article uses future and promise as a signaling mechanism. It seems more complex than needed, though interesting. An easier way would be a flag (atomically changed calling a method) to be checked whenever the thread can interrupt whatever it's doing. Also, using conditions can be needed to avoid the thread spins checking the flag when it hasn't anything to do. Likely, anyway, it must be waiting on more than one condition (new activity to do, or…) Therefore one ends up implementing a queue of messages, or something like a Go channel…↩
Regarding the section "I need to do something before I go".
ReplyDeleteTake a look at the Ada.Task_Identification package, especially the procedure Set_Specific_Handler (and the associated Termination_Handler access procedure).
They may provide some of the task termination functionality you're looking for.