×
Namespaces

Variants
Actions

Fundamentals of Symbian C++/Active Objects

From Nokia Developer Wiki
Jump to: navigation, search
Article Metadata
Article
Created: [[User: ]] (15 Jan 2001)
Last edited: hamishwillee (23 Jul 2012)

Active objects are used for event-driven multi-tasking in Symbian C++, and are fundamental to responsive and efficient event handling.

Most developers coming to Symbian C++ with previous real-time programming experience automatically assume they should be using threads in Symbian C++ for multitasking activities. However, this is usually not the case. Unless there is a definite need for pre-emptive multitasking functionality, active objects should be preferred.

Threads are pre-emptively multitasked, while active objects are not; active objects were specifically designed as a more efficient alternative to threads (for example, they eliminate the need for thread context switches to occur) when pre-emptive behavior is not essential. Section Threads, Processes and IPC discusses the RThread API.

This section describes what active objects are, how to use them and how to avoid common associated programming errors.

Contents

The Active Object Framework

The active object framework is used in Symbian C++ to simplify asynchronous programming. It is used to handle multiple asynchronous tasks in the same thread and provides a consistent way to write code to submit asynchronous requests and handle completion events.

Synchronous Versus Asynchronous Code

A synchronous function performs the service it offers immediately and only then completes, returning control to the caller.

An asynchronous function, however, immediately returns control to the caller and the requested service is performed some time later, at which point the caller is sent a signal that the service has completed. This signal is known as an event, and the code can be said to be event-driven. While the caller waits for the completion event, it can perform other processing if necessary, and so be responsive to the user, or it can wait in a low-power state. Events are managed by an event handler, which, as its name suggests, waits for an event and then handles it, and may resubmit a request for the asynchronous service. An operating system must have an efficient event-handling model to respond to an event as soon as possible after it occurs. It is also important that, if more than one event occurs simultaneously, the response occurs in the most appropriate order (user-driven events should be handled rapidly for a good user experience).

An active object encapsulates a task; it requests an asynchronous service from a service provider and handles the completion event later when the active scheduler calls it.

The active scheduler determines which active object is associated with the event and calls the appropriate active object to handle the event.

Pre-Emption

Within a single thread, the active object framework uses non-pre-emptive multitasking. Once invoked, an event handler must run to completion before any other active object’s event handler can run – it cannot be pre-empted.

Some events strictly require a response within a guaranteed time, regardless of any other activity in the system (for example, low-level telephony or video processing). This is called real-time event handling. Active objects are not suitable for real-time tasks and on Symbian C++ real-time tasks should be implemented using high-priority threads. Symbian C++ threads are scheduled pre-emptively by the kernel, which runs the highest-priority thread eligible. The kernel controls thread scheduling, allowing the threads to share system resources by time-slice division, pre-empting the running of a thread if another, higher-priority thread becomes eligible to run.

A context switch occurs when the current thread is suspended (for example, if it becomes blocked, has reached the end of its time-slice, or a higher priority thread becomes ready to run) and another thread is made current by the kernel scheduler. The context switch incurs a runtime overhead in terms of the kernel scheduler and, if the original and replacing threads are executing in different processes, the memory management unit and hardware caches.

In Symbian C++, active objects are classes that are derived from the CActive base class. Their management and scheduling is handed by the CActiveScheduler class.

The CActive Class

An active object class must derive directly or indirectly from class CActive, defined in e32base.h. CActive is an abstract class with two pure virtual functions, RunL() and DoCancel().

Construction

On construction, classes deriving from CActive must call the protected constructor of the base class, passing in a parameter to set the priority of the active object. Like threads, all active objects have a priority value to determine how they are scheduled. The priority value is only used to determine the order in which event handlers are run, not to reschedule them.

A set of priority values is defined in the TPriority enumeration of class CActive. In general, the priority value CActive::EPriorityStandard (=0) should be used unless there is good reason to do otherwise.

In its constructor, the active object should also add itself to the active scheduler by calling CActiveScheduler::Add().

Making a Request

An active object should supply a public method to make a request on the asynchronous service provider and set itself as active (so the Active Scheduler will wait on its completion). The implementation of this method follows a standard pattern.

1. Check for outstanding requests.

An active object must never have more than one outstanding request, so before attempting to submit a request, the active object must check to see if it is already waiting on completion. If it is, there are various ways to proceed:

  • Panic – if this scenario could only occur because of a programming error.
  • Refuse to submit another request – if it is legitimate to attempt to make more than one request but that successive requests should fail until the original one completes.
  • Cancel the outstanding request and submit the new one – if it is legitimate to attempt to make more than one request and successive requests supplant earlier pending requests. Cancelation of outstanding asynchronous requests is discussed in more detail in the following section.

2. Submit the request.

The active object submits a request to the service provider, passing in the TRequestStatus member variable (iStatus). The service provider must set this value to KRequestPending before initiating the asynchronous request.

3. Call SetActive() to mark the object as waiting.

A call to CActive::SetActive() indicates that a request has been submitted and is currently outstanding. This call should not be made until after the request has been submitted to the service provider.

Handling Request Completion

Each active object class must implement the pure virtual RunL() method inherited from the CActive base class. This is the event-handler method called by the active scheduler when a completion event occurs.

RunL() should check whether the asynchronous request succeeded by inspecting its completion code, which is the 32-bit integer value store in the TRequestStatus object (iStatus) of the active object. Depending on the result, RunL() may submit another request or perform other processing such as writing to a log file. The complexity of RunL() code can vary considerably.

Since RunL() cannot be pre-empted by other active objects’ event handlers while it is running, it should complete as quickly as possible so that other events can be handled without delay.

The CActive base class provides a virtual RunError() method which the active scheduler calls if RunL() leaves. If the leave can be handled, this should be done by overriding the default implementation of CActive::RunError() to handle the exception.

The figure below illustrates the basic sequence of actions performed when an active object submits a request to an asynchronous service provider.

A request to an asynchronous service provider, which generates an event on completion

Canceling an Active Object

An active object must be able to cancel an outstanding asynchronous request. An active object class must implement the pure virtual DoCancel() method of the base class to terminate a request by calling the appropriate cancelation method on the asynchronous service provider. DoCancel() must not leave or allocate resources and should not carry out any lengthy operations, but simply cancel the request and perform any associated cleanup.

CActive::Cancel() calls DoCancel() and waits for notification that the request has terminated. Cancel() must be called whenever a request it to be terminated, not DoCancel(), since the base class method checks whether a request is outstanding and performs the necessary wait until it has terminated.

Destruction

The destructor of a CActive-derived class should always call Cancel() to cancel any outstanding requests. The CActive base-class destructor checks that the active object is not currently active. It panics with E32USER-CBASE 40 if any request is outstanding, that is, if Cancel() has not been called. This catches any programming errors where a call to Cancel() has been forgotten. Having verified that the active object has no issued requests outstanding, the CActive destructor removes the active object from the active scheduler.

The reason it is so important to cancel requests before destroying an active object is that otherwise the request would complete after the active object had been destroyed. This would cause a stray signal because the active scheduler is unable to find a handler for the event. This results in a panic (E32USER-CBASE 46). Other reasons for receiving a stray signal are described later.

An Active Object Example: File System Monitoring

The following example illustrates the use of an active object class to wrap an asynchronous service: a file system change monitor that generates a completion event when the file system location specified is modified (for example, by addition of a file, modification or deletion).

An asynchronous request is submitted by StartFilesystemMonitor(), which first checks if there is an outstanding request, and then makes a call to the RFs::NotifyChange() method, the documentation for which you can find in the Symbian Developer Library. Fundamentals of Symbian C++/File Server discusses the basics of the Symbian C++ file system in more detail.

When a change occurs in the specified directory, the file system completes the NotifyChange() request, which in turn causes the RunL() event handler of class CNotifyChange to be invoked.

RunL() checks the active object’s iStatus result and leaves if it contains a value other than KErrNone, so that the RunError() method can handle the problem. In this case, the error handling is very simple: the error returned from the request is logged to debug output. This could have been performed in the RunL() method, but has been separated into the RunError() method to demonstrate how to use the active object framework to split error handling from the main logic of the event handler.

If no error occurred, the RunL() event handler resubmits the RFs::NotifyChange() request without further ado, to ensure no future changes to the file system are missed. In effect, once the initial StartFilesystemMonitor() request has been submitted, it continues monitoring until it is stopped by an error or a call is made to Cancel().

class CNotifyChange : public CActive
{
 
public:
 
~CNotifyChange();
static CNotifyChange* NewL(const TDesC& aPath);
void StartFilesystemMonitor();
 
protected:
 
CNotifyChange();
void ConstructL(const TDesC& aPath);
 
protected:
 
virtual void RunL(); // Inherited from CActive.
 
virtual void DoCancel();
 
virtual TInt RunError(TInt aError);
 
private:
 
RFs iFs;
HBufC* iPath;
};
 
CNotifyChange::CNotifyChange() : CActive(EPriorityStandard)
{ CActiveScheduler::Add(this); }
 
void CNotifyChange::ConstructL(const TDesC& aPath)
{ // Open a file server session.
 
User::LeaveIfError(iFs.Connect());
iPath = aPath.AllocL();
}
 
CNotifyChange* CNotifyChange::NewL(const TDesC& aPath)
{} // Standard two-phase construction code omitted for clarity.
 
CNotifyChange::~CNotifyChange()
{// Stop any outstanding requests.
 
Cancel();
delete iPath;
iFs.Close();
}
 
void CNotifyChange::StartFilesystemMonitor()
{ // Only allow one request to be submitted at a time.
 
// Caller must call Cancel() before submitting another.
 
if (IsActive())
{
_LIT(KAOExamplePanic, "CNotifyChange");
User::Panic(KAOExamplePanic, KAOExamplePanicCode);
}
 
iFs.NotifyChange(ENotifyAll, iStatus, *iPath);
 
SetActive(); // Mark this object active.
}
 
// Event handler method.
 
void CNotifyChange::RunL()
{
 
// If an error occurred handle it in RunError().
User::LeaveIfError(iStatus.Int());
 
// Resubmit the request immediately so as not
// to miss any future changes.
 
iFs.NotifyChange(ENotifyAll, iStatus, *iPath);
 
SetActive();
 
// Now process the event as required.
 
...
 
}
 
void CNotifyChange::DoCancel()
{ // Cancel the outstanding file system request.
iFs.NotifyChangeCancel(iStatus);
}
 
TInt CNotifyChange::RunError(TInt aError)
{ // Called if RunL() leaves, aError contains the leave code.
_LIT(KErrorLog, "CNotifyChange::RunError %d");
RDebug::Print(KErrorLog, aError); // Logs the error.
return (KErrNone); // Error has been handled.
}

The file system monitor is a useful illustration of a request that does not complete in a deterministic time, and must be asynchronous (that is – there is no way otherwise to know when a change will occur in the location being watched). It is useful for applications that, for example, maintain a display of the files in a particular directory. If active objects were not used, the application would have to perform periodic polling on the file system to get the contents of the directory and compare it against the previous listing (which is inefficient and could unduly affect battery life if the polling were too frequent). This is a good illustration of the utility of a lightweight active object implementation, which avoids the need for polling, and only executes code when an event (in this case a file system change) occurs.

It is also clear that the amount of code required is minimal, and easy to refactor for similar monitoring code, thus promoting code re-use. It is simple to understand and maintain, and is easier than creating a separate monitor thread to watch the location. The application and the active object code all run in a single thread, so synchronization and being able to re-enter are not issues that need to be considered. You can create as many of these monitors as you want on a thread, each watching a different location – all without resorting to multithreading programming. Each RunL() would be processed one at a time and so there are no re-entrance issues or synchronization issues.

The Active Scheduler

Most threads running on Symbian C++ have an active scheduler that is usually created and started implicitly by a framework (for example CONE for the GUI framework). There is only one active scheduler created per thread, although it can be nested in advanced cases.

Console-based test code must create an active scheduler in its main thread if it depends on components, which use active objects. The code to do this is as follows:

CActiveScheduler* scheduler = new(ELeave) CActiveScheduler;
 
CleanupStack::PushL(scheduler);
 
CActiveScheduler::Install(scheduler);

Once the active scheduler has been created and installed, its event-processing wait loop must be started by calling CActiveScheduler::Start(). The event-processing loop starts and does not return until a call is made to CActiveScheduler::Stop(). For implementation reasons, there must be at least one asynchronous request issued before the loop starts, otherwise the thread simply enters the wait loop indefinitely. Let’s look at why this happens in more detail by examining the active scheduler wait loop.

When it is not handling other completion events, the active scheduler suspends a thread by calling User::WaitForAnyRequest(), which waits for a signal to the thread’s request semaphore. If no events are outstanding in the system, the operating system can power down to sleep.

When an asynchronous server has finished with a request, it indicates completion by calling User::RequestComplete() (if the service provider and requestor are in the same thread) or RThread::RequestComplete(). The TRequestStatus associated with the request is passed into the RequestComplete() method along with a completion result, typically one of the standard error codes. The RequestComplete() method sets the value of TRequestStatus to the given error code and generates a completion event in the requesting thread by signaling a semaphore.

When a signal is received and the thread is next scheduled, the active scheduler determines which active object should handle it. It checks the priority-ordered list of active objects for those with outstanding requests (these have their iActive Boolean set to ETrue as a result of calling CActive::SetActive()). If an object has an outstanding request, the active scheduler checks its iStatus member variable to see if it is set to a value other than KRequestPending. If so, this indicates that the active object is associated with the completion event and that the event handler code should be called. The active scheduler clears the active object’s iActive Boolean and calls its RunL() event handler.

Once the RunL() call has finished, the active scheduler re-enters the event processing wait loop by issuing another User::WaitForAnyRequest() call. This checks the thread’s request semaphore and either suspends it (if no other events need handling) or returns immediately to lookup and event handling.

The following pseudo-code illustrates the event-processing loop.

EventProcessingLoop()
{
// Suspend the thread until an event occurs.
 
User::WaitForAnyRequest();
 
// Thread wakes when the request semaphore is signaled.
// Inspect each active object added to the scheduler,
// in order of decreasing priority.
// Call the event handler of the first which is active and completed.
 
FOREVER
{
// Get the next active object in the priority queue.
if (activeObject->IsActive())&&
(activeObject->iStatus!=KRequestPending)
 
{ // Found an active object ready to handle an event.
 
// Reset the iActive status to indicate it is not active.
activeObject->iActive = EFalse;
 
// Call the active object’s event handler in a TRAP.
 
TRAPD(r, activeObject->RunL());
 
if (KErrNone!=r)
{ // Event handler left, call RunError() on active object.
r = activeObject->RunError();
if (KErrNone!=r) // RunError() didn’t handle the error,
Error(r); // call CActiveScheduler::Error().
 
}
 
break; // Event handled, break out of lookup loop and resume.
 
}
 
} // End of FOREVER loop
 
}

Once an active object is handling an event, it cannot be pre-empted until the event-handler function has returned back to the active scheduler. During this time, a number of completion events may occur. When the active scheduler next gets to run, it must resolve which active object gets to run next. It would not be desirable for a low-priority active object to handle its event if a higher-priority active object was also waiting, so events are handled sequentially in order of the highest priority rather than in order of completion. Note that if an event associated with a high-priority active object occurs while a lower-priority active object handler is executing, no pre-emption occurs.

Common Problems with Active Objects

Stray Signal Panics

The most common problem when writing active objects is when you encounter a stray signal panic (E32USER-CBASE 46). These occur when an active scheduler receives a completion event but cannot find an active object to handle it. Stray signals can occur because:

  • CActiveScheduler::Add() was not called when the active object was constructed.
  • SetActive() was not called after submitting a request to an asynchronous service provider.
  • The asynchronous service provider completed the request more than once.

If you receive a stray signal panic, the first thing to do is work out which active object is responsible for submitting the request that later generated the stray event. One of the best ways to do this is to use file logging in every active object and, if necessary, eliminate them from your code one by one until the culprit is tracked down.

Unresponsive UI

In an application thread, in particular, event-handler methods must be kept short to allow the UI to remain responsive to the user. No single active object should have a monopoly on the active scheduler because that prevents other active objects from handling events. Active objects must co-operate and should not:

  • Have lengthy RunL() or DoCancel() methods
  • Repeatedly resubmit requests that complete rapidly, particularly if the active object has a high priority, because the event handler will be invoked at the expense of lower-priority active objects waiting to be handled
  • Have a higher priority than is necessary.

Other causes of an unresponsive UI are temporary or permanent thread blocks as a result of:

  • A call to User::After(), which stops the thread executing for the length of time specified
  • Incorrect use of the active scheduler. There must be at least one asynchronous request issued, via an active object, before the active scheduler starts. If no request is outstanding, the thread simply enters the wait loop and sleeps indefinitely.
  • Incorrect use of User::WaitForRequest() to wait on an asynchronous request, rather than correct use of the active object framework.

Background Tasks

Active objects can also be used to implement tasks that can run in chunks at low priority in the background. This avoids the need to create a separate thread. The task is divided into multiple increments, for example performing background recalculations. The increments are performed in the event handler of a low-priority active object, and must be must be kept short, since RunL() cannot be pre-empted once it is running.

The active object must be assigned a very low priority such as CActive::TPriority::EPriorityIdle (=-100), to ensure that the task only runs when there are no essential events, such as user input, to handle.

Background Task Handling Example

The following simple example code illustrates a basic background task (only the relevant code is shown; two-phase construction, destruction and most error handling are omitted for simplicity).

A basic T class performs the background task, in this case some kind of calculation (for clarity, this code is omitted). The class provides an API to start the task, perform the various discrete task steps using a state machine to track the stages and a cancelation method to stop the calculation.

The active object class, CBackgroundTask, drives the task by generating events in SelfComplete() to invoke the RunL() event handler when the active scheduler has no higher-priority active object to run. It does this by calling User::RequestComplete() on its own iStatus object.

In the event handler, CBackgroundTask checks whether there are any further steps to run by calling TLongRunningCalculation::ContinueTask(). If there are, it performs a step and sends another self-completion event, continuing to do so until the task is complete. When the calculation is complete, the caller is notified by generating a completion event.

class TLongRunningCalculation
{
public:
TLongRunningCalculation() {iState=EWaiting;};
void StartCalculation(); // Initialization before starting the task.
void DoTaskStep(); // Perform a short task step.
TBool ContinueTask(); // Returns ETrue if more left to do.
void CancelCalculation() {iState=EWaiting;};
 
private:
enum TCalcState
{ EWaiting, EBeginState, EIntermediateState, EFinalState };
 
TCalcState iState; // Keeps track of the current calculation state.
};
 
_LIT(KExPanic, "Example Panic");
 
void TLongRunningCalculation::StartCalculation()
{
 
// Flag a programming error if it's already running.
__ASSERT_DEBUG(iState==EWaiting, User::Panic(KExPanic, KExCode));
 
iState=EBeginState;
}
 
// State machine.
 
void TLongRunningCalculation::DoTaskStep()
{
// Do a short task step.
 
switch (iState)
{
case (EWaiting):
iState = EBeginState; break;
case (EBeginState):
iState = EIntermediateState; break;
case (EIntermediateState):
iState = EFinalState; break;
case (EFinalState):
iState = EWaiting; // Finished
break;
default:
ASSERT(EFalse); // Cause a panic! Should never get here.
}
}
 
// Return ETrue if there are further steps to run.
TBool TLongRunningCalculation::ContinueTask()
{
return (iState!=EWaiting);
}
 
class CBackgroundTask : public CActive
{
public:
// Public method to kick off a background calculation.
void PerformCalculation(MCallback* aCompletionCallback);
 
protected:
CBackgroundTask();
void SelfComplete();
virtual void RunL();
virtual void DoCancel();
 
private:
TLongRunningCalculation iCalc;
TBool iMoreToDo;
 
TRequestStatus* iCallerStatus; // To notify caller on completion.
};
 
CBackgroundTask::CBackgroundTask()
: CActive(EPriorityIdle) // Low-priority task
{ CActiveScheduler::Add(this); }
 
// Issue a request to initiate a lengthy task.
void CBackgroundTask::PerformCalculation(TRequestStatus& aStatus)
{
// Save the parameter to notify when complete.
iCallerStatus = &aStatus;
*iCallerStatus = KRequestPending;
 
__ASSERT_DEBUG(!IsActive(), User::Panic(KExPanic, KErrInUse));
iCalc.StartCalculation(); // Start the task.
SelfComplete(); // Self-completion to generate an event.
}
 
void CBackgroundTask::SelfComplete()
{
// Generate an event by completing on iStatus.
TRequestStatus* status = &iStatus;
User::RequestComplete(status, KErrNone);
SetActive();
}
 
// Perform the background task in increments.
 
void CBackgroundTask::RunL()
{
// Resubmit request for next increment of the task or stop.
if(iCalc.ContinueTask())
{
iCalc.DoTaskStep();
SelfComplete();
}
else
User::RequestComplete(iCallerStatus, iStatus.Int());
}
 
void CBackgroundTask::DoCancel()
{
// Call iCalc to cancel the task.
iCalc.CancelCalculation();
 
// Notify the caller of completion (through cancelation).
User::RequestComplete(iCallerStatus, KErrCancel);
}

Note that there is a problem with running two or more background tasks in the same thread – because the active scheduler looks for the first eligible active object in the order they were added to the scheduler (and starts at the top of the list each time). The first background task added to the scheduler will run to completion, starting any other active objects with the same or lower priority.

The solution to this is that background active objects should call Deque() on themselves, and then CActiveScheduler::Add() to add themselves to the end of the scheduler’s list. If all background active objects do this then there is a co-operative round-robin effect.


Licence icon cc-by-sa 3.0-88x31.png© 2010 Symbian Foundation Limited. This document is licensed under the Creative Commons Attribution-Share Alike 2.0 license. See http://creativecommons.org/licenses/by-sa/2.0/legalcode for the full terms of the license.
Note that this content was originally hosted on the Symbian Foundation developer wiki.

This page was last modified on 23 July 2012, at 10:50.
89 page views in the last 30 days.
×