Displaying Newsletter
Issue 18, 18 May 2002

  In This Issue:
Building single threaded applications by Nicklas Larsson 
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:

Device driver basics by Michael Phipps 
Device drivers are difficult to write. Understanding the hardware can be the hardest part. Often the documentation is hard to read and understand because it is written from a hardware designer's perspective (that is, if you can get the documentation -- many manufacturers are very reluctant to give it out). Drivers work directly with the kernel - a bad pointer can crash the whole OS. And, finally, you can't use warm and comfortable debugging tools, since you are working in the kernel.

Still sure that you want to write one? OK. Let's talk a little bit about the API.

There are 5 (!) functions and one global that you must export. They are:

  • status_t init_hardware(void)
    called when the system boots. Must return B_OK if the hardware exists.

  • status_t init_driver(void)
    called to initialize your driver. Must return B_OK, or the attempt to open the driver will fail.

  • void uninit_driver(void)
    called when driver is unloaded from memory.

  • const char **publish_devices(void)
    returns an array of char *'s that name the devices this driver will export. Names are relative to /dev.

  • int api_version
    this global tells the driver system which version of the API you built with. Populate it with B_CUR_DRIVER_API_VERSION.

and, finally:

  • device_hooks *find_device(const char *name)
    returns an array of function pointers that provide the dev file system calls to: open, close, free, control, read, write, select, deselect, readv, writev (in that order)

So, to look at this from a very high level, implement the 5 directly called functions, the global export, and some/all of the device hooks, and you are all set. The API here is very easy to understand.

Let's talk a little bit about how to build and test these. A simple way to build is to use Be's makefile-engine. These three example lines demonstrate what needs to be set:

NAME= usbrawpci
SRCS= usbraw.c 

The driver should be copied into ~/config/add-ons/kernel/drivers/bin. A link should be made from there to the location under ~/config/add-ons/kernel/drivers where you want the driver to show up in devfs (/dev). For example:

ln -s ~/config/add-ons/kernel/drivers/bin/usbrawpci

Debugging is hard. No two ways about that. The best way is with a terminal hooked up to your serial port. Assuming that you do not have such a thing (I don't), you can enable logging to a file. Look in ~/config/settings/kernel/drivers/sample. There is a file there named "kernel". It is a sample of a kernel config. Copy it up one level (to the drivers directory) and uncomment the line "syslog_debug_output". This causes the kernel to write the log to /var/log/syslog. Now you can use dprintf to print data to the syslog - it works pretty much exactly like printf.

Synchronization is important because you could (potentially) have two or more users using the driver at the same time. Imagine 2 apps running on a dual processor box, perfectly timed so that they both start to write to your device at the same time. Both would be in your driver code at the *same time*. That means that all of the setup work that A is doing to get ready to write, B is changing at (nearly) the same time. Very bad. There are a couple of ways to protect your code that are commonly used in BeOS drivers. One is spinlocks. This is conceptually:

static volatile myLock;
init_hardware() {myLock=0;}
lock() {while (myLock); myLock=1;}

I say conceptually, because this would not work in a multi-processor or pre-emptive kernel situation, so special magic is done to make this work right. In any case, I am sure that it is obvious to you that this wastes a lot of CPU cycles. For that reason, it should be used only to protect small, fast bits of code that is happening inside of an interrupt. Fortunately for us, we do not have to write spinlocks. You can create them simply with:

spinlock foo;             // Create a spinlock
cpu_staus old;            // To hold all of the cpu info
old=disable_interrupts(); // Shhh - don't interrupt
acquire_spinlock(&foo);   // Get the lock
.. do stuff...            // Get done fast
release_spinlock(&foo);   // Unlock
restore_interrupts(old);  // OK - interrupt me now.

Final note on spinlocks: consult with the BeBook on what you can and can not do inside spinlocks - your options are very limited. But, then again, you shouldn't do very much in your interrupt code anyway.

So spinlocks are all fine for interrupt code. What about for non-interrupt code (which most of the driver should be)? Semaphores are your answer. Just like in user land.

This is basically all there is to writing device drivers. The hard part is getting (and understanding) the specifications for the hardware. BeOS makes it very easy to create drivers once you know what the hardware needs.

However, just to make this even a bit easier, I made up a "template". It is completely untested, but you may find it helpful...

Source Code:

Media, marketing, and geeks by Michael Phipps 
When you tell people that you develop an operating system in your spare time, they sometimes look at you a bit ... oddly. As if you told them that you swallowed live mice or something. On the other hand, that is the original definition of "geek".

When Be first started out, they courted the geeks very strongly. I still have my Be pocket protector that they gave out at a demo I attended at Cornell. The BeBox has a GeekPort. The intent of courting geeks was to interest early adoptors (geeks) so that they would write software and build a user community. That user community still lives today, a testamony to the power of the dream.

Often, my friends and colleagues ask me questions about why we would choose BeOS as a model. My reply is "why not?" - what other model out there is better, over all? Sure, BSD is a better server. Probably always will be. PalmOS is a better hand held, QNX is better at real time, etc. But that isn't where BeOS shines. BeOS was designed to handle media. From the filesystem (optimized for high speed streaming, sacrificing some on the file creation/deletion end) to the security model (there isn't one, really), BeOS is a desktop system with a focus on multimedia. Translation Kit and Media Kit are really the "marketing" focus.

Marketing is not something that one thinks of, in general, when talking about open source software. We are supposed to be "indie" software developers - scratching an itch rather than following the dark side of the force (marketing). I do get interest from marketing people, though. People who are interested in doing something with OBOS, be it packaging it with sold systems or altering it to fit in their marketplace. Be changed their marketing plan several times in an attempt to live in a market that seemed to be out to get them. In retrospect, it seems that it would have been better to be bold and say "this is what we stand for, love it or hate it". And I think that media was/is the right direction to go.

Media is a hard master to serve. It means writing lots of Translators and Codecs. It means a lot of intensive study of code and profiling to make the code as low-latency as can be. Supporting high end video/audio cards is another important component. Media apps are hard to write and support - you can't email a 10 gig movie that is causing an app to crash. And media is not the open market that it once was. Apple is trying very hard to make the association between Macintosh and video, as they so successfully did with the Macintosh and Desktop Publishing in the 1980's.

I think, though, that OBOS can get Media right. We have the original Translator kit code (from Jon Watte). We have Marcus on the Media kit. Our kernel is simple and easy to understand and change. We don't have years of legacy like MacOS X. In fact, code wise, we are nearly legacy free. We have a team of people who are experts in the code as it exists today. Finally, we live in a different world than 5 years ago. There are only a few video cards out there, now. Sound cards are almost always either one of a few built in or very high end. USB and FireWire are promising to make a lot of the legacy support that BeOS and Windows had to account for obsolete. I can easily imagine a system with 10-15 drivers that runs most of the hardware out there. Finally, porting OBOS should not be all that hard. Be found that BeOS could be ported in a few months. I think that with the new kernel architecture, we should be able to port almost as quickly as we can understand new hardware.

Some people would argue that there is no hope for a new operating system anymore. I would disagree. I think that this is *exactly* the right time for a new operating system. Microsoft is completely lost, feature and technology wise. Microsoft is trying to be nimble and yet all inclusive. No one can do that for too long. They are running in circles, distracted and confused. The recent debacle with Passport should indicate that. Installing Windows is still too hard (compared to BeOS). XBox is a disappointment. I am sure that XBox 2 will be better in a couple of years. XP is OK, but there is no really compelling reason for people to upgrade. I certainly never will - I don't like the rules. To paraphrase a famous princess, "The more you tighten your grip, the more systems will slip through your fingers"