2018-03-10

Tasks and exceptions in Ada

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 (withs and uses 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_Identification2 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


  1. Actually it's called environment task in the the Ada parlance.

  2. What we want is to abort the current task, something like abort This_Task, but This_Task must be a task as in task Waitress is …. For the current task there isn't a “name” like Waitress; this is why we must use Ada.Task_Identification which has Current_Task (it's a Task_Id, good for Abort_Task but not for abort)

  3. GNAT 4.9.2.

  4. In a slightly more realistic task the select maybe is inside an infinite loop and Go and Stop would do something (accept Stop do … end Stop;).

  5. 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.

  6. The class std::vector performs bounds check if you access elements using at() 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.)

  7. 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.

  8. 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…

No comments:

Post a Comment