Get In Synch with Semaphore Sets

Article ID: 58300

RPG programmers are used to the idea of record locks and object locks. Our well-designed programs understand that only one process at a time may be updating a record and that the others have to wait. But what do you do when you want to invent your own type of lock? Instead of locking a record or a disk object, perhaps you want to lock a variable? Or a user space? Or you want to guarantee that only one copy of your program is active. Or maybe you have a weekly batch process, and you want to prevent users from running a certain program while that process is active? Or perhaps you are sharing memory between multiple jobs or multiple threads, and you want to ensure that there's no conflict?

There are many ways to solve these problems, and this article focuses on one of them: semaphore sets. Semaphores are simple, low-level, tools that you can use to create your own "locks" to protect your resources.

The Thread Safety Problem

The most common reason for using semaphores is to preserve thread safety in a multi-threaded environment. However, the same thread safety concern also applies to an application that consists of multiple jobs if it will share the same memory between the jobs. You can share memory between multiple jobs by using a user space or shared memory segments.

From this point forward, please assume that any reference to "thread" in this article also includes multiple jobs, if they happen to share memory in this manner.

When multiple threads are working with the same memory, it creates a problem because they are accessing bytes at (potentially) the exact same time. Imagine two blind men sitting at a table with a single blank piece of paper between them. Each man has memorized exactly where the paper is and has memorized the motions needed to write a word. Then, at the same time, both men are told to write their word. What would happen? Since the men can't see each other, they're likely to write on top of each other's words, and have the letters mixed together. If they write on the paper at separate times, or on separate pieces of paper, what they write is perfectly legible, but if they write at the same time on the same piece of paper it's likely to cause a problem.

Of course, people are much smarter than computers! The men would quickly detect each other and work out a solution. Computers aren't that smart, however. In an environment, such as the IBM i, in which multitasking works well, two threads can easily try to read or write the same bytes at the same time. The results become unpredictable. If one thread is writing "Hello" to a variable, and the other is writing "World" to the same variable, you may end up with "Worlo" or "Helld" or some other strange result. Likewise, if one is reading while another is writing, the reader might happen to get a mixture of part of an old value and part of a new value.

Worse, it'll be different each time. It's hard to judge exactly which thread will be first and which will be last. So each time it'll end up working differently and causing a different result. Indeed, you might test your application and find that everything works perfectly. Then run it through an extensive set of test cases and QA testing and have everything work perfectly, only to have a jumbled mess in production, where more users are using it at the same time.

Semaphores are one way to solve this problem. There are other ways, including mutexes and space locks, but this article discusses semaphores.

The Semaphore Concept

The idea behind a semaphore is very much like the idea behind a record lock—except that there's no record! There's just a semaphore ID number. It's up to you, the programmer, to determine what sort of data is protected by your semaphore.

Under the covers, a semaphore is really just an integer. Just an ordinary numeric field, not so special after all! But it's kept in a variable inside the operating system, rather than a variable kept in your program. You can add, subtract, or get the value of a semaphore by calling APIs that manipulate the contents of that number. IBM has ensured that only one thread is able to update the number at a given time, and therefore you can never have the number can't become "halfway updated by one thread, halfway by another" because the operating system protects it.

Terminology: When software limits access to only one thread at a time, causing other threads to wait for each other, that's called serialization. You could say that IBM i serializes access to a semaphore.

Terminology: When there's an operation that can't be interrupted by other threads (e.g., adding or subtracting a number from a semaphore) that operation is said to be atomic.

A semaphore's value can never go lower than zero. If a thread tries to change the value of a semaphore to a number lower than zero, the thread gets halted until the semaphore value increases. So the thread sits and waits until the semaphore value is high enough that it can perform the subtraction without going below zero.

For example:

Thread 1 Thread 2 Semaphore value
Creates a semaphore   0
Sets semaphore to 1   1
  Subtracts 1 0
Tries to subtract 1 but has to wait Does processing on shared memory 0
... still waiting... Does more processing 0
... still waiting... Adds 1 to the semaphore 1
Wakes up; subtraction gets done ...does something unrelated... 0
Does its processing Tries to subtract 1 but has to wait 0
Does more processing ... still waiting ... 0
Adds 1 to semaphore Wakes up! Finishes subtraction 0

Of course, 1 and 0 are not the only possible values for a semaphore. You can potentially set a semaphore to a higher number if desired. Think of the value of a semaphore as the quantity of apples in a store display. One thread can say, "I have 30 apples available"; the other thread might say, "I want five apples," leaving the total at 25. A third thread might say, "I need 26 apples," at which point the third thread would have to wait. While it's waiting, the first thread might add more apples to the display, and as soon as it has done that, the third thread would be able to take its 26 apples, so it would stop waiting and continue.

Identifier-based Semaphore Sets

This particular set of APIs works on what's called a "semaphore set," which is a group of semaphores. If you think about a semaphore as a numeric variable, then you can think of a semaphore set as an array of numeric variables. It's just a bunch of semaphores.

When you create a semaphore set, you tell the API how many semaphores should be in the set. You can specify a value of one if you need only one semaphore. It's very much like creating an array in RPG with DIM(1) specified on the D-spec. It's an array and uses array syntax, but there's only one field.

Each semaphore set is identified with a "key." That's exactly what it sounds like: A "key" is very much like the key you'd have on a database table. You use it to look up the semaphore. However, semaphore keys are always numbers; you can't use names.

Semaphore sets are created by calling the semget() API and specifying the IPC_CREAT flag. IPC_CREAT means "create the semaphore if it doesn't exist" (but it doesn't return any errors if it does exist). So you'd code it like this:

    key = 123;
    num_sems = 1;
    idno = semget( key: num_sems: S_IRUSR+S_IWUSR+IPC_CREAT);

The first parameter to semget() is the key for the semaphore. In this example, if the system already has a semaphore set with 123 as its key, the semget() API returns an ID number for that semaphore. If the system doesn't have a semaphore set with a key of 123, it creates a new semaphore set. Num_sems is ignored unless a new semaphore is created, and it contains the size of the semaphore set (in this example, one semaphore in the set). S_IRUSR and S_IWUSR are authorities, and in this case I want the semaphore's owner (the user ID who created it) to get read and write access. These are the same permission flags used with IFS files; however, don't confuse the two. semget() doesn't create an object in the IFS. The IPC_CREAT flag, as mentioned above, tells the API to create the semaphore if it doesn't exist.

The Number Problem

The trickiest part about creating a semaphore set is the "key" parameter, because it has to be unique systemwide. Each application that wants a separate semaphore set needs to assign a different key number. Why is that tricky? Because the operating system itself uses semaphores! Third-party software might use them as well. Somehow you have to know that the numbers you assign won't be the same as the numbers that other applications assign. How is that possible?

Basically, it's possible if you calculate the number based on an existing unique object. On Unix systems, where the semaphore APIs originate, they solve this problem by using files on disk. You create a new, empty file on disk. Unix will, under the covers, assign a unique number to that file, because that's how Unix keeps track of its disk objects. Then, you use that number as your semaphore key! Since no two files in the same directory can have the same name, there's never a conflict.

IBM i provides that same functionality via the IFS. In other words, you can use a path name to an IFS object the same way the Unix folks use a pathname to one of their objects.

There's an API named ftok() that accepts an IFS path name as a parameter and returns a unique "token" for that IFS path name. Each program that calls ftok() with the same IFS path name is given the same token. Each program that uses a different path name is given a different token. You can use that token as your semaphore key. This makes it much easier to manage keeping the tokens (and therefore the keys) unique.

You can also specify a special value of IPC_PRIVATE for the key value to semget(), and the OS generates a new unused semaphore each time. That's an easy way to guarantee that your key value is unique, but now you have to somehow communicate the semaphore ID to the other processes that want to share your semaphore. The ftok() method obviates the need to do that. The other processes just specify the same path name if they want the same semaphore set.

Terminology: The unique token generated by ftok() and specified as the key on the semget() API call is referred to as an Interprocess Communication (IPC) key.

   /copy sem_h
  D IFSOBJ          c                   '/home/klemscot/semtest.sem'
    .
    .
    mkdir(IFSOBJ: S_IRUSR+S_IWUSR);
    tok = ftok(IFSOBJ:1);
    if (tok = -1);
       reporterror();
       return;
    endif;

I have all the prototypes and named constants used with the semaphore set APIs in a copy book named SEM_H. That makes it easy to get those definitions when I need them. I simply use /copy (as shown above) to bring the definitions into my program.

The preceding code creates a directory in the IFS named semtest.sem. I don't plan to use it as a directory. It's just an easy way to generate a unique semaphore number. After creating the directory, I call ftok() to calculate a unique IPC key. I can also specify a number in the second parameter to ftok() if I want to have multiple IPC keys for my application.

Creating a Semaphore Set

Now that I have an IPC key, I can use it to retrieve a semaphore set by using the semget() API. For example:

         set = semget( tok
                     : NUMSEMS
                     : S_IRUSR + S_IWUSR + S_IRGRP + S_IWGRP
                     + IPC_CREAT + IPC_EXCL );
         p_err = sys_errno();

         if (set >= 0);
            semval(*) = 1;
            semctl(set: 0: SETALL: %addr(semval));
         elseif err <> EEXIST;
            reporterror();
            return;
         endif;

The semget() API attempts to get the ID number of an existing semaphore set. It locates the set based on the IPC key. Because I specified IPC_EXCL, it gives me an error if the semaphore set already exists. Like the other Unix-type APIs, if there's an error, semget() returns -1, and my program can call the __errno() API to determine why it failed.

If there's no error creating the semaphore set, the preceding code calls the semctl() API to set each semaphore in the semaphore set to one.

If there's an error, aside from EEXIST (which means that the semaphore didn't exist), I call the reporterror() procedure, which reports the error to the user. (If you download the code for this article, you can see the reporterror() routine, as well as the rest of the code in its entirety.)

The reason I'm ignoring the EEXIST error is because I want to open the semaphore set if it already exists (but I don't want to set the value of all the semaphores, like I did above).

    if (set = -1);
       set = semget( tok: NUMSEMS: S_IRUSR+S_IWUSR );
       if (set = -1);
          reporterror();
          return;
       endif;
    endif;

Hopefully you followed my logic. If the semaphore set doesn't exist, I want to create it and set the value of the semaphores in the set to 1. If it does exist, I want to simply open the existing one.

Locking/Unlocking a Resource with a Semaphore

Now that I have a semaphore open, I can use it as a build-your-own lock. When I want to request the lock, I call the semop() API and tell it to subtract 1 from the semaphore.

If the semaphore doesn't currently have a value of at least 1, it can't do the subtraction, and the job is suspended (in SEMW status) until the semaphore has a value of at least one.

         myops(1).sem_num = 0;
         myops(1).sem_op  = -1;
         myops(1).sem_flg = SEM_UNDO;

         status('Waiting for lock...');
         rc = semop( set: myops: %elem(myops));
         if (rc = -1);
           ReportError();
         endif;

The semaphores in the set are numbered starting with 0. Because this particular semaphore set has only one semaphore, the semaphore number is 0, which indicates the first semaphore. So myops(1).sem_op is set to -1 to tell the system to subtract 1 from the semaphore.

The SEM_UNDO flag tells the system that you want to undo your semaphore operation if the activation group ends. That way, if something unexpected happens (e.g., the program crashing) it automatically releases the lock it held.

Unlocking the semaphore is just a matter of adding 1 to the semaphore value:

    myops(1).sem_num = 0;
    myops(1).sem_op  = 1;
    myops(1).sem_flg = 0;

    rc = semop( set: myops: %elem(myops));

Semaphores wait until the semaphore has a high enough value to perform the subtraction, the semaphore is deleted, or the job receives a signal. Therefore, if you want to wait for only a limited time before giving up, you could use the alarm() API to send you a signal. If you are interested in reading about that, send me an email at tips@scottklement.com, and I'll cover that technique in a future article.

Deleting a Semaphore Set

The semctl() API with the IPC_RMID flag can be used to remove an existing semaphore set from the system. You need only pass it the ID number (returned by semget()), and it deletes the semaphore from the system.

     semctl(set: 0: IPC_RMID);

Code Download and Documentation

I've written a quick program so you can try semaphores and see how they work. You can download that sample program along with the required copy books.

Another nice feature of the semaphores is the SEM_UNDO flag mentioned at the end of Scotts article. Yes, ALCOBJ would also release the lock if the activation group ended, but the value of the data area would not be reset. I believe the semaphore handles this. (Although, I certainly would never want to use semaphores in everyday programming.)

I look forward to any follow-up article(s) on the topic.

Now this is good... I think I was unwittingly playing devil's advocate in my response to Scott's article, but Scott didn't bite. I'm glad you did though, Wilson. You brought up an aspect of lock control that simply isn't foremost in my thinking 99% of the time - performance. That's true to a large extent because of the applications I support, which are not transactional in nature, and the software that I write, which tends to be utility/tool oriented, but also because of the incremental performance improvements we've gotten with this platform over the years - IBM has always managed to keep my response times sub-second no matter how sloppy, er, I mean, "non-optimized" my code might be. So, I have yet to be adversely affected by a lock operation that potentially consumes a few milliseconds.

However, I can easily imagine a banking application, as you mention, traversing through millions, or even billions, of transactions, and having the need to set locks on and off for each atomic operation. So, in that scenario I can see those "few milliseconds" adding up to a sizable chunk of change. And if those little nannites, uh, semaphores can chomp that latency down to microseconds or less, then my money's on them suckers as well. And who knows - you didn't say what bank you're with - maybe my money is riding on those little speed demons! ;-)

Thanks for making me think just a little bit more...

Regards,
Jim Rothwell

Jim, this particular topic is one that I've put off writing about for 2 years, and the reason is the exact reason that you've mentioned. Why would I ever want to use this instead of the standard native ways of doing things? Recently, however, I received three separate (unrelated) requests from readers to cover these topics, so I began to cover it. Unfortunately, there is way too much to cover for me to cover it all in a single article, so I began with one about semaphores.

Do I think the average RPG programmer is going to use semaphores in his average daily work? Yikes, I hope not! But occasionally something unusual comes up.

Understanding how things work at a lower level is often very useful, even if you don't use it directly. And if you ever need to use it directly, you'll have a resource you can use to learn it from.

If you have better ideas for what I should be covering -- or better yet, if you'd like to write about something -- I'd love to hear about them. Send me an e-mail at tips@scottklement.com.

I disagree from Jim. I use semaphore sets on a banking app where time and resources are VERY important and ALCOBJ consumes a lot of both. In a IBM's manual (I don't remember which one) there is a scaling of resources and time usage for all sync methods and the less "expensive" is MemSwap followed by semaphores. The last in the list is ALCOBJ! So, semaphores are not just to "play" but I think that it's all about to find the best solution for each problem. Scott, it will be fine to talk about what happens if a thread ends abnormally without "unlocking" the semaphore and which methods we can use to avoid that. Regards, Wilson Alano
Scott, I like playing with new or different technology as much as the next iGeek, but I think using semaphores on the i would be just that - playing. I appreciate you sharing your research into semaphores, and I understand that they have a bit of flexibility, but I've looked at them before and didn't see any compelling reason to change what I use - a simple, 1-character data area.

It seems so much easier to simply allocate a data area. I don't have to be concerned with a system-wide naming conflict, as I can simply make it unique within a library of my choosing. I don't have to use any APIs to manage it - I simply use the CRTDTAARA and ALCOBJ commands. And I don't even have to manipulate its contents, since usually all I care about is whether it's locked or not. And if I do have data to be shared with concurrent processes, then I can accommodate that need as well, increasing the data area's size or changing its type as necessary. Plus, I seem to have more flexibility with object locks, since I can choose how "tightly" to lock, i.e., I can lock exclusively, shared-for-read-only, shared-for-update, etc., if I were to have complex concurrency situations that would call for such control. I also can easily specify the scope of the lock via the ALCOBJ command's SCOPE parameter - specifying JOB or THREAD scoping, for instance. And, finally, I can control how long I want to wait to obtain the desired lock before giving up, even getting as fancy as specifying a previously-defined CLASS's wait time rather than hard-coding a literal value.

The purist in me looks over my argument and says "but that's not the purpose of data areas - you're obfuscating what you're actually doing", but the pragmatist in me says "sometimes going with the simplest approach that gives you everything you need trumps 'purity'". And, seriously, it's not that confusing to the average maintenance programmer who comes along later.

Bottom line, sometimes we just don't need to seek out the Unix-based solutions; sometimes the i's native solutions do the job just fine.

But thanks, as usual, for the article. If nothing else they never fail to make me think - either about what I'm doing and why, or about what I'm not doing and why not.

Regards,

Jim Rothwell

ProVIP Sponsors

ProVIP Sponsors