Concurrency, asynchronous calls, Symbian
This is a combined clearing my head/presenting some common idioms/rant. Feel free to skip it :-) . All these things are probably very well known
at Symbian, but not necessarily discussed so much out-in-the-world.
In Symbian you are not supposed to do multithreading, instead you are
supposed to use asynchronous requests and the activescheduler. The
reasoning is that this costs a lot less than threads and additionally
non-reentrant code should be easier to write. It's not quite that
simple though...
1: Sequential flow/state machines
Think of making a simple network request. In a sequential program
you'd write something like:
reply DoRequest(aName, aRequest):
addr=lookup(aName)
socket.connect(addr)
socket.write(aRequest)
socket.receive(reply)
socket.close()
return reply
In symbian you write
void DoRequest(aName, aRequest, aReply*, aRequestStatus&):
if (iState!=EIdle)
CompleteRequest(aRequest, KErrServBusy)
else
iName=aName
iRequest=aRequest
iClientStatus=aRequestStatus
Lookup()
RunL:
if (iStatus!=KErrNone && iState!=EClose)
iState=EIdle
CompleteRequest(iClientRequest, iStatus)
else
switch(iState)
ELookUp
Connect()
EConnect
Write()
EWrite
Receive()
EReceive
Close()
EClose
iState=EIdle
CompleteRequest(iClientRequest,
KErrNone)
Lookup:
iState=ELookUp
lookup(iName, iAddr, iStatus)
Connect:
iState=EConnect
iSocket.Connect(iAddr)
Write:
iState=EWrite
iSocket.Write(iRequest)
Receive:
iState=EReceive
iSocket.Receive(iReply)
Close:
iState=EClose
iSocket.Close()
DoCancel:
// switch statement for cancelling omitted
Hmm. It doesn't look that simple anymore. So we gained the
responsiveness without multithreading and re-entrancy, but
on the other hand the code became a lot more complicated:
- all of the state necessary for DoRequest()
has to be in class scope instead of function
scope. It gets much harder to keep track of the
variables since they are declared much further away
from the code that uses them.
- you have to explicitly code a state machine
- the logically sequential chain of statements
gets broken up
(Of course in a realistic application, there is complexity
somewhere else to handle the posting of requests to other
threads and getting results back - so on some level there
are always asynchronous requests and state machines).
And this is the funny part: threads are supposed to be
too expensive largely because they need stack space. Put
since we had to lift all of the local variables from DoRequest
to class scope, we need all that memory anyway. And instead
of being allocated on the stack, which cannot fragment, it's
now on the heap which can. (It'll get released when the object
is deleted, but you could argue that you can also reuse/destroy
threads when the service is not in use).
2: Callbacks or User::RequestComplete
All(?) the old pure client-server requests use TRequestStatus
and active objects for notification of completed requests
(think RFs, RFile, RSocket etc.). A lot of newer modules
(e.g. the Media bits) use callbacks, and of course UI events
come as callbacks through the UI framework code.
Active objects are simple, clean and easy (ish) to get right.
You post a request, when it completes your RunL gets called.
If you need to cancel it, there's supposed to be a Cancel method
on the object you made the request to.
There's one big problem with the pure active object paradigm though:
you can only have one outstanding request at a time. So even
simple things like having a timeout for an async request aren't
possible without something else - a callback for the timer so
that the object can cancel the request.
The other thing with pure active objects is that you get into
trouble if the service that completes those requests runs in
the same thread. Consider first code like:
RX x; TRequestStatus s;
x.DoStuff(s);
User::WaitForRequest(s)
the last line blocks this thread. So if RX runs in the same thread,
the request will never complete. This bites people from time to time.
In this case you can say 'don't do that then'. But there's is
a case where you can't avoid it: cancelling:
CActive::Cancel
if (IsActive())
DoCancel()
User::WaitForRequest(iStatus)
CX::DoCancel
iX.CancelStuff()
This means that the CancelStuff() has to make absolutely sure
that it finishes the cancelling before returning. No biggie, as
long as the implementor realizes that.
3: Lack of Cancel functions
There are numerous async functions without a corresponding cancel
(RFile::Write, CObexClient::Connect, RConnection::Start). Since
some of these requests might take an indefinite time to complete
it's really painful - you might get a ViewSrv 11 panic just by
trying to cancel an action. Some of them can be worked around,
but none of those workarounds are documented.
4: No timeout on RMutex/RSemaphore ::Wait
If you believe that all code running on the device is perfect,
that's alright. And if you believe that, I've got a bridge
to sell...
In Real Life (tm), you get situations like:
thread 1: RMutex::Wait()
thread 2: RMutex::Wait()
thread 1: crash
thread 2: hang
(esp. since the example server code in Symbian C++ programming
does
RMutex::Wait()
if (err) Panic()
RMutex::Signal()
)
5: (Really) Bad example code
Since the complete state machine in point 1 is completely
useless when trying exemplify how an API is to be used,
the examples do a lot of User::WaitForRequest()s. Which
of course cannot be used in real code. So esp. novice
Symbian coders have a real struggle in going from the example
code to actual code. (Not that this is unusual in any
environment ...)
Quite a few of at least the Nokia examples completely ignore
Cancelling: empty DoCancel() functions in classes that do
async requests.
Since the callback style isn't 'natural' in Symbian, the
rules are not as clear. E.g., when can you delete an object
from its callback? The examples and documentation don't
give you any clues.
In the end, you often do need to do your own client-server
and multithreading in any complex system. But there are hardly
any examples, and even the ones that exist tend to get things
wrong.