Threads implement preemptive multitasking, because one thread can preempt another if it has to handle an event — for instance, the window server can handle a key event while an application is running, by preempting the running application thread.
The ability of one thread to preempt another depends on their relative thread priorities. The most critical threads in the system are given the highest priorities — with the kernel, including device drivers, the highest priority of all.
EPOC implements multithreading so that it can run multiple applications and servers simultaneously.
Active objects implement non-preemptive multitasking within the context of a single thread, because each event must be completely handled by its RunL() before the next event on that thread can be handled.
Active objects, like threads, have priorities that affect their scheduling. But they affect it in quite a different way:
q With multithreading, the scheduling issue is "which thread should be running now?", to which the answer is always "the currently eligible-to-run thread with the highest priority". The question gets asked whenever thread priorities or eligibility are changed.
q With active objects, the scheduling issue only materializes when a RunL() has completed. The question for the active scheduler is then "which object's RunL() shall I run next?". The answer is "if there is just one object now eligible to run, then run it. If there is more than one eligible object, then choose the one with the highest priority. If there are no eligible objects, then wait for the next event and then decide what to do".
Some events are more important than others. It's much better to handle events in priority order than (say) first in, first out (FIFO). Events that control the thread (key events to an application, for example,) can be handled with higher priority than others (for instance, some kinds of animation). But once a RunL() has started — even for a low-priority event — it runs to completion. No other RunL() can be called until the current one has finished. That's ok, provided that all your event handlers are fairly quick — a couple of seconds at most, say, in an application, or a few milliseconds in a high-priority server.
Non-preemptive multitasking is surprisingly powerful. Actually, there should be no surprise about this: it's the natural paradigm to use for event handling. For instance, the window server handles key and pointer events, screen drawing, requests from every GUI-based application in the system, and animations including a flashing text cursor, sprites and self-updating clocks. It delivers all this sophistication using a single thread, with active-object-based multitasking.
And a sophisticated application such as EPOC Word uses active objects to handle status display update and text pagination at lower priority than more critical events — responding to editing events at and around the cursor position.
In many systems, the preferred way to multitask is to multithread. In EPOC, the preferred way to multitask is to use active objects.
In a truly event-handling context, using active objects is pure win-win over using threads:
q You lose no functionality over threads, because preemption gains you nothing in a truly event-handled system
q You gain enormous convenience over threads, because you know you can't be preempted: you don't need to use mutexes, semaphores, critical sections or any kind of synchronization to protect against the activities of other active objects in your thread
Non-preemptive multitasking is not at all the same thing as cooperative multitasking. The 'cooperation' in cooperative multitasking is that one task has to say "I am now prepared for another task to run", for instance by using Yield() or a similar function. What this really means is "I am a long-running task, but I now wish to yield control to the system, so it can get any outstanding events handled if it needs to". This is a potentially messy way to mix event handling and non-event handling code: it often results in messy programs, and justifiably gives 'cooperative multitasking' a poor reputation. Active objects don't work like that: during RunL(), you have the system to yourself, and 'yield' occurs when your RunL() has finished.
Having said all that, EPOC does support a kind of yield. It's used exclusively for EIKON dialogs. It's dangerous to use it for any other purpose, since it's essential to guarantee that all yields nest correctly. In the case of dialogs, this is assured, since dialogs naturally nest. We'll see more of this in Chapter 20.
All multitasking systems require a degree of cooperation, so that tasks can communicate with each other where necessary. Active objects require less cooperation than threads, because they are non-preemptively scheduled. And they can be just as independent as threads: a thread's active scheduler manages active objects independently of one another, just as the kernel scheduler manages threads independently of one another.
Most multithreaded programming in EPOC uses the client-server framework:
q A server thread is responsible for managing one or more related resources
q One or more client threads may use the server to perform functions which use those resources
EPOC's two most critical servers are the file server, which handles all files, and the window server, which handles user input and drawing to screen. A wide range of other servers is used to manage communications, databases, schedule, contacts, and the like. The kernel also acts as a kind of server. A client program may be either another server, or an application.
Client-server programming involves two potentially difficult issues:
q It involves multithreaded programming disciplines, which are difficult to get right
q It involves crossing process boundaries, which are a key guardian of system integrity
In order to minimize any difficulty associated with these issues, EPOC constrains the client/server interface to something that is small enough to maintain confidence in the usefulness of the process boundary, and built in such a way that you don't need to use thread synchronization as either the user or even the implementer of a server. The key elements of the interface are
q The client interface: each server provides an API to its clients — the client interface — which disguises all the client-server communications, so that clients can use the server easily without knowing the specifics of the client-server framework
q Kernel-supported message passing: if you're implementing a server (along with its client interface), this is the main method by which you pass requests from the client to the server, and handle them. The message-passing framework is powerful enough for the job — but no more complex than it needs to be.
q Kernel-supported inter-thread read and write: messages can't convey much information from client to server, and even less from server to client. To pass more information, a server can read from, or write to, a client's address space.
Most client classes that access server-based resources have names beginning with R. Two examples are RFile (a file, with functions such as Read(), Write(), Open() etc.) and RWindow (an on-screen window, with functions such as SetSize(), BeginRedraw() etc.). These client interface classes are implemented (by the server designer) using message passing and inter-thread read and write.
Clearly servers are event handlers. The central classes in any server are a single CServer-derived class to implement the behavior of the whole server and a number of CSession-derived classes to handle requests on behalf of each active client. CServer is an active object, whose RunL() interprets incoming messages, creates or destroys CSessions as needed, and calls their ServiceL() function to handle routine client requests. Most servers use more active objects to handle other events — such as key and pointer events, in the case of the window server, or disk-door-opened events, in the case of the file server.
The kernel server uses a similar framework. The RTimer class, and many other R classes in the user library, implement their APIs by message passing similar to that used by servers. The kernel server's framework is different from the standard server framework, to take account of the privilege-mode environment and the fact that there's only one kernel. But the principles are the same.
Device drivers also use a message passing system similar to that used by servers.
There's a lot more to say about servers. I cover this thoroughly in Chapter 21, which explains the message-passing framework in more detail, and also provides many tips for getting the best performance from servers.
Most tasks are event handlers. So EPOC's design is optimized for event handling, with good results for ease of programming, system efficiency, and robustness.
But some tasks really are long-running threads. Game engine calculations, spreadsheet recalculation, background printing, and the like can be particularly long running. Status display updates, animations, and the like are only slightly less demanding.
EPOC has broadly two approaches to handling tasks that really are long-running threads.
q Simulate them using active objects, and chains of pseudo-events. Split the task into short increments, generate a low-priority pseudo-event that will be handled if no real events (such as user input) need handling; handle an increment and, if that doesn't complete the task, generate another pseudo-event.
q Really use multithreading. Launch a background thread and work out some scheme of communication between the application's (or server's) main thread and the background thread.
If it's possible, the first approach is strongly preferred, because it's more efficient.
In fact, the second approach is often impossible. EPOC servers have been designed to treat each client thread as a distinct entity. This means that an object, such as an RFile or RWindow, which was opened by one thread, cannot be used at all by any other thread — even a thread in the same process as the one that opened it.
So background threads are limited in their functionality to things that strictly don't require any sharing of server-provided resources with the main thread.
There is an area of conflict where on the one hand your long-running task needs to share server-provided resources with the main application tasks, but on the other hand can't be cast into the incremental form that would enable you to drive it with active objects. It's not essentially difficult to write active-object-friendly code when starting from scratch, but this situation often arises when code is being ported to EPOC. It's an important situation, which John Forrest covers in detail in Chapter 28, and I cover to some extent in Chapter 20.
The sharing of server-based resources by different threads — in the same process — will also be implemented in the next release of EPOC. This will eliminate some EPOC-specific difficulties for multithreading.
Symbian's current Java implementation gives process-wide access to server-based resources by using an intermediate server thread (the design is given in Chapter 21). The new support for sharing will remove the need for this intermediate thread. This will speed up the Java implementation (which is already pretty good), and simplify the addition of future APIs, such as those required for JavaPhone.