Table of Contents
BE ENGINEERING INSIGHTS: Fun with Threads, Part 1 By Pavel Cisler Threads make BeOS responsive. Threads help programmers achieve good perceived performance and snappiness in their application even on slow hardware. Threads also make it really easy to add subtle, hard to track down bugs that only happen on the user's system, never on the programmer's machine. This article is for C++ programmers. It shows a few typical cases of thread use, pointing out common pitfalls and isolating the low-level thread calls into prefabricated C++ classes that make it easier to avoid some common mistakes. The article also contains some mandatory advanced C++ trickery that we all live for. Let's start with the simplest kind of thread -- the fire and forget thread. This thread is self-contained. It has a copy of the state it needs for its work, so it doesn't need to obtain any information from other threads, and therefore needs no complex synchronization. First we need a simple thread base class that we'll subclass in all our examples (we include all the code inside the class definition to save valuable article real estate; in the real world we would split it up): class ThreadPrimitive { public: ThreadPrimitive(int32 priority = B_LOW_PRIORITY, const char *name = 0) : scanThread(-1), priority(priority), name(name) {} virtual ~ThreadPrimitive() { if (scanThread > 0) { kill_thread(scanThread); ASSERT(!"should not be here"); } } void Go() { scanThread = spawn_thread(&ThreadPrimitive::RunBinder, name ? name : "UntitledThread", priority, this); resume_thread(scanThread); } virtual void Run() = 0; private: static status_t RunBinder(void *castToThis) { // In this call we do the dirty casting work, making // the rest of the interfaces fully typed and clean ThreadPrimitive *self = (ThreadPrimitive *)castToThis; self->Run(); return B_OK; } protected: thread_id scanThread; int32 priority; private: const char *name; // only valid in the constructor and in // the Go call }; ThreadPrimitive is an abstract base class -- you have to subclass it to make it instantiable. Specifically, you must implement the pure virtual Run() function. Your implementation should incorporate the code that does the actual work for which the thread was spawned. In other words, each ThreadPrimitive subclass performs a specific task. In addition, we expect all subclasses to privatize the constructor and provide a static "perform" function that constructs an object and then calls Go(). Note that the thread has two sides. One side (the constructor and the Go() call) is accessible from the caller side, and one is accessed once the thread is running. These two sides are in two totally different contexts and we need to be aware of that. For instance, the name member variable is not a copy, but a pointer to the string passed in the constructor. It may not be a pointer to a valid name by the time RunBinder() or Run() gets to run -- it may go out of scope on the spawner side, get deleted, etc. We'll augment this by making it private so that a subclass can't use it in Run() by accident. Let's look at a PrimitiveThread subclass. The FindAFileThread does a recursive search for a file starting at a specified directory, and then opens the file with its preferred app. All the object needs is the name of the file and the directory it should start at. In order to be self contained, the subclass needs its own copies of these two pieces of data. class FindAFileThread: private ThreadPrimitive { public: static void Launch(const BEntry *startDir, const char *lookForName, int32 priority = B_LOW_PRIORITY, const char *name = 0) { FindAFileThread *thread = new FindAFileThread(startDir, lookForName, priority, name); if (thread->Go() != B_OK) // failed to launch, clean up delete thread; } private: FindAFileThread(const BEntry *startDir, const char *lookForName, int32 priority, const char *name) : ThreadPrimitive(priority, name), startDir(*startDir), lookForName(strdup(lookForName)) {} virtual ~FindAFileThread() { free (lookForName); } virtual void Run() { char buffer[B_FILE_NAME_LENGTH]; startDir.GetName(buffer); printf("looking for %s in directory %s\n", lookForName, buffer); // ... look for <lookForName> recursively in startDir // left out as an exercise for the reader delete this; // clean up after ourselves when we are done } // copy of the state our thread needs to Run() BEntry startDir; char *lookForName; }; We said the thread needs to be self contained. That also means it needs to clean up after itself once it's done running. You can see that it deletes itself at the end of the Run() call. To use the object, we call the static Launch() function: FindAFileThread::Launch(&someDirRef, "APM.h"); Remember that the constructor must be private. This is to only allow a heap-allocated instance of the thread. A stack-based instance wouldn't work for a couple of reasons. First, the thread deletes itself when it's done. Second, if it was declared as a local instance and the spawning function quit while the object was still running, the local instance would be deleted, killing the thread with it. The constructor also makes copies of the state the thread uses -- we need a destructor to delete the lookForName copy obtained by strdup(). (Note that in R4 there will be a nice new BString class that we could have used here, allowing us to avoid the explicit destructor). As you can see, to implement our FindAFileThread we wrote a fairly simple subclass of ThreadPrimitive. But we still needed to subclass and there was still quite a bit of stuff to remember (and mess up), and this is, after all, a very simple example. If you're adventurous (or are writing a big app with a lot of threading), you could use the following thread class, which utilizes function objects to avoid having to subclass ThreadPrimitive each time. As you may know, function objects are classes that have an operator() -- they know how to call themselves. They usually pack a function pointer and necessary parameters to be used as arguments during a function call. They're actually very useful in threading code. When you're using a thread, you still want to call some code, not right there but asynchronously, in a different context. A function object is about packaging up all the information you need to perform the call later. Function objects are a part of STL; we'll use our own here to serve the purpose of the interface we want to use. class FunctionObject { public: virtual void operator()() = 0; virtual ~FunctionObject() {} }; This is the FunctionObject base class that defines the interface our thread will understand. class FireAndForgetThread: private ThreadPrimitive { public: static void Launch(FunctionObject *functor, int32 priority = B_LOW_PRIORITY, const char *name = 0) { FireAndForgetThread *thread = new FireAndForgetThread(functor, priority, name); if (thread->Go() != B_OK) // failed to launch, clean up delete thread; } private: FireAndForgetThread(FunctionObject *functor, int32 priority, const char *name) : ThreadPrimitive(priority, name), functor(functor) // take over the function // object ownership {} virtual ~FireAndForgetThread() { delete functor; } virtual void Run() { (*functor)(); // invoke the function object to get threads work done delete this; // clean up after ourselves when we are done } FunctionObject *functor; // functor owned by the thread }; This time there are no task-specific arguments in the Launch() call and in the constructor. The directory and filename parameters that we passed explicitly in the previous example are now packaged up in a function object, along with a target function that's called when the thread runs. The bare FunctionObject doesn't do much. In practice you'll use one of the prefabricated function objects that you have for this purpose, for instance: template <class Param1, class Param2> class TwoParamFunctionObject : public FunctionObject { public: TwoParamFunctionObject(void (*callThis)(Param1, Param2), Param1 param1, Param2 param2) : function(callThis), param1(param1), param2(param2) { } virtual void operator()() { (function)(param1.Pass(), param2.Pass()); } private: void (*function)(Param1, Param2); ParameterBinder<Param1> param1; ParameterBinder<Param2> param2; }; The function object above works with static target functions that take two parameters. ParameterBinder is a little bit of magic that uses template specialization to accommodate different function object parameters differently. Remember, we need to make a copy of everything. For example, if we pass a const BEntry * to our target searching function, we still need to keep a copy of the entire BEntry instance in the function object, since the original BEntry might be long gone when our thread does its job. Passing a BEntry as a parameter would be inefficient; it would cause multiple unnecessary copy operations. The BEntry specialization of ParameterBinder ensures that const BEntry * can be passed to the function object constructor, a copy of the BEntry is saved, and a const BEntry * is passed to the target function, which is exactly what we need. Default ParameterBinder used for scalars: template<class P> class ParameterBinder { public: ParameterBinder(P p) : p(p) {} P Pass() { return p; } private: P p; }; ParameterBinder specialization for const BEntry *: template<> class ParameterBinder<const BEntry *> { public: ParameterBinder(const BEntry * p) : p(*p) {} const BEntry *Pass() { return &p; } private: BEntry p; }; In a real application you'd have a whole army of function object templates and would just pick the one for the right number of function arguments. You wouldn't need to worry about picking the right ParameterBinder once you had specializations for the different struct types you might be using. The function object works on different types of parameters and does full type checking, making sure the types of arguments we pass to it and the types required by the target function are compatible. If you are interested in knowing more about function objects (or are a function object junkie, like Hiroshi, and can't get enough of them), I recommend that you read, for instance, the excellent "Ruminations about C++," by Andrew Koenig and Barbara Moo. Here's how we would use our new FireAndForgetThread: static void FindAFile(const BEntry *startDir, const char *name) { char buffer[B_FILE_NAME_LENGTH]; startDir->GetName(buffer); printf("looking for %s in directory %s\n", name, buffer); // do some work here } ... BEntry entry("/boot/home"); FireAndForgetThread::Launch(new TwoParamFunctionObject<const BEntry *, const char *>(&FindAFile, &entry, "APM.h")); ... The Launch call packages up the function address and the parameters into a function object and sends it off. Note that we didn't need to tweak the thread class itself; all we had to do was supply the FindAFile function itself. There's practically no room left for thread setup code that we could make a mistake in. Oh, and by the way, if we tried really hard to screw up and pass, say an entry_ref * in place of entry, the compiler would catch it because FindAFile takes a const BEntry *. We're reaching the Holy Grail of programming here -- the compiler will not let us make any mistakes. This concludes the first part of this article, in the next part we'll examine more types of threads and their use.
BE ENGINEERING INSIGHTS: BeOS and the Simple Life By Robert Chinn Over the weekend I had a chance to tour one of SGI's buildings. It's an impressive site. A huge building with slanted purple walls, a cafeteria, auditoriums, sand volley ball court, nicely landscaped grounds, $1000 chairs, and even clean carpets. Coming back to Be I realized that while other companies' campuses are distinguished and nice to look at, I really enjoy the simplicity of what we have at Be. In fact, I've made that the subject of this article -- doing things in a simple and direct way; no incredible tricks, no obscure C++, just getting the job done. Most of our developers have probably already addressed the issue of transitioning from writing code in a straight C or a non-message based system to writing applications for the BeOS. On many systems you have the controls just do the work it needs to do. In the BeOS most of the interactions are message based: a control is pressed and it sends a message that you catch somewhere else and do something. Sometimes this is the best way to accomplish the task at hand. Other times you just want the control to do something immediately or interact directly with another part of your application directly. Accomplishing this "liveness" with the BeOS is simple and easy, but how and where should it be done? For any object based on BControl, the place to do this is in SetValue. One example is when you're using a control such as a BColorControl. For this example it will change the Desktop color. In BColorControl's most basic state, when you click on a color tile or change an individual RGB value a message is sent to its parent window: ... BColorControl* indirectColorControl = new BColorControl( BPoint(0, 0), B_CELLS_32x8, 8, "color control", new BMessage('ccnt'), false); ... On receiving the message, in our example, the window unpacks the value into an rgb_color and sets the desktop color. void TWindow::MessageReceived(BMessage* m) { int32 value; rgb_color dcColor; switch (m->what) { case 'ccnt': // message sent from BColorControl { m->FindInt32("be:value", &value); dcColor.red = (value >> 24); dcColor.green = (value >> 16); dcColor.blue = (value >> 8); dcColor.alpha = 255; BScreen b(B_MAIN_SCREEN_ID); b.SetDesktopColor(dcColor, true); } break; } } This method works, but is not very "live." Changes to the Desktop color only occur when the mouse button is released, so dragging around on the color tiles doesn't actively change the Desktop color. So, how can you make it "live"? Override the SetValue method of a custom BColorControl, get the rgb value for the current selection and set it directly: void TCustomColorControl::SetValue(int32 v) { // always remember to actually set the //control's value when overriding BColorControl::SetValue(v); // convert the value to an rgb_color rgb_color dcColor = ValueAsColor(); // set the desktop color BScreen b(B_MAIN_SCREEN_ID); b.SetDesktopColor(dcColor, true); } Now, when the user drags around on the color tiles, as each new tile is hit, the Desktop color changes immediately. You can use this same technique with any BControl for a similar "liveness." In the same way, letting a control "know" about another object enables it to communicate directly and maintain this "live" feel. Here we have a view and a set of three sliders that modify the individual rgb components for the view's view color and the Desktop color: ... // get the current Desktop color BScreen b(B_MAIN_SCREEN_ID); rgb_color dcColor = b.DesktopColor(); BRect objectFrame(10, 5, Bounds().Width() - 10, 15); // create a simple BView as a 'color swatch' BView* colorSwatch = new BView(objectFrame, "color swatch", B_FOLLOW_NONE, B_WILL_DRAW); colorSwatch->SetViewColor(dcColor); AddChild(colorSwatch); // add 3 custom BSliders, one for the red, green and blue // components of an rgb value objectFrame.top = 20; objectFrame.bottom = 55; TSlider* redSlider = new TSlider(objectFrame, "Red", colorSwatch); AddChild(fRedSlider); objectFrame.OffsetBy(0, 40); TSlider* greenSlider = new TSlider(objectFrame, "Green", colorSwatch); AddChild(fGreenSlider); objectFrame.OffsetBy(0, 40); TSlider* blueSlider = new TSlider(objectFrame, "Blue", colorSwatch); AddChild(fBlueSlider); // set the individual values for each slider redSlider->SetValue(dcColor.red); greenSlider->SetValue(dcColor.green); blueSlider->SetValue(dcColor.blue); ... Where TSlider is as follows: class TSlider : public BSlider { public: TSlider(BRect frame, const char* name, BView* colorSwatch); void SetValue(int32 value); private: BView* fColorSwatch; }; TSlider::TSlider(BRect frame, const char* name, BView* colorSwatch) : BSlider(frame, name, name, NULL, 0, 255, B_TRIANGLE_THUMB), fColorSwatch(colorSwatch) { } For our custom slider, the individual rgb components are modified in SetValue, based on which slider was changed. The new rgb value is then used to set the view color for the color swatch and the Desktop color. The fill color for the slider is also set to the individual rgb component that the slider represents: void TSlider::SetValue(int32 v) { // tell the slider its new value BSlider::SetValue(v); // get the current color of the view rgb_color viewColor = fColorSwatch->ViewColor(); // each slider will represent its individual color // in its fill color rgb_color fillColor = {0,0,0,255}; // determine which slider has been modified // get its new value if (strcmp("Red", Name()) == 0) { fillColor.red = Value(); viewColor.red = Value(); } else if (strcmp("Green", Name()) == 0) { fillColor.green = Value(); viewColor.green = Value(); } else if (strcmp("Blue", Name()) == 0) { fillColor.blue = Value(); viewColor.blue = Value(); } // set the fill color UseFillColor(true, &fillColor); // set the view color fColorSwatch->SetViewColor(viewColor); fColorSwatch->Invalidate(); // set the Desktop color BScreen b(B_MAIN_SCREEN_ID); b.SetDesktopColor(viewColor, true); } Since all the processing is done in SetValue, all the changes are "live." Once again, if messages had been used, the color would change only when the mouse button was released. The above techniques are quite simple -- and that is the point. By simplifying the process with a little directness we make the interaction of the parts of the application a bit more responsive. With this responsiveness comes the "live" feel that most users really appreciate.
DEVELOPERS' WORKSHOP: Translation Kit, Again! By Jon Wattehplus@be.com "Developers' Workshop" is a weekly feature that provides
answers to our developers' questions, or topic requests.
To submit a question, visit
http://www.be.com/developers/suggestion_box.html.
When I talked about the Translation Kit at the last BeDC, I
promised to make code available on our web page that showed
how to create a Save As menu using the Translation Kit to
save in a format of the user's choice. It's about time I
delivered on that promise, so here it is.
In BeOS Release 4, the BTranslationUtils class will have
learned new tricks. One of them is to populate an existing
BMenu with menu items for available translations, to make
creating that Save As menu a no-brainer. However, since
anyone who doesn't work here has to live with R3.2 for some
time to come, it can't hurt to put the same code in your
app, at least until you get around to updating it for R4.
Thus, I give you:
Usage is easy. Create the BMenu that you want to hang off
your "Save As" menu item. Call AddTranslationItems() with,
at a minimum, that menu and the "class" of formats you deal
with. If you deal with bitmaps, this would be
B_TRANSLATOR_BITMAP. The last four arguments can be NULL
unless you want to customize the operation of the function,
which falls outside of the scope of this article (but you
can read the code to figure out how).
The function figures out which translators can translate
from that "class" format to some interesting format, and add
the name of each of those formats as an item to a menu. The
message for that item will have a value for the translator
(by ID) and the format (by code) requested.
You then do this in your Save As handler, assuming you
already know how to run a BFilePanel to tell you where to
create an output BFile, and that you're saving a member
bitmap named fBitmap:
As you can see, we're setting the file type by calling
another function that we also need to implement. This time
to get the actual MIME type corresponding to the internal
type ID for the translator we're using:
Well, this should fulfill my promise, and I hope it will
also lead to a few more applications that can use the
Translation Kit to its fullest. We're doing even more
interesting stuff with the Translation Kit for R4, including
a control panel to let the user configure default settings
for installed translators, and an API for your app to
retrieve those settings so you can pass them as ioExtension
when you call the Translation Kit.
Also in R4 we're defining a standard format for styled text
import/export, and writing some more standard Translators to
ship with the system. Ah, yes, R4 will be exciting indeed!
The question has been asked many times, of me individually
and collectively of all of us Be-ans. Often our questioners
worry about our well-being. They remind us of the high
degree of risk involved in writing "yet another operating
system" while Windows reigns supreme or, earlier, while
NeXTStep appeared headed in the same direction as OS/2.
If Steve Jobs can't establish a platform, if IBM can't shake
Microsoft's hold on the office market, you must be crazy to
think you can gain critical mass for yourself and your
developers. And by the way, your investors are also crazy if
they think they'll ever get back a red cent of their money.
It's this kind of feedback that keeps us humble and ever
aware that we have much to do and much to prove.
Fortunately, Steve Jobs proved that NeXT was going
somewhere. As for us, we've been able to show that the BeOS
is not on the same futile path as OS/2 in trying to offer
"better DOS than DOS, better Windows than Windows." Rather,
in a context that considers the possibility of more than one
OS on your hard disk, we are positioning ourselves as a
specialized A/V platform coexisting with the general-purpose
Windows, a low-risk complement instead of a replacement, as
OS/2 attempted to do.
Some still question our sanity about this notion of peaceful
OS coexistence, but at least the doubt has shifted. Now it's
when we say positive things about Windows and Microsoft that
our soundness of mind comes into dispute again. Our
concerned correspondents get a little frustrated with us, or
with me personally. They ask, "How can you say those things
about a product and a company that...," followed by a long,
colorful list of sins.
Perhaps this is a good time to explain ourselves. In the
first place, we understand and respect the range of opinions
and emotions aroused by Microsoft and its products. The
company has ascended to preeminence in the industry. Many
accuse it of transgressions on its rise to sovereignty. They
see flaws in its products. They perceive it as a
monopolistic power, and fear what they see as a natural
tendency to abuse its position and to crush competition.
Consider the PC market. Today, when you order a PC, it comes
with Windows, even when there is an IBM logo on it. In
office productivity applications, Microsoft Office is the
standard. Why should our little company expend its energy
fighting that?
As one of my favorite existential philosophers, Ross Perot,
once said, "I don't have a dog in that fight." Well, we
don't have a dog in the fight against Microsoft as the
general-purpose OS. We're not going to waste our energy on
that front. We have too much work to do to improve our
product, to create better opportunities for BeOS developers,
to service our OEM and distribution partners and,
ultimately, to fulfill our shareholders' expectations.
Now put yourself in the position of someone who's just
bought a PC. It comes with Windows 98 and, most likely, with
some OEM version of Office. We have some choices. If we
launch into an anti-Microsoft, anti-Windows diatribe, we run
the risk of our potential BeOS user hearing that he or she
has just bought a bad product from a bad company.
In our view, this is a bit like meeting someone who's just
bought a car and ranting, "You bought that lemon from those
felons?" If you're trying to sell this person an after
market stereo and on-board DVD player, what do you think
your chances are? Since we are, in effect, selling an after
market A/V system, it only makes sense to position it as a
complement to Windows, rather than disparage the system you
just bought. That way, we don't get stuck arguing the pros
and cons of Windows. Instead, we move quickly to the merits
of our product.
All right then, our questioners persist, you're not insane,
but you are hypocritical. You can't tell me you don't have
negative feelings towards Microsoft and its products. You're
just hiding them because you've calculated it would be bad
for business.
Yes, it would be bad for business. Yes, my Jesuit confessor
advises me it is permissible to harbor ambivalent feelings
towards such a powerful entity. No, my confessor continues,
focusing on the positive is not the mortal sin of
prevarication, but merely the venial one of californication.
For my penance I must say three Hail Marys and two Our
Fathers.
1997 Be Newsletters | 1995 & 1996 Be Newsletters Copyright ©1998 Be, Inc. Be is a registered trademark, and BeOS, BeBox, BeWare, GeekPort, the Be logo and the BeOS logo are trademarks of Be, Inc. All other trademarks mentioned are the property of their respective owners. Comments about this site? Please write us at webmaster@be.com. |