Abstract:
BREW is an event-based execution environment with no support for multithreading and where long tasks are specifically discouraged. BREW offers native mechanisms to overcome the problems usually associated with the lack of these features, but implementing and maintaining them in non-trivial code is complicated.
This article introduces a possible implementation of cooperative multitasking in the BREW environment. This is actually a framework that allows fast and easy development of concurrent tasks. The debut of the article reviews the main functionality offered by BREW in this respect with a presentation of the framework following. A link is included at the end of the article for you to download the full source code.
BREW “Native” Ways
BREW can handle one thread of execution and one applet at a time. This means that running a long task might block access to the event loop and result in a non-responsive user interface (UI). Because of this, most devices running a RTOS have a watchdog that checks the threads periodically. On BREW enabled Qualcomm devices (running REX RTOS), if an application takes too much of the CPU and does not yield so that another task on the phone can be executed, the phone will be reset. BREW provides a callback mechanism used to segment a long task in several small ones to prevent this reset. Control can be relinquished between these tiny execution sequences to the event loop.
To implement this functionality a callback is registered with the shell, like:
Step 1. Filling the AEECalback structure cb:
cb.pfnCancel = (void *)NULL; //updated by shell cb.pfnNotify = ResumeNotifyCB; // address of the // callback function cb.pNotifyData = (void *)pMe; //data to pass
There is a helper function that might be used for this purpose:
void CALLBACK_Init( AEECallback * pcb, PFNNOTIFY pfn, void * pd);
Step 2: Invoke ISHELL_Resume.
This will pass control to the callback function when the event loop is called next time, adding the callback to a list of pending operations. If the callback has already been registered, it is cancelled (deregistered) and then re-registered.
ISHELL_Resume (pMe->a.m_pIShell, &cb);
Additional functionality:
If a callback has already been registered but not executed, it can be cancelled by:
if(cb.pfnCancel) cb.pfnCancel(&cb);
or using the helper function:
void CALLBACK_Cancel(AEECallback * pcb);
Using this functionality allows cooperative multitasking, like:
static void Task0 (void *pi) { CIShellApp *pMe = (CIShellApp *)pi; pMe->callBack0_.pfnCancel = (void *)NULL; pMe->callBack0_.pfnNotify = Task0; pMe->callBack0_.pNotifyData = (void *)pMe; executeTask0JobAtStep (pMe->step_); //task related functionality pMe->step_++; ISHELL_Resume (pMe->a.m_pIShell, &pMe->callBack_); } //other tasks executeTaskNJobAtStep (pMe->step_);
Several tasks can execute small pieces of their entire job and yield to each other, synchronizing their work or not.
As the above example demonstrates, Tasks implementation involves a fairly cumbersome and error prone infrastructure to be handled — creating tasks, deleting tasks, keeping track of them, the actual implementation, etc. This is without even considering adding timer and/or notify/wait support.
A simple framework saves the day.
A simple framework might help in this respect. In fact, considering that threading may lead to poor performance due to context switching, synchronization, and data movement a callback based solution might be attractive even on multithreaded platforms. The framework presented here is a simplified version – for a full fledged version please contact Epicad
The main actors of this implementation are:
- Dispatcher – registers Tasks and manipulates them via callbacks.
- Callback – Callbacks are paired with Tasks and implement the platform dependent operations (context). Completely transparent for the user.
- Task – Tasks must be implemented by the developer, and contain the application specific logic. Task implements ITask and actually all the Task management in the framework is done through this interface.
Interactions:
- A Dispatcher registers all the Tasks and creates one Callback for every Task
- Dispatcher does all the operations with Tasks (start, startTask, stop, stopTask as implemented in this simple version).
- Callbacks, once started, support the interaction of the Tasks with the OS. They notify Dispatcher of all the events that might be of interest for the community of Tasks (for example once a Task terminates a free position in the Task list is created, etc.)
- Tasks are loosely coupled with the rest of the framework. Usually a Task is a state machine keeping track of its own state and implementing the needed application logic.
Usage scenarios:- Developer implements a set of tasks.
- A Dispatcher is created.
- Tasks are registered with the Dispatcher (internally Callbacks are created and associated with each Task)
- Dispatcher is instructed to start the activity of Tasks.
- Tasks run.
- Tasks might be stopped/started individually or in block from external code via Dispatcher (as opposed from inside Tasks themselves)
- Dispatcher is deleted (this cleans the stack of Tasks too and all the associated Callbacks)
We’ll look into each one of these activities and see how they are reflected in the current framework.
a. Developer implements a set of tasks.
Every task implements ITask:
class ITask { public: virtual EXEC_STATUS execute() = 0; virtual ~ITask(){}; };
where EXEC_STATUS is a simple enumeration of 2 states:
const static enum EXEC_STATUS {CONTINUE, STOP};
A possible trivial Task might look like:
AsyncTask2::AsyncTask2( IShell* shell, int lineNo) : pos_(0), lineNo_(lineNo) { writer_ = new Writer(shell); } EXEC_STATUS AsyncTask2::execute() { pos_ += 10; if (pos_ < 30001) { doStepJob(); writer_->writeIntAtLine(pos_, lineNo_); // signal intention to continue activity // requests to be registered for a new task slice return CONTINUE; } // requests termination of current task return STOP; } void doStepJob() { //do real stuff here. } AsyncTask2::~AsyncTask2() { delete writer_; };
Writer is a BREW dependent object that offers printing services and pos_ is the state variable. What does POS stand for? POS stands for Position.
Implementing execute() is straightforward. The task is segmented in slices governed by pos_ and the task is run as long as pos_ doesn’t exceed a certain value.
b. A Dispatcher is created:
dispatcher_ = new Dispatcher(m_pIShell);
Create the Dispatcher in the global InitAppData and pass it the IShell reference, used everywhere for BREW specific duties.
c.Tasks are registered with teh Dispatcher
dispatcher_->registerTask(new AsyncTask1(m_pIShell)) ; //other tasks registered.
d. Dispatcher is instructed to start the activity of Tasks
dispatcher_->start();
e/f. Task Run and Tasks may be stopped/started…
dispatcher->stop();
Stop and Start would normally be implemented in EVT_KEY event. Interrupts all tasks associated with the dispatcher_ if a key event is received (a key was hit)
g. The Dispatcher is deleted
delete dispatcher_;
Deleted in the global FreeAppData.
Inside the framework
class Dispatcher { public: Dispatcher(IShell* shell); int registerTask(ITask* task); void start(); void startTask(int i) ; void stop(); void stopTask(int caller); ~Dispatcher(); private: void loop(doCurrentIndexJob job); void initTask(int i); static boolean isOutOfBoundaries(int pos); int addTask(ITask* task, int position); private: Dispatcher(); Dispatcher(const Dispatcher&); Dispatcher& operator=(const Dispatcher&); private: IShell* shell_; CallbackHandle* cbkHolder_[POS_LIMIT]; int nextAvailablePosition_; };
Please note that in order to maintain the simplicity of this example, cbkHolder_ was implemented rigidly as an array rather than as a more flexible container. As mentioned earlier operations like start/stop are general level operations, affecting all the tasks and are based on startTask/stopTask. For example:
void Dispatcher::startTask(int i) { if (cbkHolder_[i]) cbkHolder_[i]->start(); } void Dispatcher::start() { loop(startTask); }
where loop iterates using functions of type:
typedef void (Dispatcher::*doCurrentIndexJob)(int pos);
Registering a task needs some precautions:
int Dispatcher::registerTask(ITask* task) { int err = addTask(task, nextAvailablePosition_); if (err == SUCCESS) ++nextAvailablePosition_; return err; }
nextAvailablePosition is an internal counter keeping the next slot available for a new Callback. It cannot be incremented as long as addTask fails.
int Dispatcher::addTask(ITask* task, int position) { if (!task) return EFAILED; if ( isOutOfBoundaries(position) || cbkHolder_[position] ) { delete task; return EFAILED; } cbkHolder_[position] = new CallbackHandle(shell_, task, this, position); if (!cbkHolder_[position] ) return EFAILED; return SUCCESS; }
addTask might fail if:
- a task was not properly initialized
- the position exceeds the allowed boundaries
- there is already a callback active at the specified position.
In all these cases the task is deleted and a BREW error code is returned.
The most interesting part is obviously the Callback implementation:
class CallbackHandle { public: CallbackHandle( IShell* shell, ITask* task, Dispatcher* d, int position) : shell_( shell), task_(task), dispatcher_(d), position_(position) { } virtual ~CallbackHandle() { release(); delete task_; task_ = 0; } void start() { CALLBACK_Init(&cbk_, executeStatic, this); executeResume(this); } private: static void executeStatic(CallbackHandle* cb) { ITask* t = cb->task_; if (!t) return; Dispatcher* d = cb->dispatcher_; switch (t->execute()) { case CONTINUE: executeResume(cb); return; case STOP: d->stopTask(cb->position_); return; default: d->stopTask(cb->position_); return; } } static void executeResume(CallbackHandle* cb) { ISHELL_Resume (cb->shell_, &cb->cbk_); } void release() { CALLBACK_Cancel(&cbk_); } private: CallbackHandle(); CallbackHandle(const CallbackHandle&); CallbackHandle& operator=(const CallbackHandle&); private: ITask* task_; IShell* shell_; AEECallback cbk_; Dispatcher* dispatcher_; int position_; };
Internally a Callback maintains its position in the callback stack as well as references to the associated Task and to the Dispatcher. A Callback wraps the BREW callback mechanism — meaning that this is the only class needing changes when porting on a different platform.
executeStatic() is the actual workhorse, the method registered in the callback mechanism. It’s centered on the 2 possible states transmitted by the Task:
- CONTINUE when the process is continued
- STOP that interrupts the Task. Please note that in this case the Dispatcher is advised that a Task intends to stop its processing and the Dispatcher is the one that does the cleanup and might inform other interested Tasks of this event.
void Dispatcher::stopTask(int caller) { if (isOutOfBoundaries(caller)) return; delete cbkHolder_[caller]; initTask(caller); --nextAvailablePosition_; }
Implementation observations:
This code was tested using BREW2.0 SDK version 2.0.0.32. Tests conducted on the emulator on previous SDK releases (including 1.1) revealed an abnormal behavior when sending event keys — these events were queued until the end of the task(s) — meaning that actually there is no way to interrupt task(s).
There are other mechanisms in BREW that might be used instead of callbacks — notably timers and PostEvent. Unfortunately there are other issues that might deter their use. Most devices enter into a sleep mode when there is no keypad activity on the handset for a fixed duration. When in this mode timers expire much more slowly than the actual duration set for the timer. On BREW 2.0 one can use EVT_APP_NO_SLEEP event (returning TRUE from this event changes the above described default behavior). The problem is that this event exists only on BREW 2.0 and its implementation is at the discretion of the OEM. PostEvent behaves exactly like callbacks on previous versions – it’s a known bug Qualcomm is aware of.
Advantages of using the framework:
- Tasks can be developed separately and assembled at runtime
- Each task keeps its own state
- The state can be shared in a very controlled way
- The tasks can easily collaborate/synchronize (notify/wait)
- The tasks can easily vary their speed of execution (important in applications like games or where there is a need for fine control over the task execution)
- There is no need to know the mechanism used by the platform to activate and run tasks – all these aspects are hidden and the mechanisms are easily interchangeable.
Downloads: Source Code – 37 kb.
About the Author
Radu Braniste is Director of Technology at Epicad. He can be contacted at rbraniste@epicad.com
# # #