I have uploaded the source code to BitBucket, you can easily download it using Mercurial (see here for an intro) by simply using:
https://marco@bitbucket.org/marco/j2me_threads...
In my experience, one of the most difficult things to achieve in software (and even
more so in "limited" environments, such as J2ME) is to harness certain "long-running tasks" so that:
- they have enough time to complete their task, yet they do not
hog the system so as to make it unresponsive to users (who would
then have the impression the application is 'hung'); - it is easy to both check on whether they are still running or are, indeed, 'hung';
- it is relatively straightforward to set a timeout, expired which, they will be, rather uncerimoniously aborted;
- it is relatively straightforward to enable a 'Cancel' option for the user to press if they get bored before the timeout expires.
The hortodox answer to this has been (and quite rightly so) to have the long running task run in a separate thread from the main (GUI) thread, with a boolean variable to control execution.
Something like this:
public class MyLongTask implements Runnable { private volatile boolean stopped = false; public void stop() { stopped = true; } public void run() { boolean finished = false; // do some setup here // ... while (!stopped && !finished) { doAChunkOfWork(); finished = amIDone(); } } }
or something similar to that.
The controlling thread (typically the one that runs the GUI too, which has probably a "Cancel" button hooked to a cancelTask() method) looks something like this:
public class Controller { private MyFrameView theView; private MyLongTask task = null; public Controller(MyFrameView theView) { this.theView = theView; } // various other stuff... public void doStartTimeConsumingTask() { task = new MyLongTask(); new Thread(task).start(); } public void doCancel() { if(task != null) if (! task.isFinished()) task.stop(); } // other Controller "stuff" }
This is, in fact, what the good folks at Sun in the NetBeans team have essentially done, by defining the CancellableTask interface and a generic
SimpleCancellableTask
class (although, the latter, quite sadly has not implemented... the cancel() method!).This was mostly done to supporting NetBeans' WaitScreen component and, even then, I found it not terribly compelling. (see Lukas Hasik's blog here).
Initially, when confronted with the problem, I simply extended
SimpleCancellableTask
, and used the stopped/stop()
mechanism to enable the cancel()
mechanism - that enabled my application to stop a long running task that had overstayed its welcome; however, at the price of having to throw a RuntimeException
to communicate back to the WaitScreen
that it was done, but had not finished its task.So far, so good.
However, there is still no easy way to set a timeout task, nor we have implemented any mechanism to let
MyLongTask
tell Controller they're done (let alone let the latter communicate back to the the View, which, most likely, will display something to the user to tell her we're done).The latter problem is easily solved with the Observer pattern (see my other post - to be published later): this can be implemented by either the view or the controller - my preference has always been for the Controller to act as the observer (again, I seem to be in broad agreement with most practitioners - those times when, for expediency, I have implemented that in the View, I have always come to regret it... Thank God for Refactoring!).
Setting a timeout and generally keeping tabs on the long-running task, in fact, requires that one uses an additional, controlling thread.
In other words, to properly manage a time consuming task, you need two additional threads - just one won't do.
I decided then that a more general solution was needed, one that would allow an easy, programmatic way of controlling a long running task and was, ideally, also "pre-wired" with an observer.
My approach uses a
ControllerThread
, a LongRunningTask
interface and an Observer
interface.The code can be downloaded here, please note that this is written specifically for 'microedition' environments(J2ME), hence no generics or any other Java 5 niceties are used.
The
LongRunningTask
interface is reviewed below, the ControllerThread
will be analysed in Part 2 of this article. LongRunningTask
is a relatively straightforward interface, that defines a couple of 'check-me' methods (isDone()
and isStopped()
) the obvious cancel()
one and an optional progress()
method, that returns a percentage value to completion (the actual semantic of the value returned by progress(), is worth noting, is entirely application-dependent, although I like to use it as a % value, to update, for example, a Gauge
UI indicator). The 'quirk' here is that
LongRunningTask
does not extend Runnable: the reason for this will be clearer when we'll come to look at ControllerThread (that does implement Runnable). All the work is done in execute()
that throws a TaskException
(unlike run()
in Runnable) and can thus indicate abnormal termination to a 'wrapper' class. To use the framework, one simply implements
LongRunningTask
for a class with a time-consuming method, and checks regularly on a boolean variable that may be set by cancel()
(or any other sensible way to communicate to a running thread that it's time to quit it, enough is enough).public class RecordTask implements LongRunningTask { private boolean stopped; private boolean done; private int progressIndicator; private byte[] audioData; /** Creates a new instance of RecordTask */ public RecordTask() { } public boolean cancel() { stopped = true; return true; } public boolean isStopped() { return stopped; } public boolean isDone() { return done; } public int progress() { return progressIndicator; } /** * This is where all the time-consuming work is done: * here we record a speech segment, long at mostmaxDuration
* or until the user presses a "Stop" command (that in turn will cause * the controller to callcancel()
* * @param observer the observing class */ public void execute(Observer observer) throws TaskException { observer.addMsg("Record started"); try { done = false; stopped = false; Player player = Manager.createPlayer(getAudioMimetype()); if (player != null) { player.realize(); RecordControl rc = (RecordControl) player.getControl("RecordControl"); if (rc != null) { observer.addMsg("RecordControl: OK"); } else { observer.addMsg("No RecordControl, exiting"); return; } ByteArrayOutputStream bos = new ByteArrayOutputStream(); rc.setRecordStream(bos); rc.startRecord(); player.prefetch(); player.start(); long start = System.currentTimeMillis(); while (!isStopped() && ((System.currentTimeMillis()-start) < getMaxDuration())) { calcProgress(start); observer.announceProgress(progress()); } rc.commit(); player.close(); audioData = bos.toByteArray(); observer.announceProgress(100); } done = true; } catch (Exception ex) { observer.addMsg("Recorder exception: "+ex.getMessage()); // The following is necessary to communicate back to ControllerThread // that an error condition was encountered throw new TaskException("Recorder exception: "+ex.getMessage()); } } // Other utility methods follow.... }
Then, an instance of this class is passed, at construction, to a newly created instance of
ControllerThread
:public class Multimedia implements Observer { // Observer interface implementation omitted here // ref. source code & Part II of this article public void startRecording() { task = new RecordTask(); thread = new ControllerThread(task, 1000L, 10000L, this); new Thread(thread).start(); } // Other stuff goes here... }
And that's pretty much about it: with the code above, we have created and started a new task which we'll check every second (1000L msec) and will allow at most 10 sec (10000L msec) to complete.
All the magic is done in ControllerThread - explained in part 2 of this series.
The link to this code it broken. Is it still possible to get it?
ReplyDeleteI have updated the links, they should work now (yes, the code is still very much available!)
ReplyDeleteThanks for your interest and if you have comments / ideas to improve it, please feel free to post!
Hi,
ReplyDeleteThe link is timing out.
Can you please update the link to the code.
Regards,
Aidan
Yes, indeed: the link is pointing to my personal server who 'died' a few weeks ago.
ReplyDeleteI'm planning to move the code to BitBucket or Google Code (code.google.com) soon as I find 10 minutes' breathing space.
(relocating to the US whilst managing a team of 20 engineers at Google is hard work :-)
Please do bear with me, I'll probably get round to it this weekend.
Hi Marco,
ReplyDeleteIf you get a chance to put your code up on Google Code that will be great.
I understand if you don't have time.
Good luck in the US don't work too hard.
Uploaded the code to BitBucket.org, for download using a Mercurial client:
ReplyDeletehg clone https://bitbucket.org/marco/j2me_threads
enjoy!
Seems this code is no longer available on BitBucket?
ReplyDeleteHi Steve,
ReplyDeleteI've just checked, it still seems to be there:
https://bitbucket.org/marco/j2me-threads
It's a publicly available repository, please let me know if still can't reach it and what do you see instead.
Thanks for reading!
(BTW - I've moved my blog to http://codetrips.com)