Ir al contenido principal

Ralsina.Me — El sitio web de Roberto Alsina

Cooperative Qt apps using GNU pth

In­tro­duc­tion

The pur­pose of this doc­u­ment is to ex­plain the pros and cons of im­ple­ment­ing a mul­ti­thread­ed KDE (or Qt) ap­pli­ca­tion, as well as ex­plain­ing briefly the way in which that can be done.

Why mul­ti­thread­ed apps are good

Mul­ti­thread­ed apps are good in that they al­low you to have a non-block­ing GUI with­out do­ing much trick­ery. For in­stance, when you want to per­form a lengthy cal­cu­la­tion in a sin­glethread­ed Qt ap­pli­ca­tion, you need to put some pro­ces­sEv­ents() calls in be­tween lengthy op­er­a­tions like this:

void doLongThing()
{
    bool a;
    for (;a;)
    {
        a=doSomething();
        qApp->processEvents();
    }
}

This has a se­ri­ous down­sid­e: while you will have a UI that re­draws it­self cor­rect­ly (as long as do­Some­thing() does­n't take long), you STILL need to block (or rather, dis­able) most of your UI.

If you don't dis­able part of the UI, qAp­p->pro­ces­sEv­ents() will get a but­ton press and send your app to do some­thing else en­tire­ly mak­ing your life dif­fi­cult, or you have to write your app to be able to jug­gle sep­a­rate calls to do­LongTh­ing() at the same time, for ex­am­ple.

There is an al­ter­na­tive, which is mov­ing lengthy op­er­a­tions in­to sep­a­rate pro­cess­es, and just have your app wait for the sep­a­rate pro­cess­es to end. This is the ap­proach used in KDE for KIO. This is good! This is hard to do well, just ask the guys who de­signed KIO ;-)

On a mul­ti­thread­ed ap­p, you would not do a qAp­p->pro­ces­sEv­ents() and sim­ply loop at will. You won't dis­able the UI be­cause there is no a pri­ori rea­son why you can't do sev­er­al do­LongTh­ing() at the same time, just make sure each one runs in its own thread.

Why mul­ti­thread­ed apps are bad

Mul­ti­thread­ed apps are bad in sev­er­al ways. For one thing, there is no way to im­ple­ment a re­al­ly mul­ti­thread­ed app that us­es Qt from sev­er­al threads at on­ce, be­cause Qt is not reen­tran­t.

What's be­ing reen­tran­t? Let me quote Ralf S. En­gelschall that knows much more than I do ;-)

reen­trant thread­-safe and asyn­chronous-safe func­tion­s:

A reen­trant func­tion is one that be­haves cor­rect­ly if it is called si­mul­ta­ne­ous­ly by sev­er­al threads and then al­so ex­e­cutes si­mul­ta­ne­ous­ly. Func­tions that ac­cess glob­al state, such as mem­o­ry or files, of course, need to be care­ful­ly de­signed in or­der to be reen­tran­t. Two tra­di­tion­al ap­proach­es to solve these prob­lems are caller-­sup­plied states and thread­-spe­cif­ic da­ta.

Thread­-safe­ty is the avoid­ance of da­ta races, i.e., sit­u­a­tions in which da­ta is set to ei­ther cor­rect or in­cor­rect val­ue de­pend­ing up­on the (un­pre­dictable) or­der in which mul­ti­ple threads ac­cess and mod­i­fy the da­ta. So a func­tion is thread­-safe when it still be­haves se­man­ti­cal­ly cor­rect when called si­mul­ta­ne­ous­ly by sev­er­al threads (it is not re­quired that the func­tions al­so ex­e­cute si­mul­ta­ne­ous­ly). The tra­di­tion­al ap­proach to achieve thread­-safe­ty is to wrap a func­tion body with an in­ter­nal mu­tu­al ex­clu­sion lock (a­ka mu­tex). As you should rec­og­nize, reen­trant is a stronger at­tribute than thread- safe, be­cause it is hard­er to achieve and re­sults es­pe­cial­ly in no run-­time con­tention be­tween thread­s. So, a reen­trant func­tion is al­ways thread­-safe, but not vicev­er­sa.

So, you CAN im­ple­ment a mul­ti­thread­ed Qt ap­pli­ca­tion, as long as you keep all your Qt (and KDE) re­lat­ed func­tion calls in a sin­gle thread, and move to oth­er threads things like nu­mer­i­cal cal­cu­la­tion­s.

Sad­ly, this is a re­al pain in a KDE ap­pli­ca­tion, be­cause we do LOTS of things us­ing Qt and KDE lib­s. For in­stance, I use kfileio (part of ked­it, I be­lieve) to load text files in­to mem­o­ry, be­cause it shows nice warn­ing box­es when some­thing goes wrong. I can't do that from a sep­a­rate thread. That's not good.

There is al­so a now mi­nor prob­lem: re­al threads (Pthread­s) are not all that por­ta­ble. How­ev­er these days most unix-­like sys­tems sup­port them in their lat­est ver­sion­s, so it's not as bad as it used to be.

GNU Pth, and why its good

Since re­al mul­ti­thread­ed apps are not all that pos­si­ble, and good non-­mul­ti­thread­ed apps are not too easy to write in some sit­u­a­tion­s, I looked around, and found GNU Pth, the GNU Por­ta­ble threads li­brary.

GNU Pth does co­op­er­a­tive userspace mul­ti­thread­ing. What does that mean?

Userspace means that it does­n't re­quire ker­nel sup­port.

That helps with the porta­bil­i­ty prob­lem. GNU Pth works on sys­tems like libc5 lin­ux where pthreads don't ex­ist (or suck­).

Co­op­er­a­tive mul­ti­thread­ing means that threads are not sus­pend­ed to al­low for the ex­e­cu­tion of an­oth­er thread un­til they yield con­trol, or sus­pend im­plic­it­ly by call­ing some func­tions that, well, sus­pend them while wait­ing for some event (like I/O).

To those here who are now think­ing of Win­dows 3.1 and Mac­in­tosh's co­op­er­a­tive mul­ti­task­ing, don't be too afraid, you on­ly need to co­op­er­ate with your­self, so it's not that crap­py :-)

So, how would our code look us­ing GNU Pth?

void doLongThing()
{
    bool a;
    for (;a;)
    {
        a=doSomething();
        pth_yield(NULL);
    }
}

Not all that dif­fer­ent :-)

How­ev­er, there is a big dif­fer­ence: run­ning sev­er­al do­LongTh­ing()s at the same time is rather nat­u­ral, as long as you make sure each runs in a sep­a­rate thread, and you don't re­al­ly need to dis­able the UI!

Al­so, since us­ing co­op­er­a­tive MT means that your code is not pre­empt­ed, you CAN call Qt and KDELibs from the sec­ondary thread­s, with some very mi­nor caveats (which we will see).

How to make a mul­ti­thread­ed QAp­pli­ca­tion

Here is the sim­ple class I use to make a mul­ti­thread­ed QApp (mod­i­fy in the ob­vi­ous way to make it a KAp­p):

class QMTApplication: public QApplication
{
Q_OBJECT
public:
        QMTApplication (int argc, char **argv);
        ~QMTApplication ();
        int exec();
        void quit();
        bool exitguithread;
}

QMTApplication::QMTApplication (int argc, char **argc): QApplication (argc,argv)
{
        exitguithread=false;
        //Initialize the Pth library
        pth_init();
}

QMTApplication::~QMTApplication ()
{
        //Kill the Pth library
        pth_kill();
}

int QMTApplication::exec()
{
        for (;!exitguithread;)
        {
                //Process any events
                processEvents();
                //wait 1/1000 of a second, so we don't hog
                //the CPU and let other threads execute.
                pth_nap(pth_time(0,1000));
        }
}

void QMTApplication::quit()
{
        exitguithread=true;
}

What does it do? It sim­ply ini­tial­izes and clos­es the Pth li­brary in the right mo­ments, and yields pro­cess­ing to the oth­er threads 1000 times per sec­ond (more or less).

Even if ex­ec() seems to be a CPU hog, it is not. CPU us­age is not mea­sur­able on a idle ap­pli­ca­tion.

Now, while this is tech­ni­cal­ly a mul­ti­thread­ed ap­pli­ca­tion, it's a one-thread MT ap­p, which is not too in­ter­est­ing.

What you need to do to take ad­van­tage of Pth, is make your ap­pli­ca­tion spawn new threads when­ev­er you start do­ing some­thing that takes long.

So, sup­pose a but­ton has a slot con­nect­ed to its clicked() sig­nal, and that slot is called slot­Do­LongTh­ing().

Here is how that slot will look (but it can be done pret­tier, I bet).

//Prototype of the real function that does the work
void *slotDoLongThingThread(void *arg);

void MyButton::slotDoLongThing(void)
{
        //This defines some aspects of the child thread, look at the Pth
        //docs.
        pth_attr_t attr = pth_attr_new();
        pth_attr_set(attr,PTH_ATTR_CANCEL_STATE,
                PTH_CANCEL_ASYNCHRONOUS);
        pth_attr_set(attr,PTH_ATTR_JOINABLE,FALSE);
        pth_attr_set(attr, PTH_ATTR_STACK_SIZE,
                128*1024);
        pth_spawn(attr,slotDoLongThing,this);
}

//The real function that does the work.
void *slotDoLongThingThread(void *arg)
{
        MyButton *myb=(MyButton *)arg;
        bool a;
        for (;a;)
        {
                a=doSomething();
                myb->doThingWithButton();
                pth_yield(NULL);
        }
        return NULL;
}

So, when­ev­er slot­Do­LongTh­ing is called, it will spawn a new thread to do­Some­thing() a long while, and your UI will not block (be­cause you call pth_yield­(), or at least, it will block as lit­tle as it would if you called pro­ces­sEv­ents(), and you don't need to dis­able the UI be­cause mul­ti­ple do­LongTh­ings can be done with­out any fur­ther pro­gram­ming trick­s.

As a rule, I nev­er call pro­ces­sEv­ents() from any of the spawned threads be­cause it does­n't make sense: you get in the same mess as if you were not us­ing Pth! I just pth_yield­(NUL­L), and even­tu­al­ly it will get to a pro­ces­sEv­ents() in the main thread.

What us­ing Pth won't get you

You won't take ad­van­tage of mul­ti­ple CPUs. You need re­al Pthreads for that.

You don't get non-block­ing I/O... un­less you use pth_read in­stead of read­(), pth_write() in­stead of write() and so on. You may or may not be able to take ad­van­tage of that, de­pend­ing on your ap­pli­ca­tion.

You don't get PER­FECT­LY non-block­ing UI. You get a most­ly non-block­ing one. If you don't call pth_yield­(), you WILL block. In most cas­es, you get a GOOD ENOUGH non­block­ing UI, though.

You don't get rid of da­ta races. You get rid of MOST da­ta races, but you can get some. Con­sid­er what hap­pens in the ex­am­ple if some­one deletes the but­ton :-) You can fix most of these things by mak­ing ob­jects abort threads that han­dle ref­er­ences to them. here's what I use (with a bit of mul­ti­ple in­her­i­tance):

class ThreadOwner
{
public:
        ThreadOwner() {};
        ~ThreadOwner()
        {
                QListIterator <pth_t> it(threads);
                for (;it.current();++it)
                {
                        pth_abort(*it.current());
                }
        };
        void registerThread(const pth_t *t)
        {
                threads.append(t);
        };
        void releaseThread(const pth_t *t)
        {
                threads.removeRef(t);
        };

        QList <pth_t> threads;
};

Just make sure you call reg­is­terThread when you spawn a thread, re­leaseThread when the thread ex­it­s, and you are prob­a­bly safe.

Fi­nal word­s:

While this is by no means a uni­ver­sal so­lu­tion, I have found it use­ful.

I am sure there are lots of im­prove­ments bet­ter pro­gram­mers than I can do, so don't be shy to ex­plain them to me :-)

You can get GNU Pth from http://www.gnu.org/­soft­ware/pth/

employment background check / 2011-12-27 23:23:


Hi very nice article


Contents © 2000-2023 Roberto Alsina