So now that you know all about how threads work in REALbasic, let's talk about data protection via locking mechanisms.
There are going to be times when your multi-threaded application will need to access a piece of shared data between threads. For example, two threads may be processing on the same file (one as a reader, the other as a writer). It would be disasterous if the writer wasn't finished with its operation when the reader tried to read the value from the file. So how do you deal with this sort of situation? By using a locking mechanism to manage the shared resource.
Semaphore
The first type of locking mechanism I'd like to discuss is the Semaphore. This class allows you to define how many references to a resource are available.For example, let's say that a person needs a magic bean from the jar in order to accomplish their task. Once it's done with its task, they put a magic bean back into the jar. If they try to take a magic bean to start their task and no beans are available, then the jar grabs the person's hand and doesn't release it until a magic bean is put back in.
This example demonstrates exactly what happens with a semaphore. Every time a thread calls Semaphore.Signal, the Semaphore decrements a counter. If the counter is less than zero, it suspends the thread, otherwise the thread continues to run. One thing to remember about Semaphores is that the counter is decremented for every call to Signal without regard to which thread called it. Once the counter is less than zero, the thread is locked. This is important because it means that a thread can put itself into deadlock by calling Signal too many times!
Once you are done with the shared resource (the magic bean), then the thread should call Semaphore.Release to increment the internal counter (put a bean back in the jar). If there were any threads waiting for the release, one of them is woken up and the Semaphore is signaled again.
Quick Facts: Share a single resource with a counter. Cannot be re-entered by the same thread. Each call to Signal must be matched with a call to Release. Application-wide scope.
CriticalSection
So let's take another type of example to demonstrate how CriticialSections work. There's a whiteboard, and a person must walk up and write their name (assume everyone has a unique name) on the board before they can go do their task. If another person walks up and there's a name on the board, then they must wait for it to be erased before beginning their task. However, let's say the person doing the current task happens to walk up to the board again -- they can still continue with their task because it's their (unique) name on the board still. Once the person is done with their task, they walk up to the board and erase their name.In this example, the white board is the CriticalSection. A thread calls CriticalSection.Enter (writes its unique name on the board) and can do its task, calling Enter multiple times if it wants to. Once the thread is done with its task, it calls Leave. You'll notice that this is slightly different than how a Semaphore works in that a single thread can call Enter as many times as it wants with a CriticialSection, unlike the Semaphore.
Quick Facts: Share a single resource with a flag. Re-entrancy by the same thread is allowed. Perfect for recursive algorithms. Many calls to Enter can be "erased" by a single call to Leave. Application-wide scope.
Mutex
A Mutex is a subclass of CriticalSection, so you'd expect the behaviors to be quite similar, which they are. In fact, the only differences between a Mutex and a CriticalSection is that you can name a mutex and its scope is system wide instead of application wide.Taking our whiteboard example, we only need to modify it a bit to demonstrate how a Mutex works. Let's say that all the people (threads) are inside a building (the application), and the buildings are inside a city (the system). Now the whiteboard is in the public park and the people from all the buildings have to walk out to it first. All of the same rules apply from the previous example, it's just that the whiteboard is shared amongst all buildings in the city.
So a Mutex is just a system wide CriticalSection, essentially. This allows you to access a system resource instead of an application resource. For example, let's say your suite of applications all share a special printer application -- but only one application can use the special printer at a time. You'd use a Mutex to ensure that you're the only application accessing the special printer -- the other applications would have to block if they tried to access it while you're using it.
Quick Facts: Share a resource across application boundaries (system-wide). Same behavior (otherwise) as a CriticalSection.
What's with the "Try" methods?
You'll notice that each of the various locking mechanisms have a Try method (Semaphore.TrySignal, CriticalSection.TryEnter, Mutex.TryEnter). This handy paradigm allows a thread to test whether they can lock the mechanism without actually causing themself to block. For example, let's say you're using a Mutex to determine if another instance of your application is running. You'd use TryEnter to see if you can Enter the Mutex, and if it returns false, then you know you can't. And since you're not suspended (which is what would happen if you called Enter), you're application is free to quit (or do whatever else it likes).Examples of when to use the different locking mechanisms
So now that you know the basics about how all the different locking mechanisms work, how do you know when to use each type? Here's a few examples, but each situation is different. So long as you understand how each of the mechanisms work, you should be able to look at your specific situation and determine which mechanism is right for the task.Semaphores are good when you need to manage a number of resources. For example, if you have an array of sockets, a semaphore can ensure that any thread requesting a socket is blocked if there are no sockets left in the array.
CriticalSections are good when you need to make sure that no two threads are accessing the same data at the same time. For example, if you have a file that threads read and write to, you may use a CriticalSection to make ensure that no two threads are accessing the same data. However, you need to be careful with this -- remember that the same thread can enter a CriticalSection multiple times. If you really want to make sure that nothing re-enters the code, you can use a Semaphore (with the counter set to 1 initially). CriticalSections are great for recursive methods within a thread since it means the thread can't lock itself out.
Finally, a Mutex is just a system-wide CriticalSection, so it's really only useful if you need to protect a shared resource between applications. Working with the file system, memory mapped files, a port for a socket, serial device, etc are good examples of when to use a Mutex.
So that wraps it up for thread theory in REALbasic; I'm pretty sure I've covered every aspect of the APIs. Are there things you're still confused about? If so, feel free to ask questions!
Only Google could take a posting about thread locking mechanisms and decide to put up ads for Jelly Bellys. Lol, magic beans indeed.
Hey Aaron,
Nice examples, thanks for that, now I undertsnad what Critical Sections are for!
I didn't realize the Leave method cleared all the section, I thought it just added one to the counter, so couldn't understand why it even existed when semaphores pretty much did that already. Now I understand, the idea of calling it multiple times and not having to worry about blocking when you already own the Critical Section totally makes sense.
Thanks again for these little articles, they really help clear the fog on some elements of RealBasic and programming in general.
Cheers,
Ian
Yeah, it took a long while before I got them really straight in my head as well. I used to think "why would you ever need anything more than a Semaphore", until I wrote a recursive threaded method. :-P
Glad to hear the articles help!
Thank you!