Please
Note: MaxQueue requires StringTheory
and Reflection
One of Clarions strongest features is a built-in data structure known as a
Queue. MaxQueue builds on this feature, making queues more powerful,
easier and safer to use.
Global Unthreaded Queues
Global, Unthreaded Queues are bad because they are
not thread safe. They were popular in Clarion 5.5 and earlier (because
they could be safely used there) but from Clarion 6 they became unsafe,
and should be removed, where possible from a program. Unfortunately in
large programs this is easier said than done, and replacing global
unthreaded queues (with memory tables, or whatever) can be a large task.
MaxQueue offers an alternative to replacing global unthreaded queues, by
making them thread safe. Changes are made at the global declaration
level, but existing code which access the queue remains unaltered. [1]
This increases the safety of the global queue, while at the same time
reducing the number of changes required to existing code.
[1] Not all the queue functions are duplicated, so there are some
possible places in the code where a compile error may occur. A simple
alternate line of code can easily be used in these situations. The
compiler will alert you to places where this is necessary[2].
[2] There is one catch. There is a Clarion system intrinsic which cannot
be overridden with a method. And unfortunately the compiler will not
detect it. So this is one case where a search of the source will be
required. For more information see
Global
Unthreaded Queues.
Extended Queue Syntax (More Methods!)
While the supplied Queue procedures are adequate to
read and write to the queue, there are cases where the syntax is clumsy.
It is also notable that the syntax is similar to, but does not match the
syntax for the file drivers. MaxQueue offers additional methods which
can make processing queues simpler and more intuitive (and in some cases
faster).
New methods include
Set,
Next,
Previous,
Copy,
Add
and more.
Load and Save
Queues In Queues
It is possible, and reasonably straight-forward to
create Queues-in-Queues. However working with these can tricky, and if
they are not free'd correctly then they can lead to memory leaks. By
linking into the
Reflection accessory (which is free) safe Queue
disposes are done internally using the normal FREE method.
Run the supplied installation file.
No additional files are required for shipping. The
files in this product are copyright 2024 by CapeSoft.
This product is provided as-is. CapeSoft Software and CapeSoft Electronics
(collectively trading as CapeSoft), their employees and dealers explicitly
accept no liability for any loss or damages which may occur from using
this package. Use of this package constitutes agreement with this license.
This package is used entirely at your own risk.
Use of this product implies your acceptance of this, along with the
recognition of the copyright stated above. In no way will CapeSoft , their
employees or affiliates be liable in any way for any damages or business
losses you may incur as a direct or indirect result of using this product.
MaxQueue is a class product, and has no User Interface,
so it is completely compatible with programs running under AnyScreen.
In Clarion 5.5 and earlier, Global Unthreaded, Queues were a useful global
data structure, and they were safe to use, even across threads, because
Clarion 5.5 and earlier employed a cooperative threading model. Clarion 6
and later switched to a preemptive threading model, which made the use of
global, unthreaded, queues unsafe.
During the change over there was an incorrect assumption that global
unthreaded queues were safe, as long as they were primed once, and then
only read from. This is not the case though because queues have a record
buffer, and the act of reading a queue entry overwrites the record buffer.
Thus if multiple threads access (read or write) the queue at the same time
then bad things can happen.
The solution is to wrap the queue access code in a Critical Section. This
serializes access to the queue across threads, and thus makes it safe. The
problem with this approach is that the programmer has to be sure each
block of code is wrapped, and wrapped correctly. Mistakes in the wrapping
could lead to program hangs, data corruption, or GPF's. Perhaps even worse
all future code accessing the queue has to be wrapped correctly - so the
solution depends on future programmers on the system knowing what to do,
and to do it correctly.
Not surprisingly many programs have instead chosen to simply ignore the
problem, noting that in most real-world situations the issue is not likely
to arise in most cases.
Alas "hope for the best" is not a particularly scaleable programming
strategy, and MaxQueue offers an alternative approach which solves the
problems with minimal changes to the existing program.
The key difference when using MaxQueue is that code using the queue does
not need to be altered[1]. Rather the declaration of the queue changes.
Changing the declaration is much simpler than changing the code as it is
in one place (per app).
[1] Actually that's almost true, but usefully where it does have to be
altered it will generate a compiler error, so invalid code is
automatically detected, and can be easily fixed.
Changing the Global
Declaration
- Change the Label of the queue, by adding an underscore to the
front of the Label. For example change DepartmentQueue to
_DepartmentQueue. Do the same to the Prefix of the
queue. Add an underscore to the front of it, for example changing dq
to _dq.
- Add a new global declaration, using the original Queue label, but
setting the Data Type to Type and the Base Type
to MaxQueueGlobalClass. This object must have the
THREAD attribute ticked on (on the Attributes tab)
- Create a new global group declaration, using the original queue
label and adding Record to the name. For example
DepartmentQueueRecord. Set the prefix of this
group to be the original queue prefix. Set Data Type to Group
and Set the Base Type of this group to the new Queue
label. For example _DepartmentQueue. This new
declaration must also be threaded.
- Note: In Multi-DLL systems this step only needs to be done in the
DLL where the Queue is declared for the first time. In other words
it does not need to happen in places where the Queue is declared as
EXTERNAL.
Go to the MaxQueue Global Extension, to the Global Unthreaded Queues
tab, and add a line to the MaxQueueGlobalClass Objects
list.
This should name the three parts created above, first the object,
then the queue, then the record. For example;
You should end up with the following;
If you are declaring in the Global Data Pad;
If you are declaring in a Global Data embed point;
DepartmentQueue MaxQueueGlobalClass,THREAD
_DepartmentQueue QUEUE,PRE(_dq)
END
DepartmentQueueRecord GROUP(_DepartmentQueue),PRE(dq),THREAD
END
You can now go ahead, and compile the application. If it compiles ok,
then all the existing code working on DepartmentQueue is now thread
safe. If you get any compile errors, then carry on reading.
CLEAR(SomeQueue)
There is one line of code which may exist in your
current program, and which will be legal in the new program (ie the
compiler will not complain) but it will not work (correctly). That is
the CLEAR(QUEUE) statement.
This clears the queue record buffer, or in the MaxQueueGlobalClass case
clears the object (in other words clears all the properties in the
object.)
The call to
Clear(DepartmentQueue)
must be replace with
ClearQueue(DepartmentQueue)
or
DepartmentQueue.ClearQueue()
The good news is that if you miss a call to CLEAR, and accidentally
leave this in the program, then it will trigger a fatal call to
ErrorTrap, which will display a message, and generate GPF. The GPF
is handy because it leads you back to the line of code where you called
a method after calling CLEAR. (If you're not sure how to read a GPF, see
ClarionHub.)
Global Writing / Hold
In most cases a global unthreaded queue is written
to on the main thread, then read from on the other threads. The
considerations here do not apply in this case.
In other situations, records are written to the queue using the Add
command. This is now safe, and will work as-is.
The Put and Delete commands are more problematic. In these cases a
record is read first, then updated or deleted. MaxQueue will reget the
most recently read record before doing a Put or Delete. However calls to
Sort, Add or Delete on other threads can lead to index values being
changed, which in turn can read to the wrong record being Put or
Deleted.
So, for global unthreaded queues, that make use of Sort, Delete or Add
(on various queues at various times) it is necessary for updates on the
queue to be wrapped with the critical section. Since the read, and
write, occur in the program code it will be necessary to change the
program code.
A new method,
Hold, is used before the record is
read (via Get, Next or Previous). A hold is good for a single read. This
hold will automatically be released on the next write into the class
(via an Add, Put, Sort or Delete etc.) If a second read is done when a
row is held, then the hold is cleared. The second read does not result
in a new hold. If no write is necessary then call
ClearHold
to clear the hold on the queue. If a single read is done, and then no
write (and no subsequent read) is done, and ClearHold is not called,
then the queue will be locked until this thread ends.
In summary, Hold is designed to be used in a very tight block of code.
Hold, then Read, then Write. more code than this will likely result in
the Hold being lost.
Example
Loop x = 1 to mqg.records()
mqg.Hold()
mqg.Get(x)
! change the record here
! mqg.Put()
End
or, in old queue-style syntax
Loop x = 1 to records(Somequeue)
Hold(Somequeue)
Get(Somequeue,x)
! change the record here
Put(Somequeue)
End
Pointer
The
Pointer method returns
the index to the most recently read record. However this value could
quickly become out of date if the queue is altered by another thread,
more specifically if a record is added, or deleted, with an index value
lower than the value in the pointer.
For this reason the use of the Pointer method in the context of a
global, unthreaded, queue, where writes are possible on other threads,
should be avoided. The most common use of Pointer is simply to restore
the queue record after doing some other work. New methods
SaveGroup
and
RestoreGroup provide handy alternatives
for this situation. these methods save, and restore, the threaded group
and index values.
Compile Errors
There are two possible places in code where there
will now be an error generated by the compiler. Errors generated by the
compiler are good, because they are an affirmative defense against
problems. In both cases the error is the same;
No matching prototype available.
- When you inspect the code which caused the error it reads as
someQueueLabel.someQueueField = whatever
(or vice versa)
for example
DepartmentQueue.Code = 3
This is the long-hand version of specifying a queue field. It is
preferred over using the prefix for (dq:code) by some programmers
(including myself.) This code has to be tweaked to add the suffix
Record. Thus it becomes
DepartmentQueueRecord.Code = 3
- If you inspect the code which caused the error it reads as
How Does it Work ?
The technique of changing the original queue label
to be an object rather than a queue works because of a little-used
Clarion syntax.
As you may know, when you call the method of an object, as in object.method(whatever)
then there is a (hidden) first parameter which is a reference
to the object itself. This is why the use of OMITTED
inside a method should always use the parameter name not the
parameter number.
This happens because the compiler transcribes the text to read method(object,whatever).
In other words all methods are simply normal procedures that take a
*Class (itself) as the first parameter. From there it's a simple matter
for the compiler to route the call to the correct class method.
So In MaxQueue, if you make an object (called say DepartmentQueue) of
MaxQueueGlobalClass, and then call the method Add, then this transcribes
to
Add(DepartmentQueue)
which is exactly the syntax you would use to add to a Queue structure.
By cunningly naming all the methods (and parameters) the same as the
ones for a Queue structure, code that works on existing queues will also
work as-is on MaxQueue objects.
Internally (via the INIT method call) the object knows about the actual
global queue (_DepartmentQueue) and the threaded record buffer
(DepartmentQueueRecord). Putting all this together MaxQueue wraps up all
the existing queue functionality, and then extends it with the addition
of new methods, critical sections and so on.
The Clarion language provides a simple, and sufficient
set of commands to access a queue. However these commands can become
unnecessarily complicated at times, and in some cases the commands do not
match the commands offered by the File Driver interface. MaxQueue
extendeds the command set available, simplifying code, and making some
operations more intuitive.
Set / SetFirst / SetLast / Next /
Previous
Perhaps the most glaring omission from the Queue
command lineup is the lack of a SET / NEXT pattern which is used a lot
when managing table data. MaxQueue addresses this by adding
Set,
Next and
Previous
methods.
Set sets the current order of the queue (by calling the
Sort
method[1]), then sets the Index to the first record that matches the
current record values, or if none match then to the next record
following the current record position. Next and Previous then loop
through the queue, in the same way that the Clarion Next and Previous
commands loop through a table.
In more specific terms, SET sets the sort order and
current-record-index, while Next and Previous move the Index by 1 and do
a
Get.
If you call Set with no parameters, then the current sort order is not
changed, but the current index is set to the position of the current
record buffer (matching the fields in the current sort order.)
It is also possible to pass a simple index value to Set, and it will set
the index to that position. In this case the sort order of the queue is
not changed.
Set can take a comma separated string. This will be passed to the Sort
method, and the queue will be sorted in this order. It will then set the
index to the current record position.
Set does not read a record, the current record buffer is not altered.
Therefore the first call to Next or Previous after a SET reads the
current index position.
You do not need to call Set to make use of Next or Previous. Next and
Previous are always ready and will loop through the queue from the
current index position. In other words doing a Get, followed by Next is
allowed.
SetFirst is the same as
Set(1)
and
SetLast is the same as
Set(self.Records()).
Note 1: If you pass a sort order to Set, then the Queue will be sorted
into that order. It will remain in that order after the Set, and by
implication it will remain in that order after the Next loop.
AddTo / AddFrom / Subtract /
CopyTo / CopyFrom / MergeTo / MergeFrom
Clarion Queue functions work on a single queue
structure. But in some cases it's useful to deal with two, or more,
queues at a time. MaxQueue offers methods which allow you to move data
between queues of the same structure.
CopyTo /
CopyFrom first clear the destination
queue, then copy the records across. Thus the result are two queues
exactly the same.
AddTo / AddFrom add records from one queue to another, while keeping the
existing records. So the resultant queue contains all the original
records, plus all the new records, and may contain duplicates.
MergeFrom / MergeTo copies the records from one queue to another, but
(based on the current sort order) does not duplicate records. Merge uses
the current sort order, so call Sort before calling MergeFrom / MergeTo
to determine the fields to be checked when duplicating.
If the source queue contains a record that matches the destination queue
then the Options parameter determine if the record will be overwritten
or not.
Simple queues can be saved to StringTheory objects
using the
Save method. They can then be loaded into
queues using the
Load method. If you need to store
the queue data persistently then use
SaveFile,
and then
LoadFile to load it.
mq.Init(DepartmentQueue)
mq.Load(str)
The methods are only suitable for simple queues - in other words queues
that do not contain any references. (ie &something). For queues with
references a more suitable save, and load option - such as the one
provided by
jFiles
or
xFiles
is recommended.
The goal of these methods is not to create a CSV-export feature, but
rather as a was of persistently storing queue data.
These methods include short-hand versions which allow you to include the
queue being saved or loaded. This removes the need to call the Init
method. The Init method will be called internally, so the object is then
bound to the passed in queue, and can be used as such.
mq.Load(DepartmentQueue,str)