Tasking and Blocking Calls and Clean Termination

this pins a core to 100%

+1 outside the caveat that sometimes the safest thing you can do is crash immediately rather than let your software do something unpredictable.

+1

+1, using Ada Interfaces.C you could call the underlying APIs directly. I find it more ergonomic to do the boilerplate in C and call a single function.

I’ll have to learn about how they work.

+1, these remarks were just stressing that you need to manually keep track of all the resources you can potentially block on, which I see as error-prone. Maybe they can be wrapped in some controlled type (? which I haven’t yet learned about) or handled with the aforementioned generics

I’m not 100% certain what you mean by this. Would this be a task with entry points for adjusting/retrieving various bookkeeping? (and also potentially maintaining a list of active tasks to cancel/abort)

I like this pattern a lot. I’ll have to experiment with it.

+1, I’ve been trying to focus on solutions within the ada language abstraction.
I also don’t mean to imply that these problems aren’t present in C/C++, just that I hadn’t run into them because C/C++ will crash immediately on an assert/uncaught exception.

Try changing

            if Got_It then
               Put_Line ("Input=" & C);
            end if;

with

            if Got_It then
               Put_Line ("Input=" & C);
            else
               delay 0.01;  -- adjust this to a value that makes your cpu usage acceptable
            end if;

input IO is buffered and since the average human response time is 0.250 (really fast folks around 0.100) you have some room here. This should quickly get all buffered values and then if none are available, take a short break.

True; but it’s easier to get this behavior out of a “frangible” system: have your task have an outermost exception-handler that triggers your globally-visible shutdown procedure.

That’s because (a) the delay 0.0; is a “yield if needed”, and (b) no other tasks are present to yield to (aside from the master), which means that you’re effectively running a tight-polling. — You’d probably want something like 0.2 or 3#0.1# (1/5th and 1/3rd of a second, respectively).

There is a way to use a protected-object —or its entry/procedure— to attach to an interrupt. See: Annex C, Section 3 and AdaCore’s Handling Interrupts article.

For something like a keyboard (or controller), I wouldn’t recommend going all the way down to interrupt-handling unless you need to: you should be able to get good performance w/o dropping down to that low of a level. — I’d also recommend using an (synchronized) interface to program against for player-input, so that the rest of your program doesn’t-know and doesn’t care about how commands are coming, only what commands are coming.

Side note, in some cases you can let tasks cancel themselves. If the tasks are library level, you can update your loop around your select statement to have a very specific condition:

while Is_Callable(Environment_Task) loop
   select 
      -- stuff here
   end select;
end loop;

Both Is_Callable and Environment_Task are found in Ada.Task_Identification. The environment task is the task with your main, so basically once the main completes, tasks can see that and stop executing. I don’t know if that helps you in your current use case, but something to keep in your back pocket. This is more for the polling style solutions, but just something to look into if you go that route in the end.

And where is a problem?

Same question.

Yes, you can of course change the delay to 0.1 etc. But normally you would not need that. The reason to use a delay at all is that the time quant is not zero. It depends on the OS, e.g. under Windows it is 10ms which can be reduced to 1ms. If a task does not enter a wait or initiates blocking I/O (“non-blocking” Get_Immediate may actually do just that) it consumes all the quant. delay 0.0 causes a re-scheduling and if another task is waiting for the processor it will get it. With multiple cores there is even less need for that, obviously.

Right, but that is for a bare bone case when there is no driver and no OS.

P.S. Long ago, when computers were really slow, the OS offered asynchronous system traps (software interrupts) on keypresses. I remember RSX-11M had such a thing. These days it is simply not needed and Get_Immediate is sufficient.

Which is why I advised against going w/ a interrupt-handler.

It might not be, but the response indicated some surprise that the construct would devolve into a polling mechanism (pegged CPU core). — Obviously having more/different task constructs is going to impact how things are scheduled… but he should also be made aware of the why of it.

Also, it could be considered “bad form” to consume the entire CPU/core, so a more coarse delta would probably yield more acceptable CPU-time… though @jere does bring up the point about input being buffered: if it is, it might be necessary to do a read/flush pair for correct behaivior.

Right, but in order to avoid polling there must be OS support. One of

  1. A handle to an OS event specified in the blocking I/O call to cancel I/O.
  2. An explicit cancel I/O API call.
  3. A close file call effectively cancelling I/O

It is true that Linux has very rudimentary OS API, but even under Linux I doubt that the option #3 does not work in terminal emulator.

Right, this is what I meant by “If this works”. If the asynchronouse transfer of control (ATC) can abort the blocking call, then it works; otherwise, it doesn’t.

A hybrid approach, when the blocking call is to an entry, is used in PragmARC.Job_Pools, where the logic is

      loop
         select
            Job_Queue.Get (Item => Info);
            Process (Info => Info);
         or
            delay Quit_Check_Interval;

            exit when Time_To_Quit;
         end select;
      end loop;

One should clearly understand how a timed entry call works. When you call an entry the task enters the entry’s queue and gets blocked. Upon expiration of the timeout the task is removed from the queue and the delay alternative is selected.

However when the entry call gets accepted and pending, then it is too late to abort. Execution of the rendezvous (entry of a task) or protected action (entry of a protected object) is not aborted!

Note that the language considers doing blocking stuff from a protected action an error. But it is fully legal in a rendezvous. This is a major difference between protected object and task entries. So a rendezvous will not be aborted and can block as long as it wishes.

Furthermore an entry call can be requeued to another entry. So the task will continue waiting in some other queue. When requeuing is done without with abort qualifier then this waiting cannot be aborted too. The reason is that if you started some stateful action in the first entry you might not be able to roll it back, so you must enter another entry no matter what.

In short, timed entry call is meant for other things.