Fundamentals of Multi-User Programming in PowerBASIC
By Michael Mattias

As mainframe computers continue to be replaced by networked personal computers and other forms of client-server and shared-resource systems, more and more the PC programmer, including the PowerBASIC programmer, is required to design and write multiuser software. 

While processing and user interface are similar in the two environments, there are two items to which greater attention must be paid when writing multiuser systems versus single-user systems: file access and data integrity.

The PowerBASIC compiler provides all the the tools needed to address these concerns; here are some methods of using them.

FILE ACCESS
First of all, sequential files are not suitable for use as shared resources: shared files should be suitable for random access; that is, should be OPEN'ed by PB for RANDOM or BINARY. In our examples, a RANDOM file with a fixed record size is used .

PB addresses our requirements for multiuser access to disk files through the use of two options in the OPEN verb: ACCESS and LOCK.

The ACCESS option controls how this program will use the file: for input only (READ); to update existing records and add new records (WRITE); or for both input and output operations (READ WRITE). The LOCK option controls how other programs (including other copies of this program) will be permitted to open and use the file - LOCK READ to prohibit input, LOCK WRITE to prohibit output, LOCK READ WRITE to prohibit any access, or LOCK SHARED  to allow all other programs access to the same file.

As we'll see later, even if we are only going to be adding records, we will always need read access to the file; so the only viable choices for ACCESS are READ (for inquiry-only programs) or READ WRITE (for programs which add or change records). 

What about  the LOCK option of the OPEN verb? For normal daily processing work, SHARED is preferred. This will permit adding new routines to the system without the need to modify file access in existing software. 

The exception to SHARED use occurs when performing a major file task, such as an index reorganization or "packing" (eliminating deleted records and the space they use). In this case you should attempt to open the file ACCESS READ WRITE LOCK READ WRITE: the OPEN will fail if any other user has the file open; and, if you are successful opening the file, no other program will be able to use the file until the major process is done and the file has been closed.

To recap, the recommended OPEN for files in a multiuser environment is:

  OPEN MyFile$ FOR RANDOM ACCESS {READ|READ WRITE}   LOCK SHARED  _
     AS Handle LEN=MyFileLength

DATA INTEGRITY
Data integrity is complicated in the multiuser environment by the fact that many users may be attempting to update the same record at one time. Assume you have a customer file which looks like this:

TYPE CustomerRecord
  AcctNo         AS INTEGER
  AcctName       AS STRING * 30
  Address        AS STRING * 30
  CityStateZip   AS STRING * 30
  AmountDue      AS SINGLE
  Status        AS  STRING * 1
END TYPE

The AcctNo field is used as the random file record number.

You are providing three functions which can update this file: customer maintenance (add/delete customers and change address), billing (adds to AmountDue) and credit memos (subtracts from AmountDue); in addition, you offer an inquiry function which is used only to look up addresses. 

What if two or more separate functions need to access the same customer at the same time? This scenario would not be unusual:

User # 1: Customer Maintenance. Typically, file maintenance routines retrieve the current record, display it on the screen, let the user change fields, and then store the record back on disk. However, customer file maintenance may not change AmountDue since all adjustments to this field are to be made by billing or credit memos to insure proper audit trails.

User # 2: Credit Memos. This user is entering credit memos, which will adjust AmountDue; this routine would typically work by accepting the customer number and displaying the customer's name and address on the screen for confirmation prior to accepting input. But just as customer file maintenance cannot change AmountDue, credit memo processing may not change any of the static customer data.

Let's say User # 1, customer maintenance, retrieves the record for customer number 100 and displays it on the screen. While this information is still on this user's screen, the accounting clerk calls up the same record to process a credit memo. The information is put on her screen as well.

The accounting clerk finishes entering the credit memo before the file maintenance clerk is done, presses the "save" key, and stores customer # 100 back on disk - with a new AmountDue.

When the maintenance changes are complete, the maintenance clerk also presses the "save" key and stores the customer record to disk: except that since this user does not "know" about the credit memo, the AmountDue field is stored as it was; that is, as it was before the credit memo was entered.

Similarly, if file maintenance "saves" first, when the accounting clerk finshes the credit memo, the customer's original address will be saved, negating the file maintenance.

This is where locking  comes into the picture. Locking a record prevents any user other than the "locker" from reading or updating the the same record. So a possible solution occurs immediately: whenever a user who is going to update a record retrieves the record,  they lock it. 

This "solution" (which, sadly, has found its way into several commercial applications) is not acceptable; because when a record is locked, it is not available to any user for any purpose, even READ only. In the Real World, the file maintenance user may retrieve the record and then take a phone call or go to lunch, preventing any other user from entering orders, billing, doing additional maintenance, or even just inquiring for a mailing address or phone number.

The solution is found by doing a little extra work. For an update user, this five-step approach is time-tested:

1. Present the desired record on the screen for edit or confirmation by reading (GET) the record from disk without locking . Save the contents of the record as read in a dataname other than the file buffer.

2. When all edits are complete and you are ready to store the record, LOCK the record.

3. Read the record from disk again, obtaining the current copy; (this is why the record is saved in another area of memory in step one: this second GET will overwrite the buffer) .

4. For each field the user is permitted to change in this program, compare the contents of the  "saved" record (see step one) with what was entered on the screen. If they are different, move the screen data to the file buffer (changing only what this user actually entered).

4. Write (PUT) the record to disk.

5. Unlock the record.

Using this sequence, the record is only locked for a fraction of a second, eliminating the possibility of the phone call or lunch break interrupting business.

However, the record is locked and any other program attempting to access that record will receive an error on their GET. The solution? Use a subroutine like ReadCustomerRecord, shown in Figure One, which allows for that fraction of a second before returning failure to the user. This routine should be used for all GET's, including the two separate GET's needed for update, and for all GET's for INPUT (read-only) users as well.

LOCKING
We've talked about locking records, but we haven't shown the use of the PB LOCK verb. That's because there are two types of locking, either of which will work.

Physical locking uses MS-DOS to control record access; and is invoked by use of the PowerBASIC LOCK verb. When using physical locks, attempts to access a locked record will result in PowerBASIC returning an error, and must be handled with ON ERROR .

Logical locking bypasses MS-DOS and PowerBASIC; and instead relies on a field in the data to indicate that the record is currently in use. In our example, this field is Status. (This type of field is also useful for marking records logically "deleted"). If this field is equal to some programmer-defined value (e.g., "L" ) , it means the record is currently locked for update by another user.  Other update users should interrogate this field; if the indicator is set, the record is locked and unavailable for update. INPUT (inquiry) users don't really care, since they are not going to update the record anyway. 

Figure One, written as an $INCLUDE file, contains three SUB's used in the example program: ReadCustomerRecord, LockCustomerRecord and UnlockCustomerRecord. By changing the value of the equate %PhysicalLocking, these subs may may used for either physical or logical locking. They all return a status code: zero indicating the operation was succesful, any other value indicating an error (set to the PowerBASIC error code). The delays for temporarily-locked records is included.

PUTTING THE PIECES INTO A POWERBASIC PROGRAM
Figure Two is a bare-bones multiuser customer maintainance program using this five-step method. In our example the "saved" record is not used; but if your program used any kind of full-screen editing (as opposed to LINE INPUT), you'd find that the saved record image is necessary (try it!).

The first thing we do in this program is to test for the presence of MS-DOS SHARE. Figure One also contains PB code to detect if SHARE is loaded ( under DOS). 

When running in a DOS box under WindowsR, this function may report that SHARE is loaded even when it isn't. If your program will be running this way, you may test for the presence of SHARE by attempting to physically lock a record (permitted even if your ACCESS is READ); if the lock fails, for a reason other than "permission denied", the services provided by SHARE are not available and the program should terminate. "Permission denied"  (PB error # 70) will occur if another user has your record physically locked at the time.

A FEW EXTRA TIPS
If using the logical locking method, one function you should write into your software is an "unlock" for either one specific record or for all records. If the system should go down while some user has a record locked, none of your regular programs will ever be able to retrive the record to clear it ( remember, the "L" was saved to disk as part of the logical locking process).

SHARE and locking may work differently when running in an MS-DOX "box" under Windows® PowerBASIC may return different error codes under selected circumstances, and network software may return different results depending on permissions and other forms of  the network setup. To test in your environment, I've written a program to open and close files with different ACCESS and LOCK, and read, write, lock and unlock records in that file. You can load this program either on multiple PC's on a network, from multiple DOS boxes under Windows or OS/2, or using a task-switcher such as DesqView. This program, MULTIUF.BAS is available ALAN ON YOUR BBS AND OTHERS DETAILS TO FOLLOW.

SUMMARY

I'm sure some of you have looked at software on the rack or in the trade papers, and have seen that the "network" version always has a higher price. You might have thought that this was the classic rip-off because all the publisher had to do was change the OPEN statements on the files.

This article has shown you that there are many other things involved (and when you get into multiple-file update there's even more complexity), and will help you move into the world of multiuser systems.

Thanks to Tim Schreiner. DesqView is a registered trademark of Quarterdeck Systems. PowerBASIC is a trademark of PowerBASIC, Inc. This article copyright (c) Michael C. Mattias.

 #############################################################################

FIGURE ONE: USEFUL FUNCTIONS
$IF 0
  FIGURE1.BAS  - $INCLUDE file for MULTIA.BAS
$ENDIF

' ITEM ONE: Detecting MS-DOS SHARE

FUNCTION ShareThere% PUBLIC
  ShareThere% = -1                      'assume Share is loaded
  REG 1, &h1000                         'set register AH 1 to 10h
  CALL Interrupt &H2F
  AL% = Reg(1) AND 255                  'isolate the result in AL
  IF AL% <> &HFF THEN ShareThere% = 0   'NOT loaded !
END FUNCTION

' ITEM TWO: Read a record, test if locked

SUB ReadCustomerRecord (RecordNo,DataRecord AS CustomerRecord,Status)

 ON LOCAL ERROR RESUME NEXT

 Status = 0

 GET %CustFileNo, RecordNo, DataRecord
 Status    = ERRTEST                      'set the error

 IF    Status   = 0 _                     ' good read
  AND  %LogicalLocking _                  ' logical locking
  AND  DataRecord.Status = "L" THEN       ' locked by another user

    Status = %PermissionDenied

 END IF

 IF    Status = %PermissionDenied THEN   ' wait and retry

    DELAY ReadDelay!
    GET %CustFileNo, RecordNo,DataRecord
    Status = ERRTEST                  ' and get new error status

 END IF

     ' so when we are here Status contains success (0) or failure.

END SUB

' ITEM THREE: Lock a record

SUB LockCustomerRecord (RecordNo, Status)

 LOCAL ReadStatus

 DIM DataRecord AS LOCAL CustomerRecord

 ON LOCAL ERROR RESUME NEXT

     IF %PhysicalLocking THEN        ' use PB and MS-DOS services
       LOCK %CustFileNo, RecordNo
       Status=ERRTEST
     ELSE
       CALL ReadCustomerRecord (RecordNo,DataRecord, ReadStatus)
       ' get current copy of the record
       IF ReadStatus THEN        ' some error in the read - maybe locked!
         Status = ReadStatus     ' return it to the main program
       ELSE
         DataRecord.Status = "L"               ' mark the disk record "locked"
         PUT %CustFileNo, RecordNo,DataRecord  ' and store it
         Status = ErrTest                      ' could get error on PUT!
       END IF
     END IF

END SUB

' ITEM FOUR: Unlock a Record

SUB UnlockCustomerRecord (RecordNo, Status)

    ON LOCAL ERROR RESUME NEXT

    DIM DataRecord AS LOCAL CustomerRecord

    IF %PhysicalLocking THEN        ' use PB and MS-DOS services
      UNLOCK %CustFileNo, RecordNo
      Status = ERRTEST
    ELSE
      CALL ReadCustomerRecord (RecordNo, DataRecord ,Status)
      IF Status = 0 THEN
        DataRecord.Status = SPACE$(01)   ' clear out the "L"
        PUT %CustFileNo, RecordNo,DataRecord
        Status = ERRTEST
      END IF
    END IF

END SUB

FIGURE 2: SIMPLE PB-DOS CUSTOMER MAINTENANCE PROGRAM

'FILE : MULTIA.bas by Michael Mattias October 1995
' bare-bones multiuser customer file maintenance
' Code for article
' Article copyright (c)  Michael C. Mattias
' This code hereby placed in the public domain by the author, October, 1995.

'Constants and types:

DEFINT A-Z

%TRUE = -1: %FALSE = 0
%PhysicalLocking  = -1                  '   (user option)
%LogicalLocking   = NOT %PhysicalLocking
%CustFileNo       = 3
%PermissionDenied = 70                  ' PB error number

TYPE CustomerRecord
 AcctNo       AS INTEGER
 AcctName     AS STRING * 30
 Address      AS STRING * 30
 CityStateZip AS STRING * 30
 AmountDue    AS Single
 Status       AS STRING * 1
END TYPE

' Assuming Customer "AcctNo" = Record number in the file.

' SHARED data:
 SHARED ReadDelay!
 ReadDelay! = .5            '(seconds) should be more than enough

'DECLARES (optional as sample code is structured)

 DECLARE FUNCTION ShareThere() AS INTEGER

 IF NOT ShareThere THEN
  PRINT " Sorry, SHARE is not loaded. Load SHARE and try again"
  END
 END IF

 DIM DataRecord   AS CustomerRecord
 DIM SaveRecord   AS CustomerRecord   ' not used in "bare-bones", but
                                      ' needed for "Real World"

 OPEN "Customer.dat" FOR RANDOM ACCESS READ WRITE LOCK SHARED _
    AS %CustFileNo LEN= LEN(DataRecord)

 INPUT " Enter Customer Number to Update", CustNum

 ' read it ....
 CALL ReadCustomerRecord (Custnum, DataRecord,Status)
 IF Status = %PermissionDenied THEN
  PRINT " Can't update this customer - record in use"
 ELSEIF Status <> 0 THEN
  PRINT " PB Error on GET # ";Status
 ELSE
  ' record available, so save the image used by operator...
 SaveRecord = DataRecord   'earlier PB many need intermediate dataname!

' .. now get the new data from the user ...

    NewAddress$= "" : NewCityStateZip$ = ""

    LINE INPUT " Enter New Address <Cr>= no change", NewAddress$
    LINE INPUT " ENter New City,State,Zip <Cr> = no change", NewCityStateZip$

 ' do any editing. When ready to save, lock the record:

   CALL LockCustomerRecord (CustNum,Status)

' .. now get a current copy of it ....
    IF NOT Status THEN
       CALL ReadCustomerRecord (Custnum, DataRecord,Status)
    END IF

 ' if this user has changed data, move it out to the buffer:

   IF NOT Status THEN                       ' OK on the LOCK and READ
     IF NewAddress$ <> "" THEN              ' move out the changed fields

       LSET DataRecord.Address = NewAddress$
     END IF
     IF NewCityStateZip$ <> "" THEN
       LSET DataRecord.CityStateZip = NewCityStateZip$
     END IF

     ' and store the record back to disk..

     PUT %CustFileNo, CustNum, DataRecord

'     .. and unlock the record..

     CALL UnlockCustomerRecord (CustNum,Status)
   END IF

   IF Status THEN
    PRINT "Error on file access"
   ELSE
    PRINT " Customer file updated"
   END IF

 END IF

$INCLUDE "FIGURE1.BAS"

' *** END OF MULTIA.BAS ***

Comments
This article originally appeared in the December 1995 issue of BASICally Speaking magazine as the copyright intellectual property of the author except for code segments, which. were placed in the public domain at that time. 

On June 3, 2005, the author combined his text and the separately published code segments "FIGURE 1" and "FIGURE 2" into a single document file in Rich Text Format. The only other changes effected  were paragraph and page formatting.

June 3, 2005:  The author and copyright owner hereby places all text and code segments of this article into the public domain and expressly relinquishes all copyright claims on this work.

Michael C. Matttias
Racine WI USA