Atul Adya, Jon Howell, Marvin Theimer, William J. Bolosky, John R. Douceur
Microsoft Research
1 Microsoft Way, Redmond, Washington 98052
{adya, howell, theimer, bolosky, johndo}@microsoft.com
“event-driven”
2.4 Conflict management
GetCAInfoFCA
// verifyFiber blocks on I/O
VerifyCert
}
CACheckCert
VerifyCert void VerifyCertCFA2(Continuation
verifyFiber
Unroll stack Done ∗vcaCont) {
FiberStart
FiberContinue
// Executed on MainFiber.
// Scheduled after verifyFiber is done
Continuation ∗callerCont =
VerifyCertCFA2
(Continuation∗) vcaCont−>arg1;
FetchCert3 callerCont−>returnValue =
vcaCont−>returnValue;
// “return” to original caller (FetchCert)
(∗callerCont−>function)(callerCont);
scheduler }
function call Manual stack mgmt code
The first adaptor function accepts the arguments
function return Auto stack mgmt code
of the adapted function and a continuation (“stack
fiber switch adaptor code frame”) for the calling task. It constructs its own
I/O message Compute-only function continuation vcaCont and creates a object called
verifyFiber that represents a new fiber (VerifyC-
Figure 2: GetCertData, code with manual stack man- ertFiber is a subclass of the Fiber class); this object
agement, calls VerifyCert, a function written with keeps track of the function arguments and vcaCont
automatic stack management. so that it can transfer control to VerifyCertCFA2
when verifyFiber’s work is done. Finally, it
performs a fiber-switch to verifyFiber. When
verifyFiber begins, it executes glue routine
VerifyCertFiber::FiberStart to unpack the
parameters and pass them to VerifyCert, which may
block on I/O:
VerifyCertFiber::FiberStart() {
MainFiber verifyFiber
// Executed on a fiber other than MainFiber
// The following call could block on I/O. FetchCert1
// Do the actual verification. GetCertData1
this−>vcaCont−>returnValue =
VerifyCert(this−>certData);
// The verification is complete.
// Schedule VerifyCertCFA2
scheduler−>schedule(this−>vcaCont); GetCertData2
SwitchTo(MainFiber); FetchCert2
} Start
VerifyCertCFA
FiberStart
GetCAInfoFCA
VerifyCert
Later, when the I/O completes, verifyFiber is
CACheckCert
resumed (for now, we defer the details on how
VerifyCert
this resumption occurs). After VerifyCert has verifyFiber
Done
performed the last of its work, control returns to FiberStart
FiberContinue
FiberStart. FiberStart stuffs the return value
into VerifyCertCFA2’s continuation, schedules it to
execute, and switches back to the MainFiber a final VerifyCertCFA2
time. At this point, verifyFiber is destroyed. When
FetchCert3
VerifyCertCFA2 executes, it “returns” (with a func-
tion call, as code with manual stack management nor-
mally does) the return value from VerifyCert back
to the adaptor-caller’s continuation, FetchCert3.
scheduler
We now discuss how the code interactions occur when a I/O message Compute-only function
function with automatic stack management calls a func-
tion that manually manages its stack. In this case, the Figure 3: VerifyCert, code with automatic stack
former function needs to block for I/O, but the latter management, calls GetCAInfo, a function written with
function simply schedules the I/O and returns. To recon- manual stack management.
cile these requirements, we supply an adaptor that calls
the manual-stack-management code with a special con-
tinuation and relinquishes control to the MainFiber,
thereby causing the adaptor’s caller to remain blocked. effect, it switches control to the MainFiber.
When the I/O completes, the special continuation runs
on the MainFiber and resumes the fiber of the blocked On the MainFiber, the continuation code that
adaptor, which resumes the original function waiting for started this burst of fiber execution, VerifyCertCFA,
the I/O result. returns several times to unroll its stack and the sched-
uler runs again. Eventually, the I/O result arrives and the
Figure 3 fills in the missing details of Figure 2 to illus- scheduler executes GetCAInfo2, the remaining work
trate this interaction. In this example, VerifyCert of GetCAInfo. GetCAInfo2 fills the local hash ta-
blocks on I/O when it calls GetCAInfo, a function ble (recall its implementation from Section 3.1) and “re-
with manual stack management. VerifyCert calls turns” control by calling a continuation. In this case,
the adaptor GetCAInfoFCA, which hides the manual- it calls the continuation (FiberContinue) that had
stack-management nature of GetCAInfo (FCA means been passed to GetCAInfo.
Fiber-to-Continuation Adaptor):
FiberContinue notices that verifyFiber has
Boolean GetCAInfoFCA(CAID caid) {
indeed been blocked and switches control back to
// Executed on verifyFiber
that fiber, where the bottom half of the adaptor,
// Get a continuation that switches control
GetCAInfoFCA, extracts the return value and passes it
// to this fiber when called on MainFiber
up to the automatic-stack-management code that called
FiberContinuation ∗cont = new
it (VerifyCert).
FiberContinuation(FiberContinue,
this);
The short circuit branch not followed in the example
GetCAInfo(caid, cont);
handles the case where GetCAInfo returns a result
if (!cont−>shortCircuit) {
immediately without waiting for I/O. When it can do
// GetCAInfo did block.
so, it must not allow control to pass to the scheduler.
SwitchTo(MainFiber);
This is necessary so that a caller can optionally deter-
}
mine whether or not a routine has yielded control and
return cont−>returnValue;
hence whether or not local state must be revalidated.
}
Without a short circuit path, this important optimiza-
tion and an associated design pattern that we describe
void FiberContinue(Continuation ∗cont) {
in Section 5 cannot be achieved. Figure 4 illustrates
if (!Fiber::OnMainFiber()) {
the short-circuit sequence: The short-circuit code de-
// Manual stack mgmt code did not perform
tects the case where GetCAInfo runs locally, performs
// I/O: just mark it as short-circuited
no I/O, and executes (“returns to”) the current contin-
FiberContinuation ∗fcont =
uation immediately. FiberContinue detects that it
(FiberContinuation) ∗cont;
was not executed directly by the scheduler, and sets
fcont−>shortCircuit = true;
the shortCircuit flag to prevent the adaptor from
} else {
switching to the MainFiber.
// Resumed after I/O: simply switch
// control to the original fiber
Fiber ∗f = (Fiber ∗) cont−>arg1;
4.3 Discussion
f−>Resume();
}
}
An important observation is that, with adaptors in place,
each style of code is unaware of the other. A function
The adaptor, GetCAInfoFCA, sets up a special con- written with automatic stack management sees what it
tinuation that will later resume verifyFiber via the expects: deep in its stack, control may transfer away,
code in FiberContinue. It then passes this continua- and return later with the stack intact. Likewise, the
tion to GetCAInfo which initiates an I/O operation and event-handler scheduler cannot tell that it is calling any-
returns immediately to what it believes to be the event- thing other than just a series of ordinary manual-stack-
handling scheduler; of course, in this case, the con- management continuations: the adaptors deftly swap the
trol returns to GetCAInfoFCA. Since I/O was sched- fiber stacks around while looking like any other continu-
uled and short-circuiting did not occur (discussed later ation. Thus, integrating code in the two styles is straight-
in this section), GetCAInfoFCA must ensure that con- forward: fiber execution looks like a continuation to the
trol does not yet return to VerifyCert; to achieve this event-driven code, and the continuation scheduler looks
FiberStart another, as with setjmp and longjmp. Since fibers
VerifyCert
are unavailable in the Windows CE operating system,
UCoM uses preemptive threads and condition variables
GetCAInfoFCA to achieve a cooperative threading facility: each thread
blocks on its condition variable and the scheduler en-
GetCAInfo
sures that at most one condition variable is signalled at
FiberContinue any moment. When a thread yields, it blocks on its con-
dition variable and signals the scheduler to continue; the
scheduler selects a ready thread and signals its condition
GetCAInfoFCA
variable.
VerifyCert
We implemented the hybrid adaptors in each direction
with a series of mechanically-generated macros. There
are two groups of macros, one for each direction of adap-
Figure 4: A variation where GetCAInfo does not need tation. Within each group, there are variations to ac-
to perform I/O. count for varying numbers of arguments, void or non-
void return type, and whether the function being called
is a static function or an object method; multiple macros
like any other fiber to the procedure-oriented code. This are necessary to generate the corresponding variations in
adaptability enables automatic-stack-management pro- syntax. Each macro takes as arguments the signature of
grammers to work with manual-stack-management pro- the function being adapted. The macros declare and cre-
grammers, and to evolve a manual-stack-management ate appropriate Fiber and Continuation objects.
code base with automatic-stack-management functions
and vice versa. Our experience with both systems has been positive and
our subjective impression is that we have been able to
preempt many subtle concurrency problems by using co-
5 Implementation Experience operative task management as the basis for our work.
Although the task of wrapping I/O functions (see Sec-
tion 2.1) can be tedious, it can be automated, and we
We have employed cooperative task management in found that paying an up-front cost to reduce subtle race
two systems: Farsite [BDET00], a distributed, se- conditions was a good investment.
cure, serverless file system running over desktops, and
UCoM [SBS02], a wireless phone application for hand- Both systems use extra threads for converting block-
held devices such as PDAs. The Farsite code is designed ing I/O operations to non-blocking operations and for
to run as a daemon process servicing file requests on the scheduling I/O operations, as is done in many other sys-
Windows NT operating system. The UCoM system, de- tems, such as Flash [PDZ99]. Data partitioning prevents
signed for the Windows CE operating system, is a client synchronization problems between the I/O threads and
application that runs with UI and audio support. the state shared by cooperatively-managed tasks.
The Farsite system code was initially written in event- Cooperative task management avoids the concurrency
driven style (cooperative task management and manual problems of locks only if tasks can complete without
stack management) to enable simplified reasoning about having to yield control to any other task. To deal with
the concurrency conditions of the system. As our code tasks that need to perform I/O, we found that we could
base grew and evolved over a period of two years, we often avoid the need for a lock by employing a particular
came to appreciate the costs of employing manual stack design pattern. In this pattern, which we call the Pin-
management and devised the hybrid approach discussed ning Pattern, I/O operations are used to pin resources in
in the previous section to introduce automatic stack man- memory where they can be manipulated without yield-
agement code into our system. The UCoM system uses ing. Note that pinning does not connote exclusivity: a
automatic stack management exclusively. pinned resource is held in memory (to avoid the need
to block on I/O to access it), but when other tasks run,
Farsite uses fibers, the cooperative threading facility they are free to manipulate the data structures it con-
available in Windows NT. With Windows fibers, each tains. Functions are structured in two phases: a loop that
task’s state is represented with a stack, and control is repeatedly tries to execute all potentially-yielding oper-
transferred by simply swapping one stack pointer for ations until they can all be completed without yielding,
and an atomic block that computes results and writes Other system designers have advocated non-threaded
them into the shared state. programming models because they observe that for
a certain class of high-performance systems, such
An important detail of the design pattern is that there as file servers and web servers, substantial per-
may be dependencies among the potentially-yielding op- formance improvements can be obtained by re-
erations. A function may need to compute on the results ducing context switching and carefully implement-
of a previously-pinned resource in order to decide which ing application-specific cache-conscious task schedul-
resource to pin next; for example, in Farsite this occurs ing [HS99, PDZ99, BDM98, MY98]. These factors
when traversing a path in a directory tree. Thus, in the become especially pronounced during high load situa-
fully general version of the design pattern, a check after tions, when the number of threads may become so large
each potentially-yielding operation ascertains whether that the system starts to thrash while trying to give each
the operation did indeed yield, and if so, restarts the loop thread its fair share of the system’s resources. We ar-
from the top. Once the entire loop has executed with- gue that the context-switching overhead for user-level
out interruption, we know that the set of resources we threads (fibers) is in fact quite low; we measured the cost
have pinned in memory are related in the way we ex- of switching in our fiber package to be less than ten times
pect, because the final pass through the loop executed the cost of a procedure call. Furthermore, application-
atomically. specific cache-conscious task scheduling should be just
as achievable with cooperative task management and au-
tomatic stack management: the scheduler is given pre-
6 Related Work cisely the same opportunities to schedule as in event-
driven code; the only difference is whether stack state is
kept on stacks or in chains of continuations on the heap.
Birrell offers a good overview of the conventional
thread-ed programming model with preemptive task For the classes of applications we reference here, pro-
management [Bir89]. Of his reasons for using concur- cessing is often partitioned into stages [WCB01, LP01].
rency (p. 2), cooperative task management can help with The partitioning of system state into disjoint stages is a
all but exploiting multiprocessors, a shortcoming we form of data partitioning, which addresses concurrency
mention in Section 2.1. Birrell advises that “you must at the coarse grain. Within each stage, the designer
be fastidious about associating each piece of data with of such a system must still choose a form of conflict
one (and only one) mutex” (p. 28); consider coopera- management, task management, and stack management.
tive task management as the limiting case of that advice. Careful construction of stages avoids I/O calls within a
There is the complexity that whenever a task yields it ef- stage; in that case, cooperative task management within
fectively releases the global mutex, and must reestablish the stage degenerates to serial task management, and no
its invariants when it resumes. But even under preemp- distinction arises in stack management. In practice, at
tive task management, Birrell comments that “you might the inter-stage level, a single task strings through mul-
want to unlock the mutex before calling down to a lower tiple stages, and reads as in manual stack management.
level abstraction that will block or execute for a long Typically, the stages are monotonic: once a task leaves
time” (p. 12); hence this complexity is not introduced by a stage, it never returns. This at least avoids the ripping
the choice of cooperative task management. associated with looping control structures.
Ousterhout points out the pitfalls of preemptive task Lauer and Needham show two programming models
management, such as subtle race conditions and dead- to be equivalent up to syntactic substitution [LN79].
locks [Ous96]. We argue that his “threaded” model con- We describe their models in terms of our axes: their
flates preemptive task management with automatic stack procedure-oriented system has preemptive task manage-
management, and his “event-driven” model conflates co- ment, automatic stack management (“a process typically
operative task management with manual stack manage- has only one goal or task”), monitors for conflict man-
ment. We wish to convince designers that the choices agement, and one big data partition protected by those
are orthogonal, that Ousterhout’s arguments are really monitors. Their message-oriented system has manual
about the task management decision, and that program- stack management with task state passed around in mes-
mers should exploit the ease-of-reasoning benefits of co- sages, and no conflicts to manage due to many partitions
operative task management while exploiting the features of the state so that it is effectively never concurrently
of their programming language by using automatic stack shared.
management.
Notably, of the message-oriented system, they say “nei-
ther procedural interfaces nor global naming schemes References
are very useful,” that is, the manual stack management
undermines structural features of the language. Neither [BDET00] William Bolosky, John Douceur, David Ely,
model uses cooperative task management as we regard and Marvin Theimer. Feasibility of a server-
it, since both models require identically-detailed reason- less distributed file system deployed on an
ing about conflict management. Thus their comparison existing set of desktop pcs. In Proceed-
is decidedly not between the models we associate with ings of the ACM Sigmetrics 2000 Confer-
multithreaded and event-driven programming. ence, pages 34–43, June 2000.