This is an Ada 2012 package that provides a task pool system for jobs which each take the form of a single request that receives a single response. By limiting the communication patterns to this simplified model, it is possible to enable multi-tasking in suitable applications without needing to deal directly with Ada's tasking features.
The package is provided under the permissive ISC-style license:
Copyright (c) 2015, James Humphry
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
The jobs to be processed by the pool must be characterised by a definite
tagged type. Values of this type should be able to describe both the
request being made, and the response received. An Execute
function
must exist:
function Execute
(R : in out Reqrep) return Reqrep_Return_Status
This function must perform the necessary work, make any necessary
changes to the request-response object and then return Success
or
Failure
from an enumeration type Reqrep_Return_Status
defined in
the package.
The actual task pool is then created by instantiating a generic package within Reqrep_Task_Pools with suitable parameters:
generic
type Reqrep is tagged private;
with function Execute
(R : in out Reqrep) return Reqrep_Return_Status is <>;
Number_Workers : Positive := 1;
package Task_Pool
The tasks in the pool will be created automatically when the package is instantiated. If the application is likely to be CPU-bound it would be better to limit the number of workers to the number of CPU, but if it is IO-bound then a larger number of worker tasks may be optimal.
The new Task_Pool package defines a new type derived from the supplied type that contains additional information about the job status. You do not have to create these manually as the conversion will happen automatically when the job requests are made, but it is normally useful to be able to query this information when the response is returned.
Requests are submitted by using Push_Job
in the Task_Pool package.
This takes a request-response object, and a timeout which by default is
set to 60 seconds. If a job can take legitimately take longer than 60
seconds this must be increased.
Behind the scenes the job request-response objects are added to a FIFO queue from which jobs are removed by the worker tasks. The results are returned on a second FIFO queue, but due to non-deterministic scheduling, there is usually no guarantee that the order will be preserved, even if the jobs are all known to take the same amount of CPU time. This is why the request data and response data are always kept together.
After the work is performed, request-response objects can be retrieved
by using Get_Result
. There are two possible return types - either the
type specified by the user or the extended derived version. If the
extended version is retrieved it is possible to find out the status of
the response using the Status
function.
The worker tasks will not automatically shut down when the main task
ends. It is therefore necessary to call the Shutdown
procedure to
make sure they are all terminated.
Apart from Success
or Failure
, it is also possible to see
the Timeout
status if the run-time of the Execute
function exceeds
the limit set. Remember that the request-response object may be in an
inconsistent state in this case.
If an exception arose that was not caught and dealt with within the
function, the status will be set to Unhandled_Exception
. A copy of
the exception occurrence is saved and the response queue is frozen so
that it is clear which request-response the saved occurrence relates
to. The procedure Get_Exception
can be used to retrieve the exception
occurrence for logging or display, and it will also unfreeze the
response queue.
The procedure Discard_Exception
can be used to unfreeze the response
queue if there is nothing useful that can be done with the exception
information.
As a result of the need to freeze the response queue, unhandled exceptions may damage performance. It is recommended that exceptions should only be propagated where they are truly unpredictable. For example, if the task pool is being used to retrieve a data from resources on the internet, a network connectivity failure is not really unpredictable and the request-response objects should be able to indicate this without raising an exception.
Two examples are provided, simple_example.adb
which illustrates the
basic mechanism, and error_handling_example.adb
which demonstrates the
error handling mechanisms discussed above.