Chapter 13

Getting Back on the Network


CONTENTS

Many developers whose early experience was with MS-DOS tend to think of computers and programs as sequential beasts. First we do this, then we do that, and then we wait while the real world catches up. In fact, all modern operating systems support some form of multitasking. Programs benefit from multitasking when they must wait for some real-world event.

In a plug-in, many real-world events have to do with waiting for the Net. If our plug-in calls, say, NPN_GetURLNotify(), there may be a delay of many milliseconds, or even seconds, before the Navigator calls NPP_URLNotify() to report that the data is available.

Sometimes, the plug-in itself needs to take some time. If you want to sort data as it arrives, for example, you may want to call a sort-and-merge routine from inside NPP_Write(). Sorts and merges are computationally expensive, and Navigator is waiting for us to finish up and turn control back over to the browser. You may want to start a separate process or thread and leave it up to this task to finish the job, while the main thread returns control to Navigator.

Introduction to Multitasking

Many compute-intensive programs benefit from getting the CPU and hanging onto it, but most programs aren't so CPU-intensive. They do some computing, and then wait for the user. Next, they may do some file I/O or some socket I/O to the network, and then they wait for the user again. These programs benefit from multitasking, a system by which a single processor works on more than one task. This situation is quite common in plug-ins that open more than one stream to (or from) the Net. The program spends a great deal of time waiting for network I/O to complete. You can make your plug-ins more efficient by allowing the operating system to take advantage of free time on the processor while your plug-in waits for data. This section introduces the concept of multitasking and shows how to start multiple tasks on Windows, the Macintosh, and under UNIX.

Understanding Processes and Threads

Modern operating systems support a variety of ways to keep the processor busy. Older operating systems, such as UNIX, originally had multiple users. (Many UNIX installations still do.) These systems were multiuser-each user had his or her own set of processes that were well-isolated from all other processes. When MS-DOS arrived, the processors were so tiny that no one could conceive of running more than one process. The 8088 was usually hard-pressed to keep up with one user!

With newer, faster processors came the opportunity for multitasking. A user might have one window open, doing a recalculation on a large spreadsheet. The same user could have another window running a communications program and downloading a file through a modem. The same user could have yet another window open on a graphic, sending it to the printer. Finally, the user might be focused on still another window, using a word processor.

Although the computer still has only one processor, each of the windows in this example benefits from running a separate process, just as the users of a UNIX system benefit from having their own set of processes. A system that can run independent processes in this way is known as a multiprocessing system.

Early multiprocessing systems for desktop computers (such as Windows 3.x and Macintosh System 7) were based on the cooperative multitasking model. Each process ran through its main event loop. When the main event loop was done, it called a "yield" function to tell the operating system that the operating system could give control to another process. Figure 13.1 illustrates this design. This system is known as "cooperative" because one ill-behaved process could mess up the works, as shown in Figure 13.2.

Figure 13.1 : Cooperative multitasking is based on the "yield" function.

Note
Cooperative multitasking was introduced on the Macintosh with System 7.0. At the bottom of the main event loop, the application calls WaitNextEvent(), a native routine that yields control back to the Mac OS until another event is available for processing.
Don't bother looking for the "yield" function in Windows-it's implicit in the operating system. The operating system keeps sending messages to your program as long as they're in the queue. When the queue runs empty, Windows just stops calling you. You have "yielded" without even knowing it.

The alternative to cooperative multitasking is preemptive multitasking. In a preemptive scheme, the operating system keeps track of which application gets to run when (usually through some timer-based mechanism). When it's time for process A to run, process B is summarily suspended, and control passes to process A. The operating system resumes process B when it's that process's turn again. In a preemptive system, the application doesn't even need to know that it was suspended. Unless it looks at the clock, it thinks it just ran to completion.

Preemptive multitasking offers the following three advantages over cooperative multitasking:

Figure 13.2 : In a cooperative multitasking system, one process can hog the system.

Processes

UNIX, with its heritage as a multiuser system, places a great deal of emphasis on processes. Most programs are written as a single thread of execution-you could put your finger on the program listing and follow it down the page. At any one time, your finger would show the one step on which the computer was working.

Some advanced programs have more than one process. In the UNIX vocabulary the function fork() starts a new process by first making a copy of the existing process's address space. Then the child process follows one path of execution, and the parent process follows another. If all goes well, the child finishes before the parent (or the parent ends up waiting patiently for the child), and then both processes exit. Forking is illustrated in Figure 13.3.

Figure 13.3 : In a multiprocessing system like UNIX, each process can fork, copying its address space and resuming execution on a different path.

In a multiprocessing system, you need more than one finger to keep track of program execution-you need one finger for the parent process and one finger for every child process forked. Because the child process can itself fork new processes, you may quickly run out of fingers.

Some of the newest computers have more than one physical processor. These machines, known as Symmetric Multiprocessors or SMPs, can perform true multiprocessing-assigning a process to its own CPU.

Threads

The advantage of a multiprocessing system based on fork() is that each process has its own address space. A programming error in one process cannot affect any other process. To communicate between processes, a variety of Interprocess Communications (IPC) methods have been invented, including pipes, semaphores, shared memory, and sockets.

The disadvantage of fork() is that copying a whole address space is computationally expensive. On many UNIX systems with sophisticated multiprocessing applications, a large percentage of the system's time is taken up by the overhead of forking.

In the late 1980s a newer multitasking design, based on threads, become popular. Threads are "lightweight processes." They behave like processes in that they support an additional path of execution, but they share the same address space as their parent. Because there is only one address space, there is little need to think about IPC. It also means that a programming error in one thread can ruin data in the other thread.

Today most versions of UNIX (and Windows 95 and Windows NT) support threads. The latest version of the Mac OS, System 8, also supports threads.

Multithreading is not appropriate for some applications. As was pointed out previously, a compute-intensive task gains little by being multithreaded. Many I/O-bound programs get a degree of multitasking from the operating system. The system often initiates a large data transfer and then frees the processor to workon other jobs.

Even when multithreading is the right attack, it can add complexity to the design, implementation, and debugging of the application. The threads share memory, so a value in a variable may change unexpectedly. Slight differences in the way the threads are scheduled can make defects intermittent and difficult to reproduce and pin down. When the program exits, extra pains must be taken to ensure that each thread has finished and has deallocated all its resources before the application itself is allowed to exit.

Synchronization and Critical Sections

All the multithreaded and multiprocessing operating systems provide mechanisms to ensure that two tasks do not compete for the same memory at the same time. These mechanisms aren't automatic-they must be carefully programmed. This section shows an example of one such mechanism, called the semaphore.

Note
If you were formally trained in computer science, you will recognize the semaphore described in this section as the p-v semaphore. There are several other semaphore designs, including the resource counter, which ensures that no more than n tasks are allowed into the critical section.
For simplicity, this chapter describes only the p-v semaphore.

Suppose that two threads share access to a resource, such as a printer or a sound card. Because there is only one resource, only one thread can use the resource at a time. What is needed is a mechanism by which the first task that needs the resource can seize it, locking out the other task. Only when the first task gives up the resource can the second task access it. The mechanism that allows the two tasks to coordinate their use of the shared resource is referred to as a synchronization mechanism. The piece of code that accesses the shared resource is a critical section. One type of synchronization mechanism is the semaphore.

A semaphore is a special flag that is shared between two tasks. The operating system guarantees that the read-and-set operation is atomic (a task can read the semaphore, see that it's clear, and set it without fear that some other task will read it at the same time and see that the semaphore is clear). Figure 13.4 illustrates the difference between an atomic and a non-atomic read-and-set operation.

Figure 13.4 : Only the operating system can provide an atomic read-and-set operation on a semaphore.

Caution
Non-atomic semaphore operations can lead to subtle errors in multithreaded applications. Don't try to build your own semaphores out of shared memory. Use the operators provided by the operating system.

Deadlocks

When designing multitasking code with critical sections and synchronization mechanisms, be careful to avoid a condition referred to as deadlock (also known as the "deadly embrace"). Figure 13.5 illustrates a deadlock. Task A seizes Resource 1, and then attempts to seize Resource 2. Task B already has Resource 2, and now attempts to seize Resource 1. Neither task can proceed. Unless someone (or something) intervenes, both tasks remain blocked indefinitely.

Figure 13.5 : A deadlock occurs when a circular chain of resource requests exists.

There are various automated techniques for dealing with deadlock. None of them works perfectly. The best defense against deadlock is careful design. Many designers carefully draw resource graphs to show which task waits for which resource, in order to satisfy themselves that no opportunity exists for a circular wait condition.

In general there are four ways to deal with the problem of deadlock:

Under some conditions the "ostrich approach"-ignoring deadlock and hoping it doesn't happen-is a perfectly acceptable solution. On some systems a deadlock is theoretically possible, but actually occurs so infrequently and with so little impact that the problem cannot be solved in a cost-effective manner. When the cost of a deadlock is low, and the price of detecting or preventing deadlocks is high, consider imitating the ostrich.

An operating system can maintain a resource graph or just look for tasks that remain blocked for an extended period. Then the operating system can arbitrarily kill one of the processes to clear the deadlock. This approach is simple and effective, if occasionally you can live with one of your tasks being killed. If this solution is unacceptable, you may need to design deadlocks out of the system.

One simple (although tedious) way to prevent deadlocks is to sequentially number all resources. Then design (or even code) with a rule that says that requests must be made in numerical order. A task that holds Resource 3 cannot request Resource 1 or 2 without first relinquishing 3. After Resource 1 is allocated, the task can ask for 2. If this request is met, the task then can ask for Resource 3 back.

The resource-numbering technique works well for most applications, although it can break down in an environment such as an operating system in which the number of resources and the number of requestors is large. In this kind of environment the time required to check the allocation sequence, deallocate, and reallocate resources may become so large that the solution becomes unfeasible.

There is an algorithm that can always avoid deadlock by making the right allocation all the time, if you have the right information available. This algorithm, known as the banker's algorithm, was proposed by Dijkstra in his paper "Co-operating Sequential Processes," which appeared in Programming Languages (Academic Press, 1965). A simple version of the banker's algorithm is described in the sidebar in this section, "The Banker's Algorithm Unlocked."

The Banker's Algorithm Unlocked
If your multitasking program shares critical hardware or other resources (such as records in a data file) consider using the banker's algorithm to prevent deadlock.
The name "banker's algorithm" comes from the fact that this algorithm could be used by a small-town banker to manage loans. Suppose that a bank has four customers, A, B, C, and D, and $100 million to loan. In this analogy the banker is an operating system or other "master program" responsible for allocating resources. The customers are tasks, and the money is a critical resource, such as a set of buffers. Each "customer" in the banker's algorithm has an assigned "credit limit," beyond which they cannot request resources. Table 13.1 shows the credit line available to each customer. The credit line corresponds to the maximum amount of the critical resource the task will ever need.

Table 13.1  Customer Credit Limits

Customer
Credit Limit
A
$60,000,000
B
50,000,000
C
40,000,000
D
70,000,000

If all the customers asked to borrow up to their credit limit at the same time, the bank would run out of money. The banker knows that this occurrence is unlikely, and he or she wants to be as fair to the customers as possible. On the other hand, the banker knows that these customers are using the loans for commercial projects. If the banker is forced to defer a loan indefinitely, the project never completes, and the original loans can never be repaid. (This condition corresponds to deadlock.) The banker uses the following rule in deciding whether or not to grant a loan request:

The banker considers each request for a resource as it occurs. If granting the request leads to a "safe state" the request is approved. A state is "safe" if the banker first checks to see if he or she has enough resources to satisfy the customer who is closest to his or her maximum. If so, the banker assumes that these loans are repaid, and then performs the same check on the customer next closest to his or her maximum. This process continues until the banker has computed a way that all customers could reach their maximum and repay their loans. If all loans eventually can be repaid, the state is considered safe and the initial request is granted.

Suppose that the banker has made the loans shown in Table 13.2, and receives a request from Customer B for an additional $10,000,000.

Table 13.2  Bank Status After a Few Loans

Customer Remaining
Credit Limit
Current Loan Balance
Credit
A
$60,000,000
$10,000,000
$50,000,000
B
50,000,000
10,000,000
40,000,000
C
40,000,000
20,000,000
20,000,000
D
70,000,000
40,000,000
30,000,000
 
 
$80,000,000
Remaining funds: $20,000,000

If the banker grants the loan, the bank will have only $10,000,000 left. Customer C is $20,000,000 from her credit limit-if the banker satisfies B, and then C needs the remaining $20,000,000 to finish her project, the banker will be unable to satisfy that loan. C's project will fail, and none of the loans can ever be repaid. This situation corresponds to deadlock.

But if Customer C asks for $20,000,000, the banker can grant it, even if it brings the available funds to zero. This state is safe because, after the $20,000,000 is loaned, C's project completes and the customer can pay off the loan. See Table 13.3. After C pays off her loan, the bank has enough funds to satisfy the full demands of the next-closest customer.

Table 13.3  C Pays Off the Loan

Customer Remaining
Credit Limit
Current Loan Balance
Credit
A
$60,000,000
$10,000,000
$50,000,000
B
50,000,000
10,000,000
40,000,000
C
40,000,000
0
40,000,000
D
70,000,000
40,000,000
30,000,000
 
 
$60,000,000
Remaining funds: $40,000,000

After C's loan is repaid, the banker has $40,000,000 available, and can satisfy other requests. The customer next closest to the limit is B. If B requests the full $40,000,000, the request can be granted. When it is repaid, the bank has $50,000,000 available-more then enough to satisfy Customer D. Finally, after D's loan is repaid, the banker can clearly satisfy Customer A, which means that although B's request for $10,000,000 leads to an unsafe state, C's request for $20,000,000 leads to a safe state, and C's request is approved.

More elaborate versions of the banker's algorithm exist, and more efficient algorithms are available (although they tend to be more complex). The two principal weaknesses of the banker's algorithm are as follows:

Regardless, for specific applications such as those associated with many plug-ins, these weaknesses can be overcome, and the relatively simple banker's algorithm can keep the system out of deadlock.

If your application reads and writes records from a shared file, you may want to consider a technique known as two-phase locking as an approach to preventing deadlock. Two-phase locking is important in preserving data integrity. Many transactions require that either all the changes be made or none of them be made. With two-phase locking, a task is blocked until it can get all the resources it needs. If all the tasks competing for a file follow the algorithm, a deadlock is impossible.

The two-phase locking algorithm requires an atomic lock such as semaphore on each record. A task first goes through the file, requesting every lock that it needs for a given transaction. If it gets all the locks, it proceeds with the update, and then releases all its locks. If any locks fail, it releases all locks, waits, and then tries to reacquire all locks.

Platform Capabilities and Limitations

Each of the three major Navigator platforms offers multitasking in various forms. The ins and outs of multitasking with each platform are discussed in the following sections.

UNIX

If your application can stand the overhead of forking, UNIX makes multitasking easy. Just fork a new process, and use one of the UNIX IPC mechanisms to pass information between parent and child processes.

To avoid the overhead of forking, use threads. Although every major version of UNIX now supports threads, their API varies somewhat. Solaris, from Sun Microsystems, is representative-it supports mutual exclusion locks (mutexes), condition variables, semaphores, and read/write locks for synchronization.

UNIX makes it easy to start a daemon process-a process that is not connected to any user and is running in the background, waiting to satisfy user requests. Print spoolers, Web servers, and the Network File System are all associated with daemons. If you run the UNIX command to list the processes (ps -ef on some versions of UNIX, ps -aux on others), you can recognize a daemon process because it has process 1 as its parent. You also may recognize daemons by their name-by convention, the last character in a daemon's name is 'd'. Figure 13.6 shows a ps -ef and highlights several daemon processes. Notice that the Web server, httpd, has one process that is a true daemon (process ID 13460). The other copies of the Web server were forked by process 13460.

Figure 13.6 : Daemons have a Parent Process ID (PPID) of 1, and usually have a name that ends in a 'd'.

Macintosh

The Mac OS has provided cooperative multitasking since 1991. The "Process Manager" provides a variety of options for initiating and scheduling processes. The "Program-to-Program Communications Toolbox," part of the native OS, supports IPC and synchronization.

The Macintosh supports one foreground process and zero or more background processes. The foreground process is the one currently interacting with the user-it has its menu bar up and its windows are in front of the windows of all other applications. If you're writing a plug-in, expect Navigator to be the foreground process.

An application can be designed without windows, corresponding to a UNIX daemon process. (Apple refers to them as background-only applications.)

A Macintosh application can launch other applications, corresponding to forking in UNIX. Use LaunchApplication() to launch another application. If you want your application to continue to run, sharing the CPU with the launched application, set the launchContinue flag in the launchControlFlags field of the launch parameter block. If you want your application to be notified when an application it has launched terminates, set the acceptAppDiedEvents flag in your SIZE resource. Your application receives an "Application Died" Apple Event ('aevt' 'obit') when the launched application terminates.

The Pascal prototype for LaunchApplication() is as follows:


FUNCTION LaunchApplication(LaunchParams: LaunchPBPtr) : OSErr;

For more details on LaunchApplication() and related functions, see Inside Macintosh (Addison-Wesley, 1994).

Note
Although Macintosh programmers have to wait for System 8 to get threads and preemptive multitasking, many toolbox routines have been asynchronous since the introduction of System 7.0 in 1991. For example, all of the low-level routines in the Data Access Manager, and some of the high-level routines, are asynchronous. To call one of these routines asynchronously, prepare a parameter block called an asynchronous parameter block and pass it to the routine. The database extension of the Data Access Manager turns control over to the device driver and returns control to your application.
As your application runs, it periodically calls WaitNextEvent(), which allows other tasks (including the database device driver) to run. The DBState() function tells you when the database device driver completes its task. When DBState() reports that the transaction completes, your plug-in calls DBGetItem() repeated to read out the database's response.

System 8 will support both threads and processes. Early versions have been available to developers since mid-1996. MacUser magazine (October, 1996) has a cover story on Apple's Future-they spend two pages on Apple's plans for rolling out new versions of Mac OS, including the OpenDoc extensions, Harmony (the interim release of Mac OS based on System 7.5,) and Gershwin (the next big release after System 8).

On the Web
For more information on System 8 (including the latest estimate of the release date), see Apple's Web site http://www.macos.apple.com/macos8/.

Windows

Microsoft introduced cooperative multitasking (which Microsoft refers to as process multitasking) in Windows 3.0. The newest versions of Windows (Windows NT and Windows 95) include preemptive multitasking (but only between the threads of a given process).

Note
Although most of the topics in this book that mention Windows 95 and Windows NT also pertain to Windows 3.1 (when 3.1 runs Win32s), thread-based multitasking is an exception. Windows 3.1 is essentially a process that runs on top of MS-DOS; as such, it supports only cooperative multitasking.

With most operating systems, it's natural to think of the user "starting" the application, which then makes calls to the operating system. When thinking about the multitasking aspects of Windows, it's better to imagine the operating system as calling various entry points into the application. WinMain is one such entry point, but so are the WindowsProcs associated with each window. Messages arrive at the operating system and are dispatched to the appropriate handlers. Figures 13.7 and 13.8 show this difference.

Figure 13.7 : With most operating systems, it's natural think of the application running "above" and independent of the operating system.

Figure 13.8 : With windows, it's more natural (and more correct) to regard the operating system as always being in control, making calls to the application as necessary.

When an application finishes handling a message, it returns control to Windows. Additionally, many of the application's functions are Windows calls, so Windows has many opportunities to take control. If the application has no messages in its queue, it has implicitly yielded to the operating system.

Because Win32 includes thread-based multitasking, there is a new emphasis on synchronization mechanisms. The Win32 API has a complete subsystem devoted to synchronization. Microsoft Foundation Classes (MFC) fully encapsulates this subsystem.

The remainder of this chapter uses Win32 to illustrate multitasking in plug-ins. Similar code can be written for UNIX, or for Mac OS System 8.

Multitasking on the Cheap

An HTML programmer can easily start two or more instances of your plug-in. Consider the following HTML page:


<HTML>

<HEAD>

.

.

.

</HEAD>

<BODY>

.

.

.

<EMBED SRC="http://www.somemachine.com/file1.tst HEIGHT=100 WIDTH=50>

.

.

.

<EMBED SRC="http://www.someothermachine.com/file2.tst HEIGHT=200 WIDTH=100>

.

.

.

</BODY>

</HTML>

When Navigator reaches the first <EMBED> tag, it contacts the remote server and gets the MIME media type of the stream. Assume that this type is the one that causes your plug-in to be invoked. Your plug-in is loaded into memory, and Navigator calls its NPP_Initialize() method. Then Navigator makes the first instance and calls NPP_New(). Next, Navigator makes the window and calls NPP_SetWindow(). Finally, Navigator calls NPP_NewStream() and gives the plug-in a chance to tell Navigator how it wants to receive the data. This example assumes that your plug-in sets the stream's stype to NP_NORMAL. Navigator begins sending you the data with a series of NPP_WriteReady() and NPP_Write() calls.

The first instance of your plug-in is now active, responding to calls from Navigator as well as mouse messages, keyboard messages, timer messages, and other messages sent by Windows to your window's WindowProc. Navigator moves on to the second <EMBED> tag.

Assuming that file2.tst is mapped to the same MIME media type as file1.tst, Navigator recognizes the need to start a second instance of your plug-in. It can bypass NPP_Initialize() because your plug-in is already loaded. Instead it goes right to NPP_New() and continues just as it did with the first file. After the second file is loaded, you have one process (Navigator) with one thread of control: the main thread in Navigator runs through each of your plug-in instances. Because the two instances share any plug-in-global data, there is a potential for conflict between the instances.

Recall that when Navigator calls NPP_New(), it sends you an NPP named instance. The pData member of instance provides a convenient place to keep instance-specific data. If all your data is instance-specific, you don't need to think too much about multitasking. But what should happen if you have plug-in-global data? Figure 13.9 illustrates this situation.

Figure 13.9 : When to copies of a plug-in instance are loaded, there is a potential for conflict on the plug-in-global data.

Returning to the Net

If your plug-in initiates any network transfers through NPN_GetURL(), NPN_PostURL(), or their ...Notify() counterparts, you can expect significant delays while the network I/O is in progress. Moreover, when the content comes back, it may trigger yet another instance of your plug-in. Chances are good that your user will be mostly idle while waiting for the transfer.

If you have background processing to do, start a thread and do it. You can afford to do a lot of work between the time of the request and the time the data comes back. To ensure reliability, use the ...Notify() version of the call, so you get a positive confirmation that the request was completed.

You also can use a thread to put up a progress control or status message. Chapter 9 "Understanding NPN Methods," in the section, "A Progress Indicator," shows how to use NPN_Status() to put up a status message similar to Netscape's.

Lack of a "Yield" Function

Another issue to consider is that there is no "yield" function between your plug-in and Navigator (or the rest of the system). Suppose that your plug-in renders a complex graphic-a compute-intensive task. Rather than hold up Navigator during the rendering, you would prefer to start the task, and then periodically yield to Navigator to let it tend to the user interface. However, if you keep the rendering task in the main thread, all of Navigator must wait for the rendering task to complete. Even clicking the Stop button has no effect because Navigator isn't listening to its messages.

Separate Threads

One solution to the problem of long tasks (such as rendering) is to split them off into separate threads. Win32 allows the programmer to start interface threads and worker threads. An interface thread is a thread capable of receiving and processing messages-it has what MFC's designers refer to as a message pump. Worker threads are simply threads without a message pump. Most applications have only one interface thread-the main thread that was started by Windows. All other threads are usually worker threads.

To start a thread, call AfxBeginThread(). The prototype for AfxBeginThread() is as follows:


CWinThread* AfxBeginThread(AFX_THREADPROC ThreadFunc,

                          LPVOID Param,

                          int InitPriority = THREAD_PRIORITY_NORMAL,

                          UINT StackSize = 0,

                          DWORD dwFlags = 0,

                          LPSECURITY_ATTRIBUTES Security = NULL);

The parameter ThreadFunc is started in its own thread. The thread continues asynchronously until ThreadFunc returns. All ThreadFuncs have the following prototype:


UINT ThreadFunc(LPVOID TFParam);

The parameter Param is a 32-bit value passed to ThreadFunc. For type safety, your ThreadFunc should cast Param to the proper type as soon as it receives it. If your compiler supports Runtime Type Identification (RTTI), you may even want to consider passing a pointer to an object, and using RTTI in ThreadFunc to ensure that ThreadFunc was called correctly.

Tip
The parameter StackSize defaults to zero. You should leave it at this value. If AfxBeginThread() sees StackSize at zero, it starts the new thread's stack out at the same size as the calling application (in the case of a plug-in, the same size as Navigator's stack). As the thread function runs, Windows adjusts the stack size dynamically to ensure that the thread always has enough room on the stack.
Similarly, leave dwFlags and Security at their default values. The default for dwFlags is for the thread to begin execution immediately. The alternative is to suspend the new thread and wait for a call to CWinThread::ResumeThread(). The default value for Security causes the new thread to inherit its security attributes from the calling thread (again, in the case of a plug-in, from Navigator).

Be careful about changing the thread's priority. If you set the priority too high, the thread can hog the CPU, and Navigator becomes sluggish. If you set the thread's priority too low, it may take a long time to do its work, giving the appearance that your plug-in performs poorly. The priorities, from highest to lowest, are as follows:

THREAD_PRIORITY_TIME_CRITICAL
THREAD_PRIORITY_HIGHEST
THREAD_PRIORITY_ABOVE_NORMAL
THREAD_PRIORITY_NORMAL
THREAD_PRIORITY_BELOW_NORMAL
THREAD_PRIORITY_LOWEST
THREAD_PRIORITY_IDLE

By default, your thread runs at THREAD_PRIORITY_NORMAL. If you set InitPriority to zero, the new thread takes the same priority as the calling thread (Navigator's).

Synchronization of Access to Global Data with a Semaphore

The previous section of this chapter, "Multiple Simultaneous Streams," introduced the concept of the semaphore. Windows provides semaphores, as well as the following other synchronization mechanisms:

This section uses semaphores to illustrate synchronization.

Make a new semaphore by calling new on the class CSemaphore. The CSemaphore constructor is specified by the following:


CSemaphore(LONG InitialCount = 1,

           LONG MaxCount = 1,

           LPSTR lpszName = NULL,

           LPSECURITY_ATTRIBUTES Security = NULL);

With its default values, CSemaphore behaves like a conventional p-v semaphore. You can control access to a CSemaphore object by using a CSingleLock or CMultiLock object. The constructor for CSingleLock is simple:


CSingleLock(CSyncObject* SyncOb, BOOL InitialState = FALSE);

Here, SyncOb points to a CSyncObject (the parent class of CSemaphore). If InitialState is TRUE, the newly constructed CSingleLock object attempts to get a lock on the CSyncObject specified by SyncOb.

After you have a CSingleLock, you can use its member functions to control access to the semaphore. Call its methods Lock() and Unlock() to set and clear the semaphore. These functions have prototypes:


BOOL CSingleLock::Lock(DWORD dwDelay = INFINITE);

BOOL CSingleLock::Unlock();

and


BOOL CSingleLock::Unlock(LONG Count, LONG* Previous = NULL);


Caution
CSemaphore is a kind of CSyncObject. Microsoft provides methods CSyncObject::Lock() and CSyncObject::Unlock(), but recommends the use of CSingleLock or CMultiLock objects instead. The example in this chapter illustrates the use of CSingleLock.

Lock() and Unlock() return TRUE if successful and FALSE if they fail.

dwDelay specifies a time to wait in milliseconds. If the time-out expires, the Lock() function returns a time-out error. For a simple p-v semaphore, only the first version of Unlock() is used.

To restrict access to a resource such as plug-in-global data by using a p-v semaphore, follow these steps:

  1. Construct a new semaphore like so:
    CSemaphore* theSemaphore = new CSemaphore();
  2. Construct a new CSingleLock object that encapsulates the semaphore:
    CSingleLock* theLock = new CSingleLock(theSemaphore);
  3. Request a lock; block until it is available, as follows:
    theLock->Lock();
  4. Access the resource;
  5. Release the lock, as follows:
    theLock->Unlock();

You could put step 1 into NPP_Initialize(), and construct a CSingleLock object on this class variable in each instance. To use the lock in a method such as NPP_Write(), request the lock, access the global data, and release the lock.

Remember to delete the lock in NPP_Destroy() and the semaphore in NPP_Shutdown().

Managing Thread Termination Through Events

When Navigator calls NPP_Destroy() to shut down your instance, your plug-in must painstakingly notify all threads it has started to stop. Win32 doesn't allow a parent thread to force a child thread to terminate. Rather, you must either wait for the child to exit on its own, or you must notify it that you want it to terminate and wait for it to exit or call AfxEndThread().

This section shows how to use a Boolean flag to notify a thread that it should terminate. This section shows how to use the MFC CEvent class to force the parent to wait until the child has terminated. In general, you can use CEvent to notify any thread or process that an event has occurred. The constructor for CEvent is as follows:


CEvent(BOOL InitialState = FALSE,

       BOOL Manual = FALSE,

       LPSTR lpszName = NULL,

       LPSECURITY_ATTRIUBUTES Security = NULL);

With its default parameters, the CEvent is constructed in such a way that the event hasn't yet occurred, but that when it occurs, it will automatically be reset by the first thread to gain access. These defaults usually are acceptable for plug-ins.

To wait on an event, a thread constructs a CSingleLock object, just as was done for semaphores.

To signal an event, a thread calls the CEvent method SetEvent().

A typical sequence that allows the plug-in to do some work asynchronously follows:

  1. In NPP_New(), construct a new CEvent that is used to notify the child thread that it's time to terminate. Save the CEvent in this instance's pData so the child thread can reach it.
  2. In NPP_New() construct a CSingleLock and save it in the pData.
  3. When the new thread is started, have it construct its own CSingleLock on the instance's CEvent.
  4. Place a Boolean "kill flag" in the instance's pData, where it can be written by the parent and read by the child thread. Initialize this flag to FALSE in NPP_New().
  5. In NPP_Destroy(), set the kill flag to TRUE and attempt to acquire a lock on the CSingleLock. The main thread will block until the event is signaled.
  6. Have the main loop of the child thread read the kill flag. If the kill flag is FALSE, the child goes about its business. If the kill flag is TRUE, the child thread cleans up, and then calls SetEvent() on the shared event.
  7. When NPP_Destroy() sees the event occur, it acquires the lock and proceeds. Delete all dynamically allocated objects (including the lock) and exit.

Figure 13.10 illustrates this process.

Figure 13.10: Use a Boolean flag and a CEvent object to coordinate the shutdown of child threads during NPP_Destroy ().

Timers

Occasionally, you may not need the power of a separate thread, but just want a periodic "ping" to trigger your plug-in to perform some chore. In NPP_New() you can set up a timer to send your timer messages. The start a timer, call SetTimer(), which is a member of CWnd. SetTimer() is specified by the following:


UINT CWnd::SetTimer(UINT ID,

                    UINT Length,

                    void (CALLBACK EXPORT *TFunc)(HWND, UINT, UINT,

                                                  DWORD));

ID specifies a numeric identifier to the timer, so your code can tell which timer is going off. Length is the period of the timer, in milliseconds. When the timer goes off, it calls your callback function, TFunc.

For ease of use, set TFunc to NULL. Then the timer sends WM_TIMER messages to your main window. Subclass the window and put WM_TIMER in your message map to have these messages sent to your plug-in.

The handler for WM_TIMER messages is named OnTimer(), and is specified by the following


afx_msg void OnTimer(UINT ID);

Windows will pass the ID of the timer in ID; double-check that the ID matches the ID of the timer you set before taking action based on this message.

If you set a timer in NPP_New(), make sure that you turn it off in NPP_Destroy(). The call to disable a timer is as follows:


BOOL CWnd::KillTimer(int ID);

Here, ID is once again the ID of the timer.

Reentrant Calls

After you allow control to leave your plug-in asynchronously, you are open to reentrant calls from Navigator. Remember that MFC classes are reentrant, but individual objects are not. You should endeavor to store any objects you generate in NPP_New() as part of the pData structure. Anything you generate in NPP_Initialize(), of course, belongs to the plug-in as a whole. You should protect it by a semaphore or a similar synchronization object so that multiple instances can successfully read and write that data.

From Here…

Multitasking is an essential part of modern operating systems such as Windows. You can start a thread to run large background tasks, or to put up a status bar or a progress indicator during file transfers. When you start running multiple instances and multiple threads, you need to think about synchronization and deadlock.