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):
classThreadPrimitive
{ public:ThreadPrimitive
(int32priority
=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"); } } voidGo
() {scanThread
=spawn_thread
(&ThreadPrimitive
::RunBinder
,name
?name
: "UntitledThread",priority
,this
);resume_thread
(scanThread
); } virtual voidRun
() = 0; private: static status_tRunBinder
(void *castToThis
) { // In this call we do the dirty casting work, making // the rest of the interfaces fully typed and cleanThreadPrimitive
*self
= (ThreadPrimitive
*)castToThis
;self
->Run
(); returnB_OK
; } protected: thread_idscanThread
; int32priority
; 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.
classFindAFileThread
: privateThreadPrimitive
{ public: static voidLaunch
(constBEntry
*startDir
, const char *lookForName
, int32priority
=B_LOW_PRIORITY
, const char *name
= 0) {FindAFileThread
*thread = newFindAFileThread
(startDir
,lookForName
,priority
,name
); if (thread
->Go
() !=B_OK
) // failed to launch, clean up deletethread
; } private:FindAFileThread
(constBEntry
*startDir
, const char *lookForName
, int32priority
, const char *name
) :ThreadPrimitive
(priority
,name
),startDir
(*startDir
),lookForName
(strdup
(lookForName
)) {} virtual~FindAFileThread
() {free
(lookForName
); } virtual voidRun
() { charbuffer
[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 deletethis
; // 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.
classFunctionObject
{ public: virtual voidoperator()
() = 0; virtual~FunctionObject
() {} };
This is the FunctionObject
base class that defines the
interface our thread will understand.
classFireAndForgetThread
: privateThreadPrimitive
{ public: static voidLaunch
(FunctionObject
*functor
, int32priority
=B_LOW_PRIORITY
, const char *name
= 0) {FireAndForgetThread
*thread = newFireAndForgetThread
(functor
,priority
,name
); if (thread->Go
() !=B_OK
) // failed to launch, clean up delete thread; } private:FireAndForgetThread
(FunctionObject
*functor
, int32priority
, const char *name
) :ThreadPrimitive
(priority
,name
),functor
(functor
) // take over the function // object ownership {} virtual~FireAndForgetThread
() { deletefunctor
; } virtual voidRun
() { (*functor
)(); // invoke the function object to get threads work done deletethis
; // 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 <classParam1
, classParam2
> classTwoParamFunctionObject
: publicFunctionObject
{ public:TwoParamFunctionObject
(void (*callThis
)(Param1
,Param2
),Param1
param1
,Param2
param2
) :function
(callThis
),param1
(param1
),param2
(param2
) { } virtual voidoperator()
() { (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<classP
> classParameterBinder
{ public:ParameterBinder
(P
p
) :p
(p
) {}P
Pass
() { returnp
; } private:P
p
; };
ParameterBinder
specialization for
const BEntry
*:
template<> classParameterBinder
<constBEntry
*> { public:ParameterBinder
(constBEntry
*p
) :p
(*p
) {} constBEntry
*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 voidFindAFile
(constBEntry
*startDir
, const char *name
) { charbuffer
[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
(newTwoParamFunctionObject
<constBEntry
*, 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.
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
= newBColorControl
(BPoint
(0, 0),B_CELLS_32x8
, 8, "color control", newBMessage
('ccnt'),false
); ...
On receiving the message, in our example, the window unpacks the value into an rgb_color and sets the desktop color.
voidTWindow
::MessageReceived
(BMessage
*m
) { int32value
; rgb_colordcColor
; 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:
voidTCustomColorControl
::SetValue
(int32v
) { // always remember to actually set the //control's value when overridingBColorControl
::SetValue
(v
); // convert the value to an rgb_color rgb_colordcColor
=ValueAsColor
(); // set the desktop colorBScreen
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 colorBScreen
b
(B_MAIN_SCREEN_ID
); rgb_colordcColor
=b
.DesktopColor
();BRect
objectFrame
(10, 5,Bounds
().Width
() - 10, 15); // create a simple BView as a 'color swatch'BView
*colorSwatch
= newBView
(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 valueobjectFrame
.top
= 20;objectFrame
.bottom
= 55;TSlider
*redSlider
= newTSlider
(objectFrame
, "Red",colorSwatch
);AddChild
(fRedSlider
);objectFrame
.OffsetBy
(0, 40);TSlider
*greenSlider
= newTSlider
(objectFrame
, "Green",colorSwatch
);AddChild
(fGreenSlider
);objectFrame
.OffsetBy
(0, 40);TSlider
*blueSlider
= newTSlider
(objectFrame
, "Blue",colorSwatch
);AddChild
(fBlueSlider
); // set the individual values for each sliderredSlider
->SetValue
(dcColor
.red
);greenSlider
->SetValue
(dcColor
.green
);blueSlider
->SetValue
(dcColor
.blue
); ...
Where TSlider
is as follows:
classTSlider
: publicBSlider
{ public:TSlider
(BRect
frame
, const char*name
,BView
*colorSwatch
); voidSetValue
(int32value
); 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:
voidTSlider
::SetValue
(int32v
) { // tell the slider its new valueBSlider
::SetValue
(v
); // get the current color of the view rgb_colorviewColor
=fColorSwatch
->ViewColor
(); // each slider will represent its individual color // in its fill color rgb_colorfillColor
= {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 colorUseFillColor
(true
, &fillColor
); // set the view colorfColorSwatch
->SetViewColor
(viewColor
);fColorSwatch
->Invalidate
(); // set the Desktop colorBScreen
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.
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:
#include <Menu.h> #include <Message.h> #include <MenuItem.h> #include <TranslationKit.h> enum {B_TRANSLATION_MENU
= 'BTMN' }; status_tAddTranslationItems
(BMenu
*intoMenu
, uint32from_type
, constBMessage
*model
, /* default B_TRANSLATION_MENU */ const char *translator_id_name
, /* default "be:translator" */ const char *translator_type_name
, /* default "be:type" */BTranslatorRoster
*use
) { if (use
==NULL
) { use =BTranslatorRoster
::Default
(); } if (translator_id_name
==NULL
) {translator_id_name
= "be:translator"; } if (translator_type_name
==NULL
) {translator_type_name
= "be:type"; } translator_id *ids
=NULL
; int32count
= 0; status_terr
=use
->GetAllTranslators
(&ids
, &count
); if (err
<B_OK
) returnerr
; for (inttix
=0;tix
<count
;tix
++) { consttranslation_format *formats
=NULL
; int32num_formats
= 0; boolok
=false
;err
=use
->GetInputFormats
(ids
[tix
], &formats
, &num_formats
); if (err
==B_OK
) for (intiix
=0;iix
<num_formats
;iix
++) { if (formats
[iix
].type
==from_type
) {ok
=true
; break; } } if (!ok
) continue;err
=use
->GetOutputFormats
(ids
[tix
], &formats
, &num_formats
); if (err
==B_OK
) for (intoix
=0;oix
<num_formats
;oix
++) { if (formats
[oix
].type
!=from_type
) {BMessage
*itemmsg
; if (model
) {itemmsg
= newBMessage
(*model
); } else {itemmsg
= newBMessage
(B_TRANSLATION_MENU
); }itemmsg
->AddInt32
(translator_id_name
,ids
[tix
]);itemmsg
->AddInt32
(translator_type_name
,formats
[oix
].type
);intoMenu
->AddItem
(newBMenuItem
(formats
[oix
].name
,itemmsg
)); } } } delete[]ids
; returnB_OK
; }
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
:
status_tMyWindow
::DoSaveAs
(BFile
*outputFile
,BMessage
*save_as
) { int32translator
; uint32type
; status_terr
;err
=save_as
->FindInt32
("be:translator", &translator
); if (err
<B_OK
) returnerr
;err
=save_as
->FindInt32
("be:type", (int32 *)&type
); if (err
<B_OK
) return err;BBitmapStream
input
(fBitmap
);err
=BTranslatorRoster
::Default
()->Translate
(translator
, &input
,NULL
,outputFile
,type
); if (err
==B_OK
)err
=SetFileType
(outputFile
,translator
,type
); returnerr
; }
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:
status_tMyWindow
::SetFileType
(BFile
*file
, int32translator
, uint32type
) { translation_format *formats
; int32 count; status_terr
=BTranslatorRoster
::GetOutputFormats
(translator
, &formats
, &count
); if (err
<B_OK
) return err; const char *mime
=NULL
; for (intix
=0;ix
<count
;ix
++) { if (formats
[ix
].type
==type
) {mime
=formats
[ix
].MIME
; break; } } if (mime
==NULL
) { /* this should not happen, but */ /* being defensive might be prudent */ returnB_ERROR
; } /* use BNodeInfo to set the file type */BNodeInfo
ninfo
(file
); returnninfo
.SetType
(mime
); }
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.