Glory to the multi-threaded nature of BeOS.
It is one of the reasons for the snappy, responsive feeling we get when using our beloved operating system.
It is also a debated feature that will always give you one thread per window that you create.
It will definitely give you headache when you want to write a single-threaded application.
This article will assist you through the steps of how to build a single threaded application running under BeOS.
When working on the BeOS port of Opera,
one of the things we needed to do was to find a good solution for how to handle a message based single-threaded application,
which Opera was, with the fact that BeOS is happy to create one thread per window by default.
I will let you in on the historical details on how we made this possible.
Step 0: Chaos
In the beginning there was chaos.
We decided to postpone caring about the synchronization problem until later.
The problems would only show up when using more than one open window.
Step 1: The Multi-Threaded Dead-end
Being an enthusiastic BeOS programmer, I really wanted to make Opera a multi-threaded application.
I started to examine the Opera code to find out where to put the locks needed to make it all work.
My fellow collegues told me that it was not doable, but being stubborn, I spent two weeks on the problem.
I arrived at a point where I was pretty close getting the right locks in the design.
The problem was that I could not gurantee that it would work,
whether there would be issues with deadlocks or race conditions that might be still left in the code.
So, I rolled back my work, and the result was that I learned a lot about the Opera code and multi-threading,
and also some things about myself.
What we did know from my work was that we had to make the BeOS version behave as a single-threaded application.
Step 2: You are not allowed to unlock this door
To synchronize the window threads, we knew that we needed a global lock,
making sure that only one thread is processing messages at a time.
That was not a difficult task, just acquire the lock when you want to process a message.
The problem is that you might want to access one window from another window's message loop.
If one of the windows has already taken the global lock,
and the other one is trying to lock the other window to be able to manipulate it, it will result in a deadlock.
To avoid the deadlock, we unlocked a window before we acquired the global lock,
before handling the message, then we locked the window again. Problem solved.
Later, we read a newsletter article from Be, saying that you should not unlock a BLooper while handling a message,
that is a big no-no. So, we used this solution, that actually seemed to work, until a real solution to the problem was in place.
Step 3: The worker thread
Next, we introduced a worker thread, that handled all messages.
If a window thread received a message, it posted a message to the worker thread that actually processed the message.
Now, all code belonging to the application was run in the same thread, and that should have made everything work as expected.
Beta 1 to beta 6 used this syncronization model. However, some people complained about that Opera was unstable on their machines,
indicating that there was a bug in our syncronization code.
Step 4: Introducing the counting semaphore
When analyzing the worker thread synchronization model,
we found out that it was very hard to guarantee that there were no race conditions in existence.
We made the assumption that since it was hard to prove the correctness, it was likely that there was,
in fact, race conditions causing trouble. Never trust code that is hard to fully understand.
What we did to make synchronization work better was to let the worker thread, which still was kept,
acquire the semaphore as its first action (it was a regular thread, not a BLooper).
Every time a message was posted, the semaphore was released.
Since the semaphore works as a counter, we would acquire all the messages that are posted,
and if no messages were posted, we would wait for the semaphore to be released. Quite simple, runs beautifully.
Has been in Opera for BeOS since RC1.
After implementing this code, a lot less people complained about stability issues,
but there were still a few that could not get Opera running without problems.
If this was a problem with our synchronization model, or if it was a bug in BeOS, I do not know.
A full binary compatible release of OBOS might give us an accurate answer. :-)
Step 5: Running code in the window threads
When trying to get version 4.0 up and running, another synchronization model was born.
The idea was that all system messages were to be handled in the window thread directly,
without posting it to the worker thread. The global lock from step 2 was reintroduced.
However, one window thread is not allowed to do anything on another window that requires the window's looper to be locked.
Instead, a message needs to be posted if we want another window's state to change.
There is one restriction if you want to use this synchronization model.
You should only do updates to a window when processing draw messages to the window through the BWindow message loop,
you are not allowed to call the Draw method from the outside. If you do that, you will risk going into a deadlock.
That is, always call BView::Invalidate() when you want an update to occur.
The 3.6 code base of Opera violated this restriction,
and that is why we did not use this synchronization model in later implementations.
Of course, the source code is available.
This is not the actual code that we used in Opera, but a simplified version made to be easy to understand.
I do not gurantee that it will work as intended, but at least you will be able to see some details with it,
as it is often easier to understand these things with code than with plain text.
Feel free to use it and write those fantastic single-threaded applications that BeOS has been missing for a very long time.
When to use single threading
If we are going to write single threaded applications, we need a good reason to do that.
The most important reason is if you want to port an already existing application that is single-threaded.
My advice: keep it single-threaded.
Another reason is that it is simpler to write single threaded applications,
at least if you are writing more complex applications, where a lot of data objects are shared between a number of documents.
If you are writing a multi-threaded application, one that is using the one thread per window model,
and one that is handling its own document (maybe a graphics manipulation program),
you are not guranteed to get the responsiveness that you may have been hoping for,
especially if you are spending too much time processing a message.
Single-threaded does not always mean easy to implement
The most important aspect of writing single-threaded gui applications is that you must not lock the message loop for a very long time.
If processing a message takes more than 100 ms, I am sure that most users will notice that.
To avoid lock-ups, you will need to pause any processing that will take a lengthy amount of time,
and tell the message loop that you want to continue processing the message later.
One very important thing about single-threaded gui applications is that they are based on a message loop.
Instead of using threads in our message loop,
we are doing asynchronous calls that will give us a message when there is data on a socket,
when a timer has timed out, or when an asynchronous file read has been done.
In BeOS, we will implement these asynchronous services as threads.
So, writing what I call a single-threaded application does not mean that we can skip using threads,
we just use threads in a different manner.
For one group of applications the single-threaded idea breaks.
It is when you are writing a processor intensive application,
and you want to make use of all the processors available in the system.
A solution for this problem exists, too. Extending the idea of using asynchronousity,
we can create a thread that works on some data that is totally independent of the rest of the application,
and will take some time to process. When it is finished, it will post a message to the message loop.
Now, we will have a truly (but not pervasive) multi-threaded application,
where all messages are processed in one message loop, one message at a time.
There are a lot of people advocating this kind of threading model, and I think that it has a few good points.
But I actually like multi-threading. BeOS has pervasive multi-threading support built into the system.
If you are able to, then use it. Don't fight the system if you don't have to.
Source Code:
stthread.zip