Threads in Qualcomm Brew 3.1.5

One of the odd things in Brew is that its multitasking model  is cooperative.  In a cooperative model, the executing task must yield explicitly to the task scheduler so that the scheduler can dispatch another task.  So in addition to the creation of the thread, the programmer is responsible for yielding the execution and supply a resume point where the control will eventually return .  On the other hand, in traditional preemptive model, the programmer has to do little on his own; the scheduler preempts and schedules  threads as necessary.  That’s why an infinite loop is a sin in a cooperative model. POSIX threads are preemptive.  The threads in Brew are called IThread and they are, however, cooperative.

In Brew, a single system level thread is shared across all the user applications. All IThreads created by an application also share the  same system thread. It is  an example of many-to-one thread model.  This main thread of execution is used for dispatching both the user and the system level events to the application, such as key-press, low-battery, low-memory, etc. The thread is also used for invoking callbacks which can be registered with Brew for certain events, e.g., timeouts,  data availability on a socket, etc. In reality we cannot achieve true concurrency in Brew because of the many-to-one model.

I am not very sure why the thread model is many-to-one in Brew. Brew is deployed in a lot of low-end handsets for markets in developing countries. I suppose the model simplifies the integration of  Brew in the OEM’s OS.  It also reduces the overhead of error prone synchronization.  Apart from that, IThread can also act as an useful abstraction while grouping a set of asynchronous tasks,  and  ensuring  an order of execution among the tasks.

Here is an example: the INETMGR_GetHostByName API which is  equivalent to the GetHostByName of POSIX API is actually a two step operation in Brew. First, the user calls INETMGR_GetHostByName passing the domain name,  a global buffer and a callback as a parameter. Brew delegates the operation to the OS and returns. When the DNS is resolved, the buffer is populated and the callback is invoked from the main thread. This is an asynchronous behavior, and the developer must be prepared to handle the callback and then only proceed with other network operations.  In the following snippet the actual connect call is  made within the callback DNSLookupCallback, when the DNS is resolved.

void DnsLookup(MyApplet *pMe )
{
 //initialize the callback
 CALLBACK_Init(pMe->pcbDNSLookup, DNSLookupCallback, pMe);
 //call the async version of GetHostByName
 INETMGR_GetHostByName(
 pMe->piNet,
 pMe->pDNSRes,
 pMe->addr,
 pMe->pcbDNSLookup);

}
//this callback will be invoked when the DNS is resolved or an error
//occurred while calling GetHostByName
void DNSLookupCallback(MyApplet* pMe)
{

 int result;
 AEEDNSResult *pres = pMe->pDNSRes;
 if (pres->nResult > 0 && pres->nResult <= AEEDNSMAXADDRS ) {
 // We have an IP address
 pMe->inet_addr = pres->addrs[0];
 result = ISOCKPORT_OpenEx( pMe->n.pISockPort, pMe->wFamily, AEE_SOCKPORT_STREAM, 0 );
 TXTBLBIGDBGPRINTF("result %d", result);
 if(AEE_SUCCESS == result){
 TryConnect(pMe);
 }else{
 DBGPRINTF("failed to open port");
 }

 } else {
 DBGPRINTF("Error %d in DNS Lookup", pres->nResult);
 }

}

The same can be implemented using  the following IThread idioms, which looks  much simpler.

void DnsLookupSync(MyApplet *pMe )
{

 AEEDNSResult *pres = pMe->pDNSRes;
 //pMe->thread is the Ithread created
 AEECallback *pcb = ITHREAD_GetResumeCBK(pMe->thread);
 INETMGR_GetHostByName(
 pMe->n.m_piNet,
 pres,
 pMe->session.addr,
 pcb);
 //calling suspend from here. so that when DNS lookup completes
 //the execution will resume from the following line.
 ITHREAD_Suspend(pMe->thread);
 DBGPRINTF("result of lookup result:%d ",pres->nResult);
 if (pres->nResult > 0 && pres->nResult <= AEEDNSMAXADDRS ) {
 // have an IP address
 pMe->inet_addr = pres->addrs[0];
 ISOCKPORT_OpenEx( pMe->n.pISockPort, pMe->wFamily, AEE_SOCKPORT_STREAM, 0 );
 TryConnect(pMe);
 } else {
 DBGPRINTF("Error %d in DNS Lookup", pres->nResult);
 }
}

Creating an IThread is a however costly. An IThread needs a stack which is allocated from the heap available
to the application. Moreover with the many-to-one model, and cooperative nature, threads cannot help any better in multitasking. So it’s always better to reuse an IThread within an application. This can be achieved using a custom thread context.

Qualcomm ships an IThread based   implementation of the network connect/read/write  operations with Brew 3.1.5 SDK. In my computer the default install location of the implementation is C:\Program Files\BREW 3.1.5\sdk\src\thrdutil.  These APIs take an IThread as the additional parameter compared to their non-threaded counter part.  However, the implementation is a wrapper around the ISOCKET interface of Brew, which is no longer recommended for use. From version 3.1.3 onwards, the new ISOCKPORT API obsoletes ISOCKET as the way for socket programming.  Nevertheless, the code in thrdutils still serves as good reference for IThread usage.

We can reuse an IThread  if we use a model similar to thread pools.  Since threads, all events and callbacks share the same system thread, there is actually no point in having more than one thread in the pool.  In addition, we can use a queue  so that the tasks that are submitted to the pool for execution are processed in FIFO order.  Such ordering will also be useful in case we want to serialize the operations that modifies the data model of the application.

Here is an excerpt from an implementation I did:


//creates the pool. The Applet initialization
//function is the best place to create the pool.

CommandPool* createPool(MyApplet* pMe, uint16 queueSize)
{

 CommandPool* pPool = (CommandPool*)MALLOC(sizeof(CommandPool));
 if(pPool == NULL){
 DBGPRINTF("Unable to create pool");
 return NULL;
 }
 DBGPRINTF("Creating a pool with qsize %d", queueSize);
 pPool->pMe = pMe;
 pPool->queueSize = queueSize;
 pPool->queue = NULL;
 pPool->state = STOPPED_USER;
 pPool->pcbCurrent= NULL;
 pPool->pendingCb = 0;
 if(!startPool(pPool)){
 freePool(pPool);
 pPool = NULL;
 }
 return pPool;

}

static boolean startPool(CommandPool *pPool)
{
 int r;
 //create the thread
 r = ISHELL_CreateInstance( pPool->pMe->piShell, AEECLSID_THREAD,
 (void **)&pPool->cx.thread );
 if(SUCCESS !=r ){
 DBGPRINTF("Unable to create thread");
 return FALSE;
 }

 DBGPRINTF("Thread created successfully");
 //initialize the join callback for the IThread
 CALLBACK_Init( &pPool->cx.cbThreadJoin, (PFNNOTIFY)threadJoinCallback, (void *)pPool );
 //callback that should be invoked by tasks to indicate completion of the task
 pPool->cx.cbCompleted = resumeThread;
 //set the join callback
 ITHREAD_Join( pPool->cx.thread, &pPool->cx.cbThreadJoin,
 &pPool->cx.threadReturn );
 //start the thread
 DBGPRINTF("Thread starting ...");
 r = ITHREAD_Start( pPool->cx.thread, 128, (PFNTHREAD)run, (void*) pPool);
 if(SUCCESS !=r){
 DBGPRINTF("Unable to start thread %d", r);
 ITHREAD_Release(pPool->cx.thread);
 return FALSE;
 }
 //reset the state of pool
 pPool->state = STARTED;

 return TRUE;
}

The run function passed to ITHREAD_Start is the heart of the pool, that executes the tasks.


static void run(IThread* pt, CommandPool *pPool)
{
 AEECallback *pcb = NULL;

 //retrieve the resume callback and store a reference to it in the context
 pcb = ITHREAD_GetResumeCBK(pPool->cx.thread);
 pPool->cx.pcbResumeCallback = pcb;

 DBGPRINTF("pool=>state=%d",  pPool->state);

 while(STOPPED_USER != pPool->state)
 {
 //retrieve the next task from the queue
 ThreadTask *cb = NULL;
 if(NULL != pPool->queue){
 cb = (ThreadTask*)list_last(pPool->queue);
 }

 if(NULL == cb){
 DBGPRINTF("No element in the pools queue");

 }else{
 //delete the callback from the queue
 pPool->queue = list_delete(cb, pPool->queue);
 pPool->pendingCb--;
 //make it current callback
 pPool->pcbCurrent = cb;

 //invoke the task - which may involve several asynchronous steps.
 cb->pfnNotify(pPool, cb->data);

 }
 //now yield to the main thread
 DBGPRINTF("Yielding pool thread");
 ITHREAD_Suspend(pPool->cx.thread);

 //the thread execution is resumed. It may be due to addition of a new task to the queue
 //or from a completion of a task.

 DBGPRINTF("Resuming pool thread");
 //check if the callback has completed
 while(cb !=NULL && cb->pReserved != NULL ){
 boolean completed = cb->pReserved;
 if(completed == FALSE){
 DBGPRINTF("We are still processing the callback so lets yield again");
 ITHREAD_Suspend(pPool->cx.thread);
 DBGPRINTF("Waking up thread recheck callback ");
 cb = pPool->pcbCurrent;
 }else{
 DBGPRINTF("Cleaning up the callback 0x%x", cb);
 pPool->pcbCurrent = NULL;
 FREEIF(cb);
 cb = NULL;
 }

 }

 }
 ITHREAD_Exit(pPool->cx.thread, SUCCESS);
}

The user of the pool calls the following method to submit a task to the pool.


void execute(CommandPool* pPool, ThreadTask *pcb)
{

 if(pPool->pendingCb +1 > pPool->queueSize){
 DBGPRINTF("We cannot hold the callback. Please add  latter");
 if(pcb !=NULL && pcb->pfnCancel !=NULL){
 pcb->pfnCancel(pcb->pCancelData);
 }
 return;
 }

 pPool->queue = list_add((void*)pcb, pPool->queue);
 pPool->pendingCb++;
 DBGPRINTF("added callback 0x%x to pool.", pcb);

 //lets schedule a resume callback for the thread as a notification
 //for addition of a new task
 DBGPRINTF("adding a resume callback");
 ISHELL_Resume(pPool->pMe->piShell, pPool->cx.pcbResumeCallback);

}

When the task is complete, the user will notify the pool of the completion of the task by invoking
the following function, which can be accessed from the pool object passed to the task.


static void resumeThread(CommandPool *pPool)
{
 boolean *tmp = NULL;
 //indicate the callback has completed successfully
 if(pPool->pcbCurrent != NULL){
 DBGPRINTF("Setting completion flag in callback");
 pPool->pcbCurrent->pReserved = TRUE;
 }
 ISHELL_Resume( pPool->pMe->piShell, pPool->cx.pcbResumeCallback );
}

My two cents.

Advertisements

About Amar Deka

Software Engineer
This entry was posted in Brew and tagged , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s