Using fibers to simplify enumerators, part 3: Having it both ways

Raymond Chen

As we discovered in the previous two entries [second], the problem with enumeration is that somebody always loses.

Now we will use fibers to fight back. Before you decide to use fibers in your programs, make sure to read the dire warnings at the end of this article. My goal here is to show one use of fibers, not to say that fibers are the answer to all your problems. Fibers can create more problems than they solve. We’ll come back to all the dire warnings later.

As with most clever ideas, it has a simple kernel: Use a fiber to run both the caller and the enumerator each on their own stack.

#include <windows.h>
#include <shlwapi.h>
#include <stdio.h>
#include <strsafe.h>

enum FEFOUND {
 FEF_FILE,          // found a file
 FEF_DIR,           // found a directory
 FEF_LEAVEDIR,      // leaving a directory
 FEF_DONE,          // finished
};

enum FERESULT {
 FER_CONTINUE,      // continue enumerating
                    // (if directory: recurse into it)
 FER_SKIP,          // skip directory (do not recurse)
};

class __declspec(novtable) FiberEnumerator {
public:
 FiberEnumerator();
 ~FiberEnumerator();

 FEFOUND Next();
 void SetResult(FERESULT fer) { m_fer = fer; }
 void Skip() { SetResult(FER_SKIP); }

 virtual LPCTSTR GetCurDir() = 0;
 virtual LPCTSTR GetCurPath() = 0;
 virtual const WIN32_FIND_DATA* GetCurFindData() = 0;

protected:
 virtual void FiberProc() = 0;

 static void DECLSPEC_NORETURN WINAPI
    s_FiberProc(void* pvContext);

 FERESULT Produce(FEFOUND fef);

protected:
 void* m_hfibCaller;
 void* m_hfibEnumerator;
 FEFOUND  m_fef;
 FERESULT m_fer;
};

FiberEnumerator::FiberEnumerator()
 : m_fer(FER_CONTINUE)
{
 m_hfibEnumerator = CreateFiber(0, s_FiberProc, this);
}

FiberEnumerator::~FiberEnumerator()
{
 DeleteFiber(m_hfibEnumerator);
}

void DECLSPEC_NORETURN FiberEnumerator::
    s_FiberProc(void *pvContext)
{
 FiberEnumerator* self =
    reinterpret_cast<FiberEnumerator*>(pvContext);
 self->FiberProc();

 // Theoretically, we need only produce Done once,
 // but keep looping in case a consumer gets
 // confused and asks for the Next() item even
 // though we're Done.
 for (;;) self->Produce(FEF_DONE);
}

This helper class does the basic bookkeeping of fiber-based enumeration. At construction, it remembers the fiber that is consuming the enumeration, as well as creating a fiber that will produce the enumeration. At destruction, it cleans up the fiber. The derived class is expected to implement the FiberProc method and call Produce() every so often.

The real magic happens in the (somewhat anticlimactic) Produce() and Next() methods:

FERESULT FiberEnumerator::Produce(FEFOUND fef)
{
 m_fef = fef; // for Next() to retrieve
 m_fer = FER_CONTINUE; // default
 SwitchToFiber(m_hfibCaller);
 return m_fer;
}

FEFOUND FiberEnumerator::Next()
{
 m_hfibCaller = GetCurrentFiber();
 SwitchToFiber(m_hfibEnumerator);
 return m_fef;
}

To Produce() something, we remember the production code, pre-set the enumeration result to its default of FER_CONTINUE, and switch to the consumer fiber. When the consumer fiber comes back with an answer, we return it from Produce().

To get the next item, we remember the identity of the calling fiber, then switch to the enumerator fiber. This runs the enumerator until it decides to Produce() something, at which point we take the production code and return it.

That’s all there is to it. The m_fef and m_fer members are for passing the parameters and results back and forth across the fiber boundary.

Okay, with that groundwork out of the way, writing the producer itself is rather anticlimactic.

Since we want to make things easy for the consumer, we use the interface the consumer would have designed, with some assistance from the helper class.

class DirectoryTreeEnumerator : public FiberEnumerator {
public:
 DirectoryTreeEnumerator(LPCTSTR pszDir);
 ~DirectoryTreeEnumerator();

 LPCTSTR GetCurDir() { return m_pseCur->m_szDir; }
 LPCTSTR GetCurPath() { return m_szPath; }
 const WIN32_FIND_DATA* GetCurFindData()
    { return &m_pseCur->m_wfd; }

private:
 void FiberProc();
 void Enum();

 struct StackEntry {
   StackEntry* m_pseNext;
   HANDLE m_hfind;
   WIN32_FIND_DATA m_wfd;
   TCHAR m_szDir[MAX_PATH];
 };
 bool Push(StackEntry* pse);
 void Pop();

private:
 StackEntry *m_pseCur;
 TCHAR m_szPath[MAX_PATH];
};

DirectoryTreeEnumerator::
 DirectoryTreeEnumerator(LPCTSTR pszDir)
 : m_pseCur(NULL)
{
 StringCchCopy(m_szPath, MAX_PATH, pszDir);
}

DirectoryTreeEnumerator::~DirectoryTreeEnumerator()
{
 while (m_pseCur) {
   Pop();
 }
}

bool DirectoryTreeEnumerator::
      Push(StackEntry* pse)
{
 pse->m_pseNext = m_pseCur;
 m_pseCur = pse;
 return
  SUCCEEDED(StringCchCopy(pse->m_szDir,
                 MAX_PATH, m_szPath)) &&
  PathCombine(m_szPath, pse->m_szDir, TEXT("*.*")) &&
  (pse->m_hfind = FindFirstFile(m_szPath,
       &pse->m_wfd)) != INVALID_HANDLE_VALUE;
}

void DirectoryTreeEnumerator::Pop()
{
 StackEntry* pse = m_pseCur;
 if (pse->m_hfind != INVALID_HANDLE_VALUE) {
  FindClose(pse->m_hfind);
 }
 m_pseCur = pse->m_pseNext;
}

void DirectoryTreeEnumerator::FiberProc()
{
 Enum();
}

void DirectoryTreeEnumerator::Enum()
{
 StackEntry se;
 if (Push(&se)) {
  do {
   if (lstrcmp(se.m_wfd.cFileName, TEXT(".")) != 0 &&
       lstrcmp(se.m_wfd.cFileName, TEXT("..")) != 0 &&
       PathCombine(m_szPath, se.m_szDir, se.m_wfd.cFileName)) {
    FEFOUND fef = (se.m_wfd.dwFileAttributes &
                    FILE_ATTRIBUTE_DIRECTORY) ?
                    FEF_DIR : FEF_FILE;
    if (Produce(fef) == FER_CONTINUE && fef == FEF_DIR) {
     Enum(); // recurse into the subdirectory we just produced
    }
   }
  } while (FindNextFile(se.m_hfind, &se.m_wfd));
 }
 Produce(FEF_LEAVEDIR);
 Pop();
}

As you can see, this class is a mix of the two previous classes. Like the consumer-based class, information about the item being enumerated is obtained by calling methods on the enumerator object. But like the callback-based version, the loop that generates the objects themselves is a very simple recursive function, with a call to Produce in place of a callback.

In fact, it’s even simpler than the callback-based version, since we don’t have to worry about the FER_STOP code. If the consumer wants to stop enumeration, the consumer simply stops calling Next().

Most of the complexity in the class is just bookkeeping to permit abandoning the enumeration prematurely.

Okay, let’s take this fiber out for a spin. You can use the same TestWalk function as last time, but for added generality, change the first parameter from DirectoryTreeEnumerator* to FiberEnumerator*. (The significance of this will become apparent next time.)

A little tweak needs to be made to the main function, though.

int __cdecl main(int argc, char **argv)
{
 ConvertThreadToFiber(NULL);
 DirectoryTreeEnumerator e(TEXT("."));
 TestWalk(&e);
 ConvertFiberToThread();
 return 0;
}

Since the enumerator is going to switch between fibers, we’d better convert the thread to a fiber so it’ll have something to switch back to!

Here’s a schematic of what happens when you run this fiber-based enumerator:

ConvertThreadToFiber    
Main fiber    
construct DirectoryTreeEnumerator   Enumerator fiber
CreateFiber   (not running)
initialize variables    
Next(CONTINUE)    
SwitchToFiber() starts running
    FindFirstFile etc
    Produce(FILE)
Next(CONTINUE) “returns” FILE SwitchToFiber()
use the result    
Next(CONTINUE)    
SwitchToFiber() Produce(FILE) “returns” CONTINUE
    FindNextFile etc
    Produce(DIR)
Next(CONTINUE) “returns” DIR SwitchToFiber()
use the result    
Next(CONTINUE)    
SwitchToFiber() Produce(DIR) “returns” CONTINUE
and so on… until…
    Produce(DONE)
Next(CONTINUE) “returns” DONE SwitchToFiber()
cleanup    
DeleteFiber    
destruct DirectoryTreeEnumerator    
ConvertFiberToThread    

Observe that from each fiber’s point of view, the other fiber is just a subroutine!

Coding subtlety: Why do we capture the caller’s fiber each time the Next() method is called? Why not capture it when the FiberEnumerator is constructed?

Next time, we’ll see how this fiber-based enumerator easily admits higher-order operations such as filtering and composition.

Dire warnings about fibers

Fibers are like dynamite. Mishandle them and your process explodes.

The first dire warning is that fibers are expensive in terms of address space, since each one gets its own stack (typically a megabyte).

And since each fiber has its own stack, it also has its own exception chain. This means that if a fiber throws an exception, only that fiber can catch it. (Same as threads.) That’s a strong argument against using an STL std::stack object to maintain our state: STL is based on an exception-throwing model, but you can’t catch exceptions raised by another fiber. (You also can’t throw exceptions past a COM boundary, which severely limits how much you can use STL in a COM object.)

One of the big problems with fibers is that everybody has to be in cahoots. You need to decide on one person who will call the ConvertThreadToFiber function since fiber/thread conversion is not reference-counted. If two people call ConvertThreadToFiber on the same thread, the first will convert it, and so will the second! This results in two fibers for the same thread, and things can only get worse from there.

You might think, “Well, wouldn’t the GetCurrentFiber function return NULL if the thread hasn’t been converted to a fiber?” Try it: It returns garbage. (It’s amazing how many people ask questions without taking even the slightest steps towards figuring out the answer themselves. Try writing a test program.)

But even if GetCurrentFiber told you whether or not the thread had been converted to a fiber, that still won’t help. Suppose two people want to do fibrous activity on the thread. The first converts, the second notices that the thread is already a fiber (somehow) and skips the conversion. Now the first operation completes and calls the ConvertFiberToThread function. Oh great, now the second operation is stranded doing fibrous activity without a fiber!

Therefore, you can use fibers safely only if you control the thread and can get all your code to agree on who controls the fiber/thread conversion.

An important consequence of the “in cahoots” rule is that you have to make sure all the code you use on a fiber is “fiber-safe” – a level of safety even beyond thread-safety. The C runtime library keeps information in per-thread state: There’s errno, all sorts of bonus bookkeeping when you create a thread, or call various functions that maintain state in per-thread data (such as strerror, _fcvt, and strtok).

In particular, C++ exception handling is managed by the runtime, and the runtime tracks this data in per-thread state (rather than per-fiber state). Therefore, if you throw a C++ exception from a fiber, strange things happen.

(Note: Things may have changed in the C runtime lately; I’m operating from information that’s a few years old.)

Even if you carefully avoid the C runtime library, you still have to worry about any other libraries you use that use per-thread data. None of them will work with fibers. If you see a call to the TlsAlloc function, then there’s a good chance that the library is not fiber-safe. (The fiber-safe version is the FlsAlloc function.)

Another category of things that are not fiber-safe are windows. Windows have thread affinity, not fiber affinity.

0 comments

Discussion is closed.

Feedback usabilla icon