Tasking and Blocking Calls and Clean Termination

Hi,

I’m used to C/C++, learning Ada, I’m trying to implement a tetris clone.

I didn’t want to bother with a GUI framework initially, so I started with basic CLI primitives:

  • Each “frame” I just Ada.Text_IO.Put_Line to blast out the “playing field”
  • Using Ada.Text_IO.Get_Immediate to get input from the user. (Later using ada-evdev Read to get keypress events)

In order to receive the inputs from the user asynchronously, I created a separate task, which worked ok but I ran into the interesting
difference between C++ and ada: An unhandled exception doesn’t automatically terminate the program. Ok, I think its good to clean everything up anyways.

So now I end up with the following statements:

  1. I would prefer to use a blocking function for getting inputs, rather than one that does a nonblocking operation in a loop with a very short delay.
    (That would probably lead to many unnecessary context switches/syscalls? Lot of unnecessary CPU time spent spinning waiting for input?)
  2. I would like to be able to signal my thread to do any cleanup and then exit
    (maybe read any pending stdin characters and termios re-enable echo so the terminal isn’t weird when the program exits)

What is a reasonable way of doing this?

This issue probably also applies to things like network IO from sockets, but I haven’t done any socket programming in Ada yet… but I guess the story would be:

  1. I have a dedicated task for handling incoming connections and traffic.
  2. It’s blocked waiting for some client input (or some timeout?).
  3. I get an exception in some other thread and want to exit immediately.

Is throwing an abort at the blocked task the best option?
e.g.

  1. Add a handler for task termination
  2. make the non-blocking part of the task subprogram some abort-deferred block? (protected something?)

so I know that if I throw an abort at it, if it was already doing something, it will finish that thing before being kicked to the termination handler?

The reasonable way is not to use terminal I/O for the purpose. The terminal I/O is heavily dependent on the OS and terminal emulation (e.g. VT100) inside the OS. Aborting a task blocked by the OS is a bad idea too.

Regarding the sockets, to end the blocking I/O from outside the socket is closed. This cancels waiting.

I suppose the same technique can be used for a serial port I/O. Close the serial port file. Also see the package GNAT.Serial_Communications.

P.S. Any time invested into terminal I/O is 100% wasted. Start with any Ada GUI there are lots of them. E.g. native graphics GtkAda + Cairo (here). Or if you choose web-based gnoga you will find a tetris game implementation there.

It’s a bit dismissive to say that time invested into terminal I/O is 100% wasted. Computer programs (games especially) are whatever you want them to be.

I am also now using ada-evdev (arrow keys from keyboard, or potentially gamepad).

Closing the evdev input file does seem like a reasonable option, but is somewhat indirect, so it didn’t immediately occur to me.

Thanks!

Don’t be offended, it is a well-trodden path. You are not the first one going down this dead end road… :slightly_smiling_face:

For the termination question: One thing you can kind of do is make a “reverse” watch dog timer like object. Basically a Boolean that is initialized one way and as long as it stays that way, all tasks run. If a task has an exception, in the handler have it flip the boolean to the non initialized value and all the tasks can manually terminate if they see it change.

You can either use a protected type for this:

protected Watchdog is
   function Status_OK return Boolean;
   procedure Set_Not_OK;
private
   OK : Boolean := True;
end Watchdog;

protected body Watchdog is
   function Status_OK return Boolean is
   begin
      return OK;
   end Status_OK;

   procedure Set_Not_OK is
   begin
      OK := False;
   end Set_Not_OK;
end Watchdog;

or since a Boolean is atomic on probably any platform and you only change it once per platform (to signal global termination) you can just declare an atomic boolean variable:

Status_OK : Boolean := True with Atomic;

There’s probably a creative way to do it with Rendezvous too, but that was just the first thing that came to mind.

After doing some testing and poking around online, I think the behaviour of closing a FD that is being waited on isn’t reliable. In the case of monitoring evdev/stdin, the call doesn’t actually return until another character is typed.
I think this might be because the read syscall also keeps a refcount on the open file descriptor object.

You may also run into the classic race condition where the underlying file descriptors are reused. (If you just want to end some subsystem as opposed to taking down your entire program).

Any other potential solutions?

EDIT: One thing you can do with linux syscalls is any time you want your blocking state to be interruptible is to create a pipe and add it to an epoll instance (or select I guess). That way, another thread can write to the pipe to wake up your thread.

The Ada standard libraries don’t implement anything like this though.

Cancellable Read POC

The FDs have to all be made O_NONBLOCK.

    ssize_t Read(void *buf, ssize_t buflen) {
        
        ssize_t total_read = 0;
        unsigned char *ptr = (unsigned char *)buf;
        struct epoll_event events[max_events];
        while (buflen > 0) {
            int nfds = epoll_wait(mEpollFd, events, max_events, -1);
            assert(nfds != -1);
            for (int i = 0; i < nfds; ++i) {
                int const efd = events[i].data.fd;
                if (efd == mMainFd) {
                    ssize_t rd = read(mMainFd, ptr, buflen);
                    if ((rd == -1) && ((errno == EAGAIN) || (errno == EWOULDBLOCK))) {
                        continue;
                    }
                    total_read += rd;
                    buflen -= rd;
                    ptr += rd;
                } else if (efd == mWakeRead) {
                    unsigned char dat[256];
                    while (read(mWakeRead, dat, sizeof(dat)) > 0) {}
                    if (total_read == 0) {
                        errno = EINTR;
                        return -1;
                    }   
                    return total_read;
                }
            }
        }
        return total_read;
    }

What you’re looking for is handled by Ada.Task_Termination. See this.

Yes, you can register a specific handler for your task that runs on termination, but the task won’t terminate if its blocked.

One idea I theorized was to use abort-deferred regions to protect some operations, and then abort to kick the task out of the blocking state into the termination handler. This seems a bit kludgy though