Working with Fibers

[Previous] [Next]

The first thing to note is that the Windows kernel implements threads. The operating system 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 they are scheduled according to the algorithm you define. Because you define the fiber-scheduling algorithm, fibers are nonpreemptively scheduled as far as the kernel is concerned.

The next thing to be aware of is that a single thread can contain one or more fibers. As far as the kernel is concerned, a thread is preemptively scheduled and is executing code. However, the thread executes one fiber's code at a time—you decide which fiber. (These concepts will become clearer as we go on.)

The first step you must perform when you use fibers is to turn your existing thread into a fiber. You do this by calling ConvertThreadToFiber:

 PVOID ConvertThreadToFiber(PVOID pvParam); 

This function allocates memory (about 200 bytes) for the fiber's execution context. This execution context consists of the following elements:

  • A user-defined value that is initialized to the value passed to ConvertThreadToFiber's pvParam argument
  • The head of a structured exception handling chain
  • The top and bottom memory addresses of the fiber's stack (When you convert a thread to a fiber, this is also the thread's stack.)
  • Various CPU registers, including a stack pointer, an instruction pointer, and others

After you allocate and initialize the fiber execution context, you associate the address of the execution context with the thread. The thread has been converted to a fiber, and the fiber is running on this thread. ConvertThreadToFiber actually returns the memory address of the fiber's execution context. You need to use this address later, but you should never read from or write to the execution context data yourself—the fiber functions manipulate the contents of the structure for you when necessary. Now if your fiber (thread) returns or calls ExitThread, the fiber and 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 (currently running fiber) calls CreateFiber:

 PVOID CreateFiber( DWORD dwStackSize, PFIBER_START_ROUTINE pfnStartAddress, PVOID pvParam); 

CreateFiber first attempts to create a new stack whose size is indicated by the dwStackSize parameter. Usually 0 is passed, which, by default, creates a stack that can grow to 1 MB in size 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.

Next, CreateFiber allocates a new fiber execution context structure and initializes it. The user-defined value is set to the value passed to CreateFiber's pvParam, the top and bottom memory addresses of the new stack are saved, and the memory address of the fiber function (passed as the pfnStartAddress argument) is saved.

The pfnStartAddress argument specifies the address of a fiber routine that you must implement and that must have the following prototype:

 VOID WINAPI FiberFunc(PVOID pvParam); 

When the fiber is scheduled for the first time, this function executes and is passed the pvParam value that was originally passed to CreateFiber. You can do whatever you like in this fiber function. However, the function is prototyped as returning VOID—not because the return value has no meaning, but 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. However, unlike ConvertThreadToFiber, this new fiber does not execute because the currently running fiber is still executing. Only one fiber at a time can execute on a single thread. To make the new fiber execute, you call SwitchToFiber:

 VOID SwitchToFiber(PVOID pvFiberExecutionContext); 

SwitchToFiber takes a single parameter, pvFiberExecutionContext, which 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. Internally, SwitchToFiber performs the following steps:

  1. It saves some of the current CPU registers, including the instruction pointer register and the stack pointer register, in the currently running fiber's execution context.
  2. It loads the registers previously saved in the soon-to-be-running fiber's execution context into the CPU registers. These registers include the stack pointer register so that this fiber's stack is used when the thread continues execution.
  3. It associates the fiber's execution context with the thread; the thread runs the specified fiber.
  4. It sets the thread's instruction pointer to the saved instruction pointer. The thread (fiber) continues execution where this fiber last executed.

SwitchToFiber is the only way for a fiber to get any CPU time. Because your code must explicitly call SwitchToFiber at the appropriate times, you are in complete control of the fiber scheduling. Keep in mind that fiber scheduling has nothing to do with thread scheduling. The thread that the fibers run on can always be preempted by the operating system. When the thread is scheduled, the currently selected fiber runs—no other fiber runs unless SwitchToFiber is explicitly called.

To destroy a fiber, you call DeleteFiber:

 VOID DeleteFiber(PVOID pvFiberExecutionContext); 

This function deletes the fiber indicated by the pvFiberExecutionContext parameter, which is, of course, 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. But if you pass the address of the fiber that is currently associated with the thread, the function calls ExitThread internally, which causes the thread and all the fibers created on the thread to die.

DeleteFiber is usually called by one fiber to delete another. 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. We can take advantage of this ability of a fiber to cleanly delete another fiber—I'll discuss how when I explain the sample application later in this chapter.

Two additional fiber functions are provided for your convenience. A thread can execute a single 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:

 PVOID GetCurrentFiber(); 

The other convenience function is GetFiberData:

 PVOID GetFiberData(); 

As I've mentioned, each fiber's execution context contains a user-defined value. This value is initialized with the value that is passed as the pvParam argument to 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 value.

Both GetCurrentFiber and GetFiberData are fast and are usually implemented as intrinsic functions, which means that the compiler generates the code for these functions inline.

The Counter Sample Application

The Counter application ("12 Counter.exe") in Figure 12-1 uses fibers to implement background processing. When you run the application, the dialog box below appears. (I recommend that you run the application to really understand what's happening and to see the behavior as you read along.)

You can think of this application as a superminiature spreadsheet consisting of two cells. The first cell is a writable cell implemented as an edit control (labeled Count To), and the second cell is a read-only cell implemented as a static control (labeled Answer). When 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 0 and increments slowly until the value in the Answer cell becomes the same value as the entered number. For demonstration purposes, the static control at the bottom of the dialog box updates to indicate which fiber is currently executing. This fiber can be either the user interface fiber or the recalculation fiber.

To test the application, type 5 in the edit control. The Currently Running Fiber field changes to Recalculation, and the number in the Answer field slowly increments from 0 to 5. When the counting is finished, the Currently Running Fiber field changes back to User Interface and the thread goes to sleep. Now, in the edit control, type 0 after the 5 (making 50) and watch the counting start over from 0 and go to 50. But this time, while the Answer field increments, move the window on the screen. 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 to the user. When you stop moving the window, the recalculation fiber is rescheduled and the Answer field continues counting from where it left off.

One last thing to test: while the recalculation fiber is counting, change the number in the edit control. Again, notice that the user interface is responsive to your input—but also that when you stop typing, the recalculation fiber starts counting from the beginning. This is exactly the kind of behavior that you want in a full-blown spreadsheet application.

Keep in mind that no critical sections or other thread synchronization objects are used in this application—everything is done using a single thread consisting of two fibers.

Let's discuss how this application is implemented. When the process's primary thread starts by executing _tWinMain (at the end of the listing), ConvertThreadToFiber is called to turn the thread into a fiber and to allow us 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 in the global g_FiberInfo variable. Three states are possible, as described in the following table.

State Description
BPS_DONE The recalculation ran to completion, and the user has not changed anything that would require a recalculation.
BPS_STARTOVER The user has changed something that requires a recalculation to start from the beginning.
BPS_CONTINUE The recalculation was started but has not finished. Also, the user has not changed anything that would require the recalculation to start over from the beginning.

The background processing state variable is examined in the thread's message loop, which is more complicated than a normal message loop. Here is what the message loop does:

  • If a window message exists (the user interface is active), it processes the message. Keeping the user interface responsive is always a higher priority than recalculating values.
  • If the user interface has nothing to do, it checks to see whether any recalculations need to be performed. (The background processing state is BPS_STARTOVER or BPS_CONTINUE.)
  • If there are no recalculations 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, we need to start the recalculation over from the beginning (BPS_STARTOVER). The first thing to realize is that we might already have a recalculation fiber running. If this is the case, we must delete the fiber and create a new fiber that will start counting from the 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 perfectly OK: the fiber's stack and execution context are completely and cleanly destroyed. If we were to use threads instead of fibers, the user interface thread would not destroy the recalculation thread cleanly—we'd have to use some form of interthread communication and wait for the recalculation thread to die on its own. Once we know that no recalculation fiber exists, we can create a new recalculation fiber and set the background processing state to BPS_CONTINUE.

When the user interface is idle and the recalculation fiber has something to do, we schedule it time by calling SwitchToFiber. SwitchToFiber does not return until the recalculation fiber calls SwitchToFiber again, passing the address of the user interface fiber's execution context.

The FiberFunc function contains the code executed by the recalculation fiber. This fiber function is passed the address of the global g_FiberInfo structure so that 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. The address of this structure need not be passed since it is in a global variable, but I wanted to demonstrate how to pass arguments to fiber functions. Besides, passing the address places fewer dependencies on the code, which is always good practice.

The fiber function first updates the status control in the dialog box to indicate that the recalculation fiber is executing. Then it gets the number in the edit control and enters a loop that starts counting from 0 to the number. Each time the number is about to be incremented, GetQueueStatus is called to see whether 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 shows up, the user interface fiber has something to do; because we want it to take priority over the recalculations, SwitchToFiber is called immediately so the user interface fiber can process the message. After the message has been processed, the user interface fiber reschedules the recalculation fiber (as described earlier) and the background processing continues.

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 include it here to exaggerate the time required to perform the recalculation.

When the Recalculation fiber finishes calculating the answer, the background processing state variable is set to BPS_DONE and a call to SwitchToFiber reschedules the user interface fiber. At this point, if the user interface fiber has nothing to do, it calls WaitMessage, suspending the thread so that no CPU time is wasted.

Figure 12-1. The Counter sample application

 Counter.cpp /****************************************************************************** Module: Counter.cpp Notices: Copyright (c) 2000 Jeffrey Richter ******************************************************************************/ #include "..\CmnHdr.h" /* See Appendix A. */ #include <WindowsX.h> #include <tchar.h> #include "Resource.h" /////////////////////////////////////////////////////////////////////////////// // The possible state of the background processing typedef enum { BPS_STARTOVER, // Start the background processing from the beginning. BPS_CONTINUE, // Continue the background processing. BPS_DONE // There is no background processing to do. } BKGNDPROCSTATE; typedef struct { PVOID pFiberUI; // User interface fiber execution context HWND hwnd; // Handle of main UI window BKGNDPROCSTATE bps; // State of background processing } FIBERINFO, *PFIBERINFO; // A global that contains application state information. This // global is accessed directly by the UI fiber and indirectly // by the background processing fiber. FIBERINFO g_FiberInfo; /////////////////////////////////////////////////////////////////////////////// void WINAPI FiberFunc(PVOID pvParam) { PFIBERINFO pFiberInfo = (PFIBERINFO) pvParam; // Update the window showing which fiber is executing. SetDlgItemText(pFiberInfo->hwnd, IDC_FIBER, TEXT("Recalculation")); // Get the current count in the EDIT control. int nCount = GetDlgItemInt(pFiberInfo->hwnd, IDC_COUNT, NULL, FALSE); // Count from 0 to nCount, updating the STATIC control. for (int x = 0; x <= nCount; x++) { // UI events have higher priority than counting. // If there are any UI events, handle them ASAP. if (HIWORD(GetQueueStatus(QS_ALLEVENTS)) != 0) { // The UI fiber has something to do; temporarily // pause counting and handle the UI events. SwitchToFiber(pFiberInfo->pFiberUI); // The UI has no more events; continue counting. SetDlgItemText(pFiberInfo->hwnd, IDC_FIBER, TEXT("Recalculation")); } // Update the STATIC control with the most recent count. SetDlgItemInt(pFiberInfo->hwnd, IDC_ANSWER, x, FALSE); // Sleep for a while to exaggerate the effect; remove // the call to Sleep in production code. Sleep(200); } // Indicate that counting is complete. pFiberInfo->bps = BPS_DONE; // Reschedule the UI thread. When the UI thread is running // and has no events to process, the thread is put to sleep. // NOTE: If we just allow the fiber function to return, // the thread and the UI fiber die — we don't want this! SwitchToFiber(pFiberInfo->pFiberUI); } /////////////////////////////////////////////////////////////////////////////// BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam) { chSETDLGICONS(hwnd, IDI_COUNTER); SetDlgItemInt(hwnd, IDC_COUNT, 0, FALSE); return(TRUE); } /////////////////////////////////////////////////////////////////////////////// void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) { switch (id) { case IDCANCEL: PostQuitMessage(0); break; case IDC_COUNT: if (codeNotify == EN_CHANGE) { // When the user changes the count, start the // background processing over from the beginning. g_FiberInfo.bps = BPS_STARTOVER; } break; } } /////////////////////////////////////////////////////////////////////////////// INT_PTR WINAPI Dlg_Proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog); chHANDLE_DLGMSG(hwnd, WM_COMMAND, Dlg_OnCommand); } return(FALSE); } /////////////////////////////////////////////////////////////////////////////// int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int) { // Counter fiber execution context PVOID pFiberCounter = NULL; // Convert this thread to a fiber. g_FiberInfo.pFiberUI = ConvertThreadToFiber(NULL); // Create the application's UI window. g_FiberInfo.hwnd = CreateDialog(hinstExe, MAKEINTRESOURCE(IDD_COUNTER), NULL, Dlg_Proc); // Update the window showing which fiber is executing. SetDlgItemText(g_FiberInfo.hwnd, IDC_FIBER, TEXT("User interface")); // Initially, there is no background processing to be done. g_FiberInfo.bps = BPS_DONE; // While the UI window still exists... BOOL fQuit = FALSE; while (!fQuit) { // UI messages are higher priority than background processing. MSG msg; if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { // If a message exists in the queue, process it. fQuit = (msg.message == WM_QUIT); if (!IsDialogMessage(g_FiberInfo.hwnd, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } } else { // No UI msgs exist; check the state of the background processing. switch (g_FiberInfo.bps) { case BPS_DONE: // No background processing to do; wait for a UI event. WaitMessage(); break; case BPS_STARTOVER: // User changed the count; restart the background processing. if (pFiberCounter != NULL) { // A recalculation fiber exists; delete it so that // background processing starts over from the beginning. DeleteFiber(pFiberCounter); pFiberCounter = NULL; } // Create a new recalc fiber that starts from the beginning. pFiberCounter = CreateFiber(0, FiberFunc, &g_FiberInfo); // The background processing started; it should continue. g_FiberInfo.bps = BPS_CONTINUE; // Fall through to BPS_CONTINUE case... case BPS_CONTINUE: // Allow the background processing to execute... SwitchToFiber(pFiberCounter); // The background processing has been paused // (because a UI message showed up) or has been // stopped (because the counting has completed). // Update the window showing which fiber is executing. SetDlgItemText(g_FiberInfo.hwnd, IDC_FIBER, TEXT("User interface")); if (g_FiberInfo.bps == BPS_DONE) { // The background processing ran to completion. Delete the // fiber so that processing will restart next time. DeleteFiber(pFiberCounter); pFiberCounter = NULL; } break; } // switch on background processing state } // No UI messages exist } // while the window still exists DestroyWindow(g_FiberInfo.hwnd); return(0); // End the application. } //////////////////////////////// End of File ////////////////////////////////// 

 Counter.rc //Microsoft Developer Studio generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // English (U.S.) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) #ifdef _WIN32 LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #pragma code_page(1252) #endif //_WIN32 ///////////////////////////////////////////////////////////////////////////// // // Dialog // IDD_COUNTER DIALOG DISCARDABLE 0, 0, 156, 37 STYLE DS_3DLOOK | DS_CENTER | WS_MINIMIZEBOX | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "Counter" FONT 8, "MS Sans Serif" BEGIN LTEXT "Count to:",IDC_STATIC,4,6,34,8 EDITTEXT IDC_COUNT,38,4,40,14,ES_AUTOHSCROLL | ES_NUMBER LTEXT "Answer:",IDC_STATIC,90,6,25,8 RTEXT "0",IDC_ANSWER,122,6,23,8 LTEXT "Currently running fiber:",IDC_STATIC,4,24,75,8 LTEXT "Fiber",IDC_FIBER,80,24,72,8 END ///////////////////////////////////////////////////////////////////////////// // // DESIGNINFO // #ifdef APSTUDIO_INVOKED GUIDELINES DESIGNINFO DISCARDABLE BEGIN IDD_COUNTER, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 149 TOPMARGIN, 7 BOTTOMMARGIN, 30 END END #endif // APSTUDIO_INVOKED #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE DISCARDABLE BEGIN "resource.h\0" END 2 TEXTINCLUDE DISCARDABLE BEGIN "#include ""afxres.h""\r\n" "\0" END 3 TEXTINCLUDE DISCARDABLE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_COUNTER ICON DISCARDABLE "Counter.ico" #endif // English (U.S.) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED 



Programming Applications for Microsoft Windows
Programming Applications for Microsoft Windows (Microsoft Programming Series)
ISBN: 1572319968
EAN: 2147483647
Year: 1999
Pages: 193

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net