Caution: This version of this document is no longer maintained. For the latest documentation, see http://www.qnx.com/developers/docs.

Parallel Operations

This chapter discusses:

Overview

When you have to perform an operation that takes a long time to execute, it's not a good idea to implement it as a simple callback. During the time the callback is executing, the widgets in your application can't repair damage and they won't respond to user input at all. You should develop a strategy for handling lengthy operations within your application that involves returning from your callback as quickly as possible.

Returning from your callback allows the widgets to continue to update themselves visually. It also gives some visual feedback if the user attempts to do anything. If you don't want the user to be able to perform any UI operations during this time, you should deactivate the menu and command buttons. You can do this by setting the Pt_BLOCKED flag in the application window widget's Pt_ARG_FLAGS resource.

You might consider one of several different mechanisms for dealing with parallel operations:

Background processing

If a lengthy operation can't be easily decomposed, and you don't want to use multiple threads, you should at least call PtBkgdHandlerProcess() to process Photon events so that the GUI doesn't appear to be frozen.

If the operation is very lengthy, you can call PtBkgdHandlerProcess() within a loop. How often you need to call PtBkgdHandlerProcess() depends on what your application is doing. You should also find a way to let the user know what progress the operation is making.

For example, if you're reading a large directory, you could call the background handler after reading a few files. If you're opening and processing every file in a directory, you could call PtBkgdHandlerProcess() after each file.


Note: It's safe to call PtBkgdHandlerProcess() in callbacks, work procedures, and input procedures, but not in a widget's Draw method (see Building Custom Widgets) or a PtRaw widget's drawing function.

If a callback calls PtBkgdHandlerProcess(), be careful if the application can invoke the callback more than once simultaneously. If you don't want to handle this recursion, you should block the widget(s) associated with the callback.


The following functions process Photon events:

Work procedures

A work procedure is run whenever there are no messages for your application to respond to. In every iteration of the Photon event-handling loop, this procedure is called if no messages have arrived (rather than block on a MsgReceive() waiting for more messages). This procedure will be run very frequently, so keep it as short as possible.


Note: If your work procedure changes the display, call PtFlush() to make sure that it's updated.

See Threads and work procedures,” below, if you're writing a work procedure for a multithreaded program.


Work procedures are stacked; when you register a work procedure, it's placed on the top of the stack. Only the work procedure at the top of the stack is called. When you remove the work procedure that's at the top of the stack, the one below it is called.


Note: There is one exception to this rule. If the work procedure that's at the top of the stack is running already, the next one is called. This is only possible if the already running procedure allows the Photon library to start another one, perhaps by calling a modal function like PtModalBlock(), PtFileSelection() or PtAlert(), or calling PtLeave() while you have other threads ready to process events.

The work procedure itself is a callback function that takes a single void * parameter, client_data. This client_data is data that you associate with the work procedure when you register it with the widget library. You should create a data structure for the work procedure that contains all its state information and provide this as the client_data.

To register, or add, a work procedure, call PtAppAddWorkProc():

PtWorkProcId_t *PtAppAddWorkProc(
                   PtAppContext_t app_context,
                   PtWorkProc_t work_func,
                   void *data );

The parameters are:

PtAppAddWorkProc() returns a pointer to a PtWorkProcId_t structure that identifies the work procedure.

To remove a work procedure when it's no longer needed, call PtAppRemoveWorkProc():

void PtAppRemoveWorkProc(
                PtAppContext_t app_context,
                PtWorkProcId_t *WorkProc_id );

passing it the same application context and the pointer returned by PtAppAddWorkProc().

A practical example of the use of work procedures would be too long to cover here, so here's a simple iterative example. The work procedure counts to a large number, updating a label to reflect its progress on a periodic basis.

#include <Pt.h>
#include <stdlib.h>

typedef struct workDialog {
   PtWidget_t *widget;
   PtWidget_t *label;
   PtWidget_t *ok_button;
} WorkDialog_t;

typedef struct countdownClosure {
   WorkDialog_t *dialog;
   int value;
   int maxvalue;
   int done;
   PtWorkProcId_t *work_id;
} CountdownClosure_t;

WorkDialog_t *create_working_dialog(PtWidget_t *parent)
{
   PhDim_t      dim;
   PtArg_t      args[3];
   int          nargs;
   PtWidget_t   *window, *group;
   WorkDialog_t *dialog =
      (WorkDialog_t *)malloc(sizeof(WorkDialog_t));

   if (dialog)
   {
      dialog->widget = window =
         PtCreateWidget(PtWindow, parent, 0, NULL);

      nargs = 0;
      PtSetArg(&args[nargs], Pt_ARG_GROUP_ORIENTATION,
               Pt_GROUP_VERTICAL, 0); nargs++;
      PtSetArg(&args[nargs], Pt_ARG_GROUP_VERT_ALIGN,
               Pt_GROUP_VERT_CENTER, 0); nargs++;
      group = PtCreateWidget(PtGroup, window, nargs, args);

      nargs = 0;
      dim.w = 200;
      dim.h = 100;
      PtSetArg(&args[nargs], Pt_ARG_DIM, &dim, 0); nargs++;
      PtSetArg(&args[nargs], Pt_ARG_TEXT_STRING,
               "Counter:        ", 0); nargs++;
      dialog->label = PtCreateWidget(PtLabel, group,
                                        nargs, args);

      PtCreateWidget(PtSeparator, group, 0, NULL);

      nargs = 0;
      PtSetArg(&args[nargs], Pt_ARG_TEXT_STRING, "Stop", 0);
               nargs++;
      dialog->ok_button = PtCreateWidget(PtButton, group,
                                            1, args);
   }
   return dialog;
}

int done(PtWidget_t *w, void *client,
          PtCallbackInfo_t *call)
{
   CountdownClosure_t *closure =
      (CountdownClosure_t *)client;

   call = call;

   if (!closure->done) {
      PtAppRemoveWorkProc(NULL, closure->work_id);
   }
   PtDestroyWidget(closure->dialog->widget);
   free(closure->dialog);
   free(closure);
   return (Pt_CONTINUE);
}

int
count_cb(void *data)
{
   CountdownClosure_t *closure =
      (CountdownClosure_t *)data;
   char    buf[64];
   int     finished = 0;

   if ( closure->value++ == 0 || closure->value %
        1000 == 0 )
   {
      sprintf(buf, "Counter: %d", closure->value);
      PtSetResource( closure->dialog->label,
                     Pt_ARG_TEXT_STRING, buf, 0);
   }

   if ( closure->value == closure->maxvalue )
   {
      closure->done = finished = 1;
      PtSetResource( closure->dialog->ok_button,
                     Pt_ARG_TEXT_STRING, "Done", 0);
   }

   return finished ? Pt_END : Pt_CONTINUE;
}

int push_button_cb(PtWidget_t *w, void *client,
                   PtCallbackInfo_t *call)
{
   PtWidget_t   *parent = (PtWidget_t *)client;
   WorkDialog_t *dialog;

   w = w; call = call;

   dialog = create_working_dialog(parent);

   if (dialog)
   {
      CountdownClosure_t *closure =
         (CountdownClosure_t *)
          malloc(sizeof(CountdownClosure_t));

      if (closure)
      {
         PtWorkProcId_t *id;

         closure->dialog = dialog;
         closure->value = 0;
         closure->maxvalue = 200000;
         closure->done = 0;
         closure->work_id = id =
            PtAppAddWorkProc(NULL, count_cb, closure);

         PtAddCallback(dialog->ok_button, Pt_CB_ACTIVATE,
                       done, closure);
         PtRealizeWidget(dialog->widget);
      }
   }
   return (Pt_CONTINUE);
}

int main(int argc, char *argv[])
{
   PhDim_t      dim;
   PtArg_t      args[3];
   int          n;
   PtWidget_t   *window;
   PtCallback_t callbacks[] = {{push_button_cb, NULL}};
   char Helvetica14b[MAX_FONT_TAG];

   if (PtInit(NULL) == -1)
      exit(EXIT_FAILURE);

   dim.w = 200;
   dim.h = 100;
   PtSetArg(&args[0], Pt_ARG_DIM, &dim, 0);
   if ((window = PtCreateWidget(PtWindow, Pt_NO_PARENT,
                                1, args)) == NULL)
      PtExit(EXIT_FAILURE);

   callbacks[0].data = window;
   n = 0;
   PtSetArg(&args[n++], Pt_ARG_TEXT_STRING, "Count Down...", 0);

   /* Use 14-point, bold Helvetica if it's available. */

   if(PfGenerateFontName("Helvetica", PF_STYLE_BOLD, 14,
                         Helvetica14b) == NULL) {
       perror("Unable to generate font name");
   } else {
       PtSetArg(&args[n++], Pt_ARG_TEXT_FONT, Helvetica14b, 0);
   }
   PtSetArg(&args[n++], Pt_CB_ACTIVATE, callbacks,
            sizeof(callbacks)/sizeof(PtCallback_t));
   PtCreateWidget(PtButton, window, n, args);

   PtRealizeWidget(window);

   PtMainLoop();
   return (EXIT_SUCCESS);
}

When the pushbutton is pressed, the callback attached to it creates a working dialog and adds a work procedure, passing a closure containing all the information needed to perform the countdown and to clean up when it's done.

The closure contains a pointer to the dialog, the current counter, and the value to count to. When the value is reached, the work procedure changes the label on the dialog's button and attaches a callback that will tear down the entire dialog when the button is pressed. Upon such completion, the work procedure returns Pt_END in order to get removed.

The done() function is called if the user stops the work procedure, or if it has completed. This function destroys the dialog associated with the work procedure and removes the work procedure if it was stopped by the user (i.e. it didn't run to completion).

If you run this example, you may discover one of the other features of work procedures — they preempt one another. When you add a new work procedure, it preempts all others. The new work procedure will be the only one run until it has completed or is removed. After that, the work procedure that was previously running resumes. This is illustrated in the above example if the user presses the Count Down... button before a countdown is finished. A new countdown dialog is created, and that countdown runs to the exclusion of the first until it's done.

The granularity for this preemption is at the function call level. When the callback function for a work procedure returns, that work procedure may be preempted by another work procedure.

Threads

Photon applications are event-driven and callback-based; whenever an event arrives, the appropriate callback is invoked to handle it, and then the control returns to the event loop to wait for the next event. Because of this structure, most Photon applications are single-threaded.

The Photon library lets you use threads, but in a way that minimizes the overhead for single-threaded applications. The Photon library is “thread-friendly,” rather than completely thread-safe the way printf() and malloc() are thread-safe.


Note: Don't cancel a thread that might be executing a Photon library function or a callback (because the library might need to do some cleanup when the callback returns).

This section includes:

Locking the Photon library

You can use multiple threads by arranging your program so that only the thread that called PtInit() calls Photon functions, but you might find this approach restrictive.

The Photon library is mostly single-threaded, but has a mechanism that lets multiple threads use it in a safe way. This mechanism is a library lock, implemented by the PtEnter() and PtLeave() functions.

This lock is like a big mutex protecting the Photon library: only one thread can own the lock at a time, and only that thread is allowed to make Photon calls. Any other thread that wants to call a Photon function must call PtEnter() first, which blocks until the lock is available. When a thread no longer needs the lock, it calls PtLeave() to let other threads use the Photon library.

To write your non-Photon threads:


Note: Don't call PtLeave() if your thread hasn't called PtEnter(), because your application could crash or misbehave.

Remember that if you're in a callback function, something must have called PtEnter() to let you get there.


PtLeave() doesn't atomically give the library lock to another thread blocked inside PtEnter(); the other thread gets unblocked, but then it must compete with any other threads as if it just called PtEnter().

You should use PtEnter() and PtLeave() instead of using your own mutex because when PtProcessEvent() (which PtMainLoop() calls) is about to wait for an event, it unlocks the library. Once PtProcessEvent() has an event that it can process, it locks the library again. This way, your non-Photon threads can freely access Photon functions when you don't have any events to process.

If you use your own mutex that PtProcessEvent() doesn't know about, it's unlocked only when your code unlocks it. This means that the only time that your non-Photon threads can lock the mutex is when your application is processing an event that invokes one of your callbacks. The non-Photon threads can't lock the mutex when the application is idle.

Multiple event-processing threads

If you need to have a lengthy callback in your application, you can have your callback invoke PtBkgdHandlerProcess() as described earlier in this chapter. You can also spawn a new thread to do the job instead of doing it in the callback.

Another choice is to have more than one Photon thread that processes Photon events in your application. Here's how:


Note: Unlocking the library lets other threads modify your widgets and global variables while you're not looking, so be careful.

If your callback allows other threads to process events while it's doing its lengthy operation, there's a chance that the person holding the mouse may press the same button again, invoking your callback before its first invocation is complete.

You have to make sure that your application either handles this situation properly, or prevents it from happening. Here are several ways to do this:

Realtime threads

Don't make Photon calls from threads that must have deterministic realtime behavior. It's hard to predict how long PtEnter() will block for; it can take a while for the thread that owns the lock to finish processing the current event or call PtLeave(), especially if it involves sending to other processes (like the window manager).

It's better to have a “worker thread” that accepts requests from your realtime threads and executes them in its own enter-leave section. A condition variable — and possibly a queue of requests — is a good way of sending these requests between threads.

If you're using worker threads, and you need to use a condition variable, call PtCondWait() instead of pthread_cond_wait() and a separate mutex. PtCondWait() uses the Photon library lock as the mutex and makes an implicit call to PtLeave() when you block, and to PtEnter() when you unblock.

The threads block until:

PtCondTimedWait() is similar to PtCondWait(), but the blocking is limited by a timeout.

Non-Photon and Photon threads

The library keeps track of which of your threads are Photon threads (event readers) and which are non-Photon threads (nonreaders). This way, the library always knows how many of your threads are available to receive and process events. This information is currently used only by the PtModalBlock() function (see Modal operations and threads,” below).

By default, the thread that called PtInit() is an event reader, and any other thread isn't. But if a nonreader thread calls PtProcessEvent() or PtMainLoop(), it automatically becomes an event reader.


Note: Photon doesn't start new threads for you if you run out of Photon threads.

You can also turn a nonreader into a reader and back by passing a flag to PtEnter() or PtLeave():

Pt_EVENT_PROCESS_ALLOW
Turn the calling thread into an event reader.
Pt_EVENT_PROCESS_PREVENT
Turn the calling thread into a nonreader.

If you don't need to change the thread's status (e.g. for a non-Photon thread that never processes any events), don't set either of these bits in the flags.

If you're calling PtLeave() in a callback because you're about to do something lengthy, pass Pt_EVENT_PROCESS_PREVENT to PtLeave(). This tells the library that this thread isn't going to process events for a significant amount of time. Make sure to pass Pt_EVENT_PROCESS_ALLOW to PtEnter() before returning from the callback.

Modal operations and threads

A modal operation is one where you need to wait until a particular event happens before you can proceed — for example, when you want the user to make a decision and push a Yes or a No button. Since other events usually arrive before the one you're waiting for, you need to make sure that they're processed.

In a single-threaded application, attach a callback to the Yes and No buttons. In this callback, call PtModalUnblock(). When you display the dialog, call PtModalBlock(). This function runs an event-processing loop similar to PtMainLoop(), except that PtModalBlock() returns when something (e.g. the callback attached to the Yes and No buttons) calls PtModalUnblock().

In a multithreaded application, PtModalBlock() may either:

By default, PtModalBlock() uses a condition variable if you have any other Photon threads. This removes the thread from the pool of event-processing threads, but prevents a situation where starting a second modal operation in a thread that's running the event loop in PtModalBlock() makes it impossible for the first PtModalBlock() to return until after the second modal operation has completed.

In most applications, there's no chance of this happening; usually, you either don't want to allow another modal operation until the current one has completed, or you actually want the stacking behavior where the second modal operation prevents completion of the first one. For example, if the first modal operation is a file selector and the second one is an “Are you sure you want to overwrite this file?” question, you don't want to let the user dismiss the file selector before answering the question.

If you know that your application doesn't have two unrelated modal operations that may happen at the same time but can be completed in any order, you can pass Pt_EVENT_PROCESS_ALLOW to PtModalBlock(). This tells PtModalBlock() to run an event loop even if you have other Photon threads available, and may reduce the total number of Photon threads that your application needs.

Exiting a multithreaded program

Terminating a multithreaded application can be tricky; calling exit() makes all your threads just disappear, so you have to make sure that you don't exit while another thread is doing something that shouldn't be interrupted, such as saving a file.


Note: Don't call pthread_exit() in any kind of callback (such as a widget callback, an input function, a work procedure, and so on). It is likely that the library code that invoked your callback needs to do some cleanup when the callback returns. If it doesn't return, your application may leak memory.

Remember that all callbacks are run by a thread that has locked the libraries.


In a Photon application, the library may call PtExit() when your application's last window is closed. If you don't want that to happen while a thread is doing something important, turn off Ph_WM_CLOSE in your base window's Pt_ARG_WINDOW_MANAGED_FLAGS resource and handle the close message yourself. You also need to find all the calls to exit() or PtExit() in your code and make sure that you don't exit until it's safe to exit. If a widget in your base window has a Done or Cancel type callback, you have to handle that, too.

The Photon library provides some mechanisms to make handling this type of situation easier and safer:

Threads and work procedures

Note the following concerning threads and work procedures: