Essential Windows Workflow Foundation: Activity Execution
Because a WF program is just an activity (that typically is the root of a tree of activities), the best way to understand how large WF programs execute is to first understand what happens at the level of a single activity.
The WF programming model codifies the lifecycle of an activity in terms of a finite state machine that we will call the activity automaton. Chapter 1, "Deconstructing WF," introduced a simple three-state version of this automaton for resumable program statements. The lifecycle of activities also follows this basic pattern but adds several additional states that will be discussed in this chapter and the next.
The execution model for activities is fundamentally asynchronous because it is designed to accommodate activities that perform episodic execution—short bursts of execution punctuated by relatively long periods of time spent waiting for external stimulus.
For efficiency reasons, it does not make sense to keep WF program instances in memory while they are idle waiting for data to arrive. When a WF program instance becomes idle, the WF runtime is capable of storing it in a (pluggable) durable storage medium and disposing the program instance. The process of storing the instance state and disposing the instance from memory is called passivation. When relevant stimulus arrives from an external entity, perhaps after days spent waiting, the WF runtime automatically reactivates the program, bringing it out of durable storage (where it had been passivated) into memory, and resuming its execution. Relative to its logical lifetime, a typical WF program instance lives for a short duration in memory.
Supporting passivation requires serialization of not only the program state but also the execution state (managed by the WF runtime). When WF program instances are passivated, they are captured by the WF runtime as continuations. A passivated program instance may be resumed in a different process or even on a different machine than the one on which it ran prior to passivation. This means that WF programs are thread-agile and process-agile. WF programs do indeed run on CLR threads, but the execution model for activities across resumption points is stackless because it does not rely on the stack associated with a CLR thread. The lifecycle of a WF program instance may, in a physical sense, span processes and machines and is distinctly different from the lifetimes of CLR objects (of type Activity) that transiently represent such a program instance while it is in memory.
In general, when an activity executes, it quickly performs some work and then either reports its completion or (having established one or more bookmarks) yields and waits for stimulus. This pattern maps nicely to a conceptual model in which work items are queued and then dispatched, one at a time, each to a target activity.
This pattern, depicted in Figure 3.1, is generally known as scheduling, so the component of the WF runtime that encompasses this functionality is known as the scheduler. The scheduler dispatches work items one at a time (from a queue), in a first-in-first-out (FIFO) fashion. Additionally, because the WF runtime never intervenes in the processing of a work item that has been dispatched, the scheduler behavior is strictly nonpreemptive.
Figure 3.1 WF scheduler
To distinguish the scheduler's internal queue of work items from WF program queues (which are explicitly created by activity execution logic), we will call the queue that holds scheduler work items the scheduler work queue. When its scheduler work queue is empty, a WF program instance is considered idle.
Scheduler Work Items
The work items in the scheduler work queue are delegates. Each work item (delegate) corresponds to a method on an activity in the WF program instance. The activity method that is indicated by a work item (delegate) in the scheduler work queue is known as the execution handler of that work item.
Although there is no API to directly manipulate (or view) the scheduler work queue, certain actions taken by activities will cause work items to be enqueued. Delivery of input to a WF program queue can also cause work items to be enqueued.
A given activity's execution may include the invocation of any number of execution handlers. The state on which execution handlers operate is preserved across invocations of execution handlers of the same activity. This state is heap-allocated and is managed independently of the stack that is associated with the currently running CLR thread. The execution of activities across resumption points is stackless.
Scheduling of work items (delegates) is the mechanism by which methods on activities are invoked. This simple machinery drives activity, and WF program, execution. But in order to understand the rules about how and when work items are enqueued, we must understand the lifecycle of an activity, and that is our next topic.