This chapter discusses:
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:
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.
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:
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.
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.
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.
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.
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:
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:
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.
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:
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:
Or:
Or:
Or:
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.
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.
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():
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.
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:
Or:
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.
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.
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:
The functions that implement this counter, PtPreventExit() and PtAllowExit(), are not only thread-safe, but also realtime-friendly: they're guaranteed to run a bound amount of code and never generate priority inversion.
This mechanism is considered relatively low-level and meant primarily for threads that don't have anything to do with Photon (perhaps temporarily — i.e. while in a PtLeave() - PtEnter() section of a Photon callback).
The reason is that certain Photon calls that normally are blocking cause the calling thread to go to sleep (blocked indefinitely) if PtExit() is pending (otherwise PtExit() would potentially block for a long time). This also happens when a thread blocks before another thread calls PtExit(); the blocked thread stays blocked without returning from the blocking call. The sleeping threads behave as if the scheduler didn't give them any CPU cycles until the entire process terminates. This allows the thread(s) that called PtPreventExit() to finish their job as quickly as possible.
The list of Photon calls that make their calling threads sleep after another thread has called PtExit() includes attempts to process events, do anything modal, block on a condvar using PtCondWait() or PtCondTimedWait(), calling PtEnter() or PtLeave(), and calling PtExit().
To prevent such situations, there's a Pt_DELAY_EXIT flag that you can pass to PtEnter() and PtLeave(). Doing it not only prevents PtEnter() and PtLeave() from blocking indefinitely if another thread has called PtExit(), but also implicitly calls PtPreventExit(). If your thread is put to sleep by a “sleep inducing” call, the library knows to call PtAllowExit() for you. The only way to keep Pt_DELAY_EXIT turned on is by making sure that you don't call any of the “sleep inducing” calls and pass Pt_DELAY_EXIT to PtEnter() and PtLeave() each time you call them. The Pt_DELAY_EXIT flag makes your “save file” callback as simple as this:
my_callback( ... ) { PtLeave( Pt_DELAY_EXIT ); save_file(); /* You're safe here... */ PtEnter( 0 ); /* But this may never return -- and that's OK! */ }
You still have to make sure that save_file() doesn't attempt any of the “sleep inducing” calls. In particular, you can't pop up a dialog with an error message if something goes wrong. If you want to pop up a dialog that will potentially sit on the screen for minutes or hours, you have to do it before calling PtExit(), for example, by using the Pt_ARG_WINDOW_MANAGED_FLAGS trick mentioned above.
The protection that Pt_DELAY_EXIT gives your thread is disabled not only when the thread is put to sleep, but also when it dies for any reason. |
Note the following concerning threads and work procedures: