Microsoft added fibers to Windows to make it easy to port existing UNIX server applications to Windows. UNIX server applications are single-threaded (by the Windows definition) but can serve multiple clients. In other words, the developers of UNIX applications have created their own threading architecture library, which they use to simulate pure threads. This threading package creates multiple stacks, saves certain CPU registers, and switches among them to service the client requests.
Obviously, to get the best performance, these UNIX applications must be redesigned; the simulated threading library should be replaced with the pure threads offered by Windows. However, this redesign can take several months or longer to complete, so companies are first porting their existing UNIX code to Windows so that they can ship something to the Windows market.
To help companies port their code more quickly and correctly to Windows, Microsoft added fibers to the operating system.
In this post, we’ll examine the concept of a fiber, the functions that manipulate fibers, and how to take advantage of fibers. Keep in mind, of course, that you should avoid fibers in favor of more properly designed applications that use Windows native threads.
The first thing to note is that the Windows kernel implements threads. The operating system has intimate knowledge of threads, and it 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 non-preemptively 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.
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:
1) A user-defined value that is initialized to the value passed to ConvertThreadToFiber‘s pvParam argument
2) The head of a structured exception-handling chain
3) 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.)
4) Various CPU registers, including a stack pointer, an instruction pointer, and others
By default, on an x86 system, the CPU’s floating-point state information is not part of the CPU registers that are maintained on a per-fiber basis, which can cause data corruption to occur if your fiber performs floating-point operations. To override the default, you should call the new ConvertThreadToFiberEx function, which allows you to pass FIBER_FLAG_FLOAT_SWITCH for the dwFlags parameter:
PVOID ConvertThreadToFiberEx(PVOID pvParam, DWORD dwFlags);
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 (the 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. If you are using a lot of fibers, you might want to consume less memory for their respective stacks. In that case, instead of calling CreateFiber, you can use the following function:
SIZE_T dwStackCommitSize, SIZE_T dwStackReserveSize, DWORD dwFlags, PFIBER_START_ROUTINE pStartAddress, PVOID pvParam);
The dwStackCommitSize parameter sets the part of the stack that is initially committed. The dwStackReserveSize parameter allows you to reserve an amount of virtual memory. The dwFlags parameter accepts the same FIBER_FLAG_FLOAT_SWITCH value as ConvertThreadToFiberEx does to add the floating-point state to the fiber context. The other parameters are the same as for CreateFiber.
Next, CreateFiber(Ex) allocates a new fiber execution context structure and initializes it. The user-defined value is set to the value passed to the pvParam parameter, 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(Ex), CreateFiber(Ex) returns the memory address of the fiber’s execution context. However, unlike ConvertThreadToFiber(Ex), 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(Ex) or CreateFiber(Ex). 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. When all fibers are deleted, it is also possible to remove the fiber state from the original thread that called ConvertThreadToFiber(Ex) by using ConvertFiberToThread, releasing the last pieces of memory that made the thread a fiber.
If you need to store information on a per-fiber basis, you can use the Fiber Local Storage, or FLS, functions. These functions do for fibers what the TLS functions do for threads. You first call FlsAlloc to allocate an FLS slot that can be used by all fibers running in the current process. This function takes a single parameter: a callback function that is called either when a fiber gets destroyed or when the FLS slot is deleted by a call to FlsFree. You store per-fiber data in an FLS slot by calling FlsSetValue, and you retrieve it with FlsGetValue. If you need to know whether or not you are running in a fiber execution context, simply check the Boolean return value of IsThreadAFiber.
Several 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:
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(Ex) or CreateFiber(Ex). 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.