devxlogo

Cooperative Multithreading in BREW with IThread

Cooperative Multithreading in BREW with IThread

y last article mentioned the BREW IThread as a possible solution to the challenges of dealing with blocking code on BREW. Whether you’re porting existing code to BREW or writing new code, the IThread interface can be quite handy. Sadly, in my experience it’s one of the most misunderstood and maligned interfaces provided by BREW?developers seem to overlook it and write code which provides the same functionality (using AEECallback and ISHELL_Resume), often because they have decided that since BREW doesn’t provide preemptive multithreading there’s little point in using IThread. Unfortunately, this approach leads to additional testing and debugging, as well as code that requires additional documentation (or time invested in understanding where no documentation is presented).

This article begins by explaining the basic principle behind cooperative multithreading and then shows you the basics of the IThread interface. Rather than just give you some code samples along the way, you can download hellothread.c and follow along; bits of the actual file are included here for your reference.

Understanding Cooperative Multithreading
Most readers are comfortable with the basic notion of a thread of execution: you create a thread using some system API such as pthread_create, passing a function pointer called the thread’s main function. This function executes in a separate thread of execution in the same memory space at the same time as the main thread of execution, and you can share data using synchronization tools such as mutexes. When your main function exits or you call the equivalent of pthread_exit, your thread terminates and any functions joined to the thread using pthread_join are invoked at that time. Under the hood, the native operating system (typically in conjunction with a user-level library) is responsible for doing the work of sharing processor and memory and providing mutexes and the like. Because the host platform and OS support threads natively, there are typically some guarantees regarding the availability of resources?one thread can’t starve another thread for CPU access, for example. For this reason, these environments are called preemptive, because the operating system can preempt one thread (or process) another to ensure that everything gets a fair share of resources.

Not so with the BREW application environment at present, where memory and processor are shared cooperatively. That is, if your application is running, it owns the CPU for the duration of an event handler’s execution; spend too long handling an event or callback, and the handset will reset, because its watchdog timer has assumed that your application has crashed. Many developers criticize this approach as placing an unwelcome burden on application developers, and in truth it does make some kinds of applications (those requiring a guarantee of deterministic execution) impossible to create. However, the majority of applications do not have critical scheduling requirements, and, in fact, cooperative threads are not inordinately more complex than their preemptive kin. There are two key differences, however. First, threads on BREW must yield to the processor occasionally, giving other threads of execution, which requires two function calls (or one, if you’d like to write a wrapper function). Second, because your threads explicitly yield the processor, you don’t need synchronization interfaces when accessing shared data, because only one thread is running during the shared data access.

Starting a New IThreadThis article’s application is a simple “hello world” application, thread-style (see Figure 1). It increments two counters in two threads, and just for fun, one thread also draws circles of incrementally larger size on the display. To show how threads can return values to the owning program, one thread exits after a set number of iterations; the other does not.

 
Figure 1. Hello World: A simple, thread-style application.

An IThread is an interface, just like any other, so you can make one simply by calling ISHELL_CreateInstance. Before you do, however, you should have someplace with appropriate scope to contain the reference to the thread, as well as a structure to contain any thread-local storage that the thread would want to share between the functions it invokes:

typedef struct _SThreadCtx{  IThread  *pit;  int n;  IDisplay *pid;  IShell *pis;  IGraphics *pig;} SThreadCtx;typedef struct _CApp{  AEEApplet a;  SThreadCtx stc1, stc2;  AEECallback cbThread2Done;  int nThread2Result;} CApp;

This is pretty simple stuff here. The application’s threads will use an integer, access to the display, shell, and an IGraphics interface; all of this is wrapped in an SThreadCtx. The application itself will have two threads; for now, don’t concern yourself with the purpose of the cbThread2Done or nThread2Result members of the CApp structure.

With the heap use out of the way, you can create and initialize your threads when your application begins. In the application’s event handler, write:

    case EVT_APP_START:    {      int r;      r = ISHELL_CreateInstance( p->m_pIShell, AEECLSID_THREAD,                                  (void **)&pMe->stc1.pit );      r = ISHELL_CreateInstance( p->m_pIShell, AEECLSID_THREAD,                                  (void **)&pMe->stc2.pit );      r = ISHELL_CreateInstance( p->m_pIShell, AEECLSID_GRAPHICS,                                  (void **)&pMe->stc2.pig );      pMe->stc1.pis = pMe->stc2.pis = p->m_pIShell;      pMe->stc1.pid = pMe->stc2.pid = p->m_pIDisplay;      CALLBACK_Init( &pMe->cbThread2Done, (PFNNOTIFY)Thread2Done, (void *)pMe );      ITHREAD_HoldRsc( pMe->stc2.pit, (IBase *)pMe->stc2.pig );      ITHREAD_Join( pMe->stc2.pit, &pMe->cbThread2Done, &pMe->nThread2Result );      ITHREAD_Start( pMe->stc1.pit, 128, (PFNTHREAD)Thread1Start,                     (void *)&pMe->stc1);      ITHREAD_Start( pMe->stc2.pit, 128, (PFNTHREAD)Thread2Start,                      (void *)&pMe->stc2);      IDISPLAY_DrawText( p->m_pIDisplay, AEE_FONT_BOLD, szText, -1, 0, 0, NULL,                         IDF_ALIGN_CENTER | IDF_ALIGN_MIDDLE);      IDISPLAY_Update (p->m_pIDisplay);    }    return(TRUE);

This code:

  • Creates two IThread instances.
  • Creates the IGraphics instance required by the circle-drawing thread.
  • Joins the function Thread2Done with the second thread, so that it will be invoked when Thread2 exits.
  • Indicates that thread 2 will use the IGraphics interface.
  • Calls each thread’s start function.
  • Draws “Hello World” to the display.
  • Updates the display.
  • The IThread calls are quite similar to POSIX thread calls. Qualcomm has also added the ITHREAD_HoldRsc and ITHREAD_ReleaseRsc interfaces to associate a BREW interface with a thread. The BREW ITHREAD_HoldRsc interface attaches a BREW interface to a thread. Oddly, it does not update the item’s reference count, so don’t make the mistake of releasing the held interface when you call ITHREAD_HoldRsc.

    For clarity, I’ve broken out the thread’s main function and start function?you don’t have to do this, but to me main on BREW suggests an event loop, whose first argument is the application context. By comparison, the PFNTHREAD function prototype for an IThread’s main function has two arguments, the running thread and the thread context. Thus, my Thread1Start and Thread2Start functions look like this:

    static void Thread1Start( IThread *pThread, SThreadCtx *pMe ){  Thread1Main( pMe );}static void Thread2Start( IThread *pThread, SThreadCtx *pMe ){  Thread2Main( pMe );}

    This imposes a bit of space overhead in return for improved readability; in addition it provides a nice abstraction point in case I need to do additional setup work in the new thread before giving it something to do.

    Yielding the Processor
    Yielding the processor in an IThread is done using ISHELL_Resume and ITHREAD_GetResumeCBK:

    static void Thread1Main( SThreadCtx *pMe ){  AECHAR wsz[ 16 ];  while( 1 )  {    pMe->n++;    WSPRINTF( wsz, sizeof( wsz ), L"t1=%d", pMe->n );    IDISPLAY_DrawText( pMe->pid, AEE_FONT_NORMAL, wsz, -1, 0, 0, NULL,                        IDF_ALIGN_CENTER | IDF_ALIGN_TOP);    IDISPLAY_Update (pMe->pid);    ISHELL_Resume( pMe->pis, ITHREAD_GetResumeCBK( pMe->pit ) );    ITHREAD_Suspend( pMe->pit );  }}

    What raises eyebrows about this code for experienced BREW developers is the fifth line?an infinite loop, a definite no-no. Otherwise, it’s quite straightforward, until the last two lines, which obtains the AEECallback instance for this thread at this particular point in execution, and then suspends thread execution. Under the hood, this code sets up the system’s notion of the thread context at this point in your function, so when ISHELL_Resume resumes execution, it will do so at the current point in your function where you called ITHREAD_Suspend. This is functionally equivalent to a call to the Qualcomm-provided IThread_Yield, available in the BREW SDK’s src/ directory in the file AEETU_Yield.c:

    void IThread_Yield( IThread *me, IShell *pIShell ){  AEECallback *pcb = ITHREAD_GetResumeCBK( me );  ISHELL_Resume( pIShell, pcb );  ITHREAD_Suspend( me );}

    Why did Qualcomm break out the thread callback from the resume operation? Not just to keep us typing two lines instead of one, but to provide access to the callback for any callback-consuming function. Consider the Qualcomm-provided IThread_GetHostByName, which mimicks a blocking gethostbyname call, familiar to desktop developers using the Berkeley sockets interface:

    int IThread_GetHostByName( IThread *me, INetMgr *piNet,                            AEEDNSResult *pRes, const char *pszHost){  AEECallback *pcb = ITHREAD_GetResumeCBK(me);  INETMGR_GetHostByName(piNet, pRes, pszHost, pcb);  ITHREAD_Suspend(me);  if (((AEEDNSResult *)0 != pRes) &&        (pRes->nResult > 0) && (pRes->nResult <= AEEDNSMAXADDRS))  {    return pRes->nResult;  }  else  {    return 0;  }}

    By wrapping the INETMGR_GetHostByName interface within its own thread, you can simulate blocking behavior by using the thread’s own AEECallback as the callback the InetMgr invokes upon completion. Once the INetMgr invokes the callback, execution continues on the line after the ITHREAD_Suspend invocation as if the pair of calls to INetMgr and IThread were a synchronous call. This trick should work anywhere an interface consumes an AEECallback; see the thread utilities provided by Qualcomm in recent drops of the SDK.

    Obtaining the Result of an IThread’s Execution
    When a thread exits, it can provide a return value to the thread originator. Because of the prohibition on global variables in early versions of BREW, you must also provide the integer variable to store the return value. Remember the call to ITHREAD_Join in my EVT_APP_START handler?

    CALLBACK_Init( &pMe->cbThread2Done, (PFNNOTIFY)Thread2Done, (void *)pMe );ITHREAD_Join( pMe->stc2.pit, &pMe->cbThread2Done, &pMe->nThread2Result );

    This causes the IThread to execute the callback function indicated by pMe->cbThread2Done when the thread terminates. Within this function, you can get the exit value (an integer) like this:

    static void Thread2Done( CApp *pMe ){  DBGPRINTF( pMe->nThread2Result == SUCCESS ?                "Thread2 exit SUCCESS" :                "Thread2 exit FAILED" );}

    You can use ITHREAD_Join anytime you need a programmatic action on thread termination.

    Give IThread a Second Chance!
    For those new to BREW, it’s important to realize that BREW has threads?they’re just cooperative. And if you’re an old saw with BREW, consider using BREW’s IThread to replace some of your callback chains. You won’t be disappointed.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist