Jeffrey Richter wrote Advanced Windows (Microsoft Press, 1995) and Windows 95: A Developer's Guide (M&T Books, 1995). Jeff is a consultant and teaches Win32-based programming seminars. He can be reached at firstname.lastname@example.org.
Click to open or copy the COUNTER project files.
QI was reading Matt Pietrek's article, "Poking Around Under the Hood: A Programmer's View of Windows NT 4.0," in the August 1996 issue of MSJ. When I got to the section about fibers, I started wondering if fibers could be used as an easier, more efficient mechanism for performing background processing tasks.
Sometimes, tasks like spreadsheet recalculations are done using a background thread. To design the spreadsheet application to work this way, I'd have to spawn a worker thread that scans the spreadsheet's cells, recalculates each cell's value, and then saves the new value back in the cell, possibly updating the window so that the user can see the change as well. Of course, the application's user-interface (primary) thread may be responding to the user's input asynchronously and changing the contents of cells while the recalculation thread is running. Since data could be corrupted if the two threads access a cell's value at the same time, thread synchronization would have to be employed.
Without actually implementing this code myself, I guess that I would use a single critical section to guard access to all the spreadsheet's cells. But it's also possible that the user could change a cell that requires the recalculation thread to stop what it's doing and start all over from the beginning. To implement this correctly, I'd need some interthread communication, probably implemented as a global variable, that the user-interface thread would set and that the recalculation worker thread would look for. When the worker thread saw the flag, it would restart itself, recalculating all of the spreadsheet's cells.
I didn't implement this code myself because it didn't sound like much fun. But when I started reading about fibers, I thought that they could be used to easily implement this technique. It seems to me that you can structure your user-interface thread as one fiber and your recalculation thread as another fiber. Because both fibers are really running on a single thread they cannot run at the same time, and therefore you avoid the tricky thread synchronization problems.
QCan you please explain the fiber functions in more detail and demonstrate how you might use them to perform the kind of background processing I've just described?
Via the Internet
AOK, I accept your challenge. I'll begin with a conceptual overview of fibers. Then I'll examine the various functions that exist for manipulating fibers. After that, I'll present a small application that uses fibers to perform background processing tasks.
The first thing to keep in mind is that threads are implemented by the Windows¨ kernel. The OS has intimate knowledge of threads and schedules them according to the algorithm defined by Microsoft. A fiber is implemented in user-mode code; the kernel does not have knowledge of fibers, and fibers are scheduled according to your algorithm. This means fibers are nonpreemptively scheduled as far the kernel is concerned.
The next thing to be aware of is that a single thread may contain one or more fibers. As far as the kernel is concerned, a thread is being preemptively scheduled and is executing code. The thread will be executing one fiber's code at a time-you decide which fiber is executing. This will become clearer as I go on.
The first step that must be performed when using fibers is to turn your existing thread into a fiber. This is done by calling ConvertThreadToFiber:
LPVOID ConvertThreadToFiber(LPVOID lpParameter);
This function allocates memory for the fiber's execution context (about 200 bytes in size). This execution context consists of the following:
- A user-defined 32-bit value initialized to the value passed to ConvertThreadToFiber's lpParameter argument.
- The head of a structured exception-handling chain.
- The top and bottom memory addresses of the fiber's stack. When converting a thread to a fiber, this is also the thread's stack.
- Various CPU registers including a stack pointer, instruction pointer, and others.
After the fiber execution context has been allocated and initialized, the address of the execution context is associated with the thread-the thread has been converted to a fiber and the fiber is now running on this thread. (If you have only one fiber, then it is the same as having only the thread. The fun doesn't begin until you have multiple fibers.) ConvertThreadToFiber actually returns the memory address of the fiber's execution context. You will need to use this address later, but you should never read or write to the execution context data yourself. The fiber functions will manipulate the contents of the structure for you when necessary. If your fiber (thread) returns or calls ExitThread, the fiber and the thread both die.
There is no reason to convert a thread to a fiber unless you plan to create additional fibers to run on the same thread. To create another fiber, the thread (with the currently running fiber) calls CreateFiber:
LPVOID CreateFiber(DWORD dwStackSize,
CreateFiber first attempts to create a new stack of the size indicated by the dwStackSize parameter. Usually zero is passed, which, by default, creates a stack that can grow
to 1MB but initially has two pages of storage committed to it. If you specify a nonzero size, a stack is reserved and committed using the specified size.
After the stack is created, CreateFiber allocates a new fiber execution context structure and initializes it. The user-defined value is set to the value passed to CreateFiber's lpParameter, the top and bottom memory addresses of the new stack are saved, and the memory address of the fiber function (passed as the lpStartAddress argument) is saved. The lpStartAddress argument must specify the address of a fiber routine, which you must implement and which must have the following prototype:
VOID WINAPI FiberFunc(PVOID lpParameter);
When the fiber is scheduled for the first time, this function executes and is passed the lpParameter value originally passed to CreateFiber. You can do whatever you like in this fiber function. Notice that the function is prototyped as returning VOID. This is not because the return value has no meaning; it is because this function should never return at all! If a fiber function does return, the thread and all the fibers created on it are destroyed immediately.
Like ConvertThreadToFiber, CreateFiber returns the memory address of the fiber's execution context. Unlike ConvertThreadToFiber, this new fiber is not executing because the currently running fiber is still executing. Only one fiber at a time can be executing on a single thread. To make the new fiber execute, SwitchToFiber must be called:
VOID SwitchToFiber(LPVOID lpFiber);
lpFiber is the memory address of a fiber's execution context as returned by a previous call to ConvertThreadToFiber or CreateFiber. This memory address tells the function which fiber to schedule now. Internally, SwitchToFiber performs the following steps:
- Some of the current CPU registers are saved in the currently running fiber's execution context. This includes the instruction pointer register and the stack pointer register.
- The registers previously saved in the soon-to-be-running fiber's execution context are loaded into the CPU registers. This includes the stack pointer register so that this fiber's stack will be used when the thread continues execution.
- The fiber's execution context is associated with the thread-the thread is running the specified fiber.
- The thread's instruction pointer is set to the saved instruction pointer. The thread (fiber) will continue execution where this fiber was last executing.
SwitchToFiber is the only way for a fiber to get any CPU time. Since your code must explicitly call SwitchToFiber at the appropriate times, you are in complete control of the fiber scheduling. Keep in mind that this has nothing to do with the thread scheduling. The thread that the fibers are running on is always preemptable by the operating system. When the thread is scheduled, the currently selected fiber runs and no other fiber will run unless SwitchToFiber is explicitly called.
When you want to destroy a fiber, you do so by calling DeleteFiber:
VOID DeleteFiber(LPVOID lpFiber);
This function deletes the fiber indicated by the lpFiber parameter, which is the address of a fiber's execution context. This function frees the memory used by the fiber's stack and then destroys the fiber's execution context. Take note: if you pass the address of the fiber currently associated with the thread, this function calls ExitThread internally, which causes the thread and all the fibers created on the thread to die. DeleteFiber is the same as ExitThread if you delete the active fiber. Watch out!
Note that DeleteFiber is usually called by one fiber to delete another fiber. The deleted fiber's stack is destroyed and the fiber's execution context is freed. Notice the difference here between fibers and threads: threads usually kill themselves by calling ExitThread. In fact, it is considered bad form for one thread to terminate another thread using TerminateThread. If you do call TerminateThread, the system does not destroy the terminated thread's stack. I exploit a fiber's ability to cleanly delete another fiber in my sample application.
Two additional fiber functions exist simply for your convenience. A thread can be executing one fiber at a time and the operating system always knows which fiber is currently associated with the thread. If you want to get the address of the currently-running fiber's execution context, you can call GetCurrentFiber:
The other convenience function is GetFiberData:
As I've mentioned, each fiber's execution context contains a 32-bit user-defined value. This value is initialized with the value passed as the lpParameter argument to either ConvertThreadToFiber or CreateFiber. This value is also passed as an argument to a fiber function. GetFiberData simply looks in the currently executing fiber's execution context and returns the saved 32-bit value.
Both GetCurrentFiber and GetFiberData are very fast functions and are usually implemented as intrinsics, which means that the compiler generates the code for these functions inline.
The Counter Sample Application
The Counter application listed in Figure 1 demonstrates fibers for background processing. When you run the application, the dialog box shown in Figure 2 appears.
Figure 2 Counter
You can think of this application as a super-miniature spreadsheet consisting of two cells. First is a writable cell implemented as an edit control (after the "Count to" text); second is a read-only cell implemented as a static control (after the "Answer" text). As you change the number in the edit control, the Answer cell automatically recalculates. For this simple application, the recalculation is a counter that starts at zero and increments slowly until the answer becomes the same value as the entered number. For demonstration purposes, the bottom of the dialog box updates to indicate which fiber is currently executing. This can be either the user-interface fiber or the recalculation fiber.
To test the application out, type a 5 into the edit control: the currently running fiber field will change to Recalculation and the number in the Answer field will slowly increment from zero to 5. When the counting is finished, the currently running fiber field will change back to user-interface and the thread will go to sleep.
Next, type a zero in the edit control after the 5 (making 50) and watch the counting start over from zero to 50. But this time, while the answer is incrementing, move the window on the screen. When you do this, you'll notice that the recalculation fiber is preempted and that the user-interface fiber is rescheduled so that the application's user-interface stays responsive. When you stop moving the window, the recalculation fiber is rescheduled and the answer field continues counting from where it left off.
Here's another way to test the program: while the recalculation fiber is counting, change the number in the edit control. Again, the UI is responsive to your input, but when you stop typing, the recalculation fiber starts counting from the very beginning. This is exactly the kind of behavior desirable in a real spreadsheet application.
Keep in mind that there are no critical sections or other thread-synchronization objects used in this application-everything is done using a single thread consisting of two fibers.
Now let's discuss the implementation. When the process's primary thread starts by executing WinMain (at the end of Figure 1), ConvertThreadToFiber is called to turn the thread into a fiber and to allow you to create another fiber later. Then a modeless dialog box is created, which is the application's main window. Next, a state variable is initialized to indicate the background processing state (BPS). This state variable is the bps member contained inside the global g_FiberInfo variable. There are three possible states, as described in Figure 3.
The background processing state variable is examined in the thread's message loop, which is more complicated than a normal message loop. First, if a window message exists (the user interface is active), it processes the message. Keeping the user interface responsive is always higher priority than recalculating values. If the user interface has nothing to do, then it checks to see if there are recalculations that need to be performed (the background processing state is BPS_STARTOVER or BPS_CONTINUE). If there is nothing to do (BPS_DONE), it suspends the thread by calling WaitMessage; only a user-interface event can cause a recalculation to be required.
If the user-interface fiber has nothing to do and the user has just changed the value in the edit control, the app needs to start the recalculation over from the beginning (BPS_STARTOVER). The first thing to realize here is that there may already be a recalculation fiber running. If this is the case, you need to delete the fiber and create a new fiber, which will start its counting from the very beginning. The user-interface fiber calls DeleteFiber to destroy the existing recalculation fiber. This is where fibers (as opposed to threads) come in handy. Deleting the recalculation fiber is a perfectly OK thing to do; the fiber's stack and execution context are completely and cleanly destroyed. If you were using threads instead of fibers, it would not be possible to have the user-interface thread destroy the recalculation thread cleanly-you would need some form of interthread communication and wait for the recalculation thread to die on its own. Once you know that no recalculation fiber exists, you can create a new recalculation fiber and set the background processing state to BPS_CONTINUE.
When the user-interface fiber is idle and the recalculation fiber has something to do, schedule it by calling SwitchToFiber. SwitchToFiber will not return until the recalculation fiber calls SwitchToFiber again, passing the address of the user-interface fiber's execution context.
The Counter_FiberFunc function contains the code executed by the recalculation fiber. This fiber function is passed the address of the global g_FiberInfo structure so it knows the handle of the dialog box window, the address of the user-interface fiber's execution context, and the current background processing state. It's true that the address of this structure does not need to be passed since it is in a global variable, but I wanted to demonstrate how to use arguments to fiber functions. Besides, passing the address places fewer dependencies on the code, which is always a good thing.
The first thing the fiber function does is update the status control in the dialog box to indicate that the recalculation fiber is executing. Then the function gets the number inside the edit control and enters a loop that starts counting from zero to the number. Each time the number is about to be incremented, GetQueueStatus is called to see if any messages have shown up in the thread's message queue (all fibers running on a single thread share the thread's message queue). When a message does show up, the user-interface fiber has something to do, and because I want it to take priority over the recalculations, SwitchToFiber is immediately called so the user-interface fiber can process the message. After the messages have been processed, the user-interface fiber will reschedule the recalculation fiber (as described earlier) and the background processing will continue.
When there are no messages to be processed, the recalculation fiber updates the answer field in the dialog box and then sleeps for 200 milliseconds. In production code, you should remove the call to Sleep; I have it here to exaggerate the time required to perform the recalculation.
When the recalculation fiber has finished calculating the answer, the background processing state variable is set to BPS_DONE and the user-interface fiber is rescheduled through a call to SwitchToFiber. At this point, if the user-interface fiber has nothing to do, it will call WaitMessage, suspending the thread so that no CPU time is wasted.
Have a question about programming in Win32? Send your questions via email to Jeffrey Richter: email@example.com.
From the November 1996 issue of Microsoft Systems Journal.