« Even more AspectJ goodies... | Main | The New Holy Trinity »

January 23, 2005

Making concurrency a little bit easier

It's been hard to miss the discussion on concurrency and concurrent programming provoked by Herb Sutter's excellent article, "The Free Lunch is Over: A Fundamental Turn Towards Concurrency in Software". I've spent long enough building distributed and concurrent systems (about a decade of industry experience) to know that concurrency is hard and that I still don't know enough about it. Doug Lea's contribution of the java.util.concurrent library in Java 5 is a huge step forward for Java programmers. With my AspectJ hat on though, I can't help thinking that there's still more we could do...

Here's the fundamental observation: at one level, concurrency is about getting multiple objects to coordinate together at distinct points in the runtime execution of a program. There are a variety of multi-object protocols for the differing concurrency patterns. Furthermore, it's generally unlikely that this coordination is the primary concern of the modules in which it happens. So we have a multi-object protocol, somewhat distinct from the modules it is entangled in, and which relies on coordinating on events in the runtime execution of a program. A language that lets you encapsulate multi-object protocols, separate them from other modules, and that provides a first-class representation of events in the runtime execution of a program (we'll call them join points), sounds an excellent basis for writting concurrent applications. Even better if that language were to support pointcuts, giving the programmer the expressive power of abstraction and composition in selecting join points. Enter AspectJ :). Conceptually at least, it's a great fit to the problem. Of course, this is not a novel thought, way, way back on of the early predecessors to AspectJ was a language called COOL designed to assist in the development of concurrent object-oriented programs. What is new though, is the unique combination of factors that make the timing right :- the growing acceptance of aspect-oriented programming and of AspectJ, the increasing need for concurrency, the availability of Doug Lea's libraries in the Java 5 platform. The planets, as they say, seem to be aligning.

Now, I don't believe you can use an aspect-oriented programming language and just make all the hard problems simply go away. I do believe though that a library of aspects for concurrency, built on top of the java.util.concurrent libraries, could take us the next few steps and encapsulate more of the common usage patterns. This would hopefully save programmer mistakes and "make the code look more like the design". Anything that makes a concurrent design more readily apparent has got to be good.

I managed to snatch half an hour this afternoon to play with this idea just a little bit. Here's a simple program we can use as the basis for some concurrency experiments:

         
public class SpeedMeUp { private int[] myInts = new int[100]; public static void main(String[] args) { SpeedMeUp please = new SpeedMeUp(); please.doVoidLoop(); please.doIntLoop(); please.printSum(); } private void doVoidLoop() { for(int i = 0; i < 100; i++) { goFaster(); } System.out.println(); } private void doIntLoop() { for(int i = 0; i < 100; i++) { myInts[i] = goEvenFaster(i); } System.out.println(); } private void printSum() { int sum = 0; for (int i : myInts) sum += i; System.out.println("Sum = " + sum); } private void goFaster() { try { Thread.sleep(500); } catch (InterruptedException intEx) {} System.out.print("."); } private int goEvenFaster(int i) { try { Thread.sleep(500); } catch (InterruptedException intEx) {} System.out.print("."); return i; } } aspect Timing { pointcut timedEvent() : execution(* do*Loop*(..)); Object around() : timedEvent() { long start = System.currentTimeMillis(); Object ret = proceed(); long duration = System.currentTimeMillis() - start; System.out.println(thisJoinPoint + " took " + duration + " ms."); return ret; } }

If I compile and run this program as is, I see the output:

        
.................................................................................................... execution(void SpeedMeUp.doVoidLoop()) took 50072 ms. .................................................................................................... execution(void SpeedMeUp.doIntLoop()) took 50072 ms. Sum = 4950

Let's start with the easy loop to parallelize: doVoidLoop. We can speed that up no end with a single annotation:

        
@ParallelExecution private void goFaster() { try { Thread.sleep(500); } catch (InterruptedException intEx) {} System.out.print("."); }

With this annotation in place (and the Concurrency aspect we'll see next), the first loop now completes in 40ms instead of 5000. This support is very like the @Oneway tag supported by JBoss amongst others, but I choose to call it ParallelExecution because of my emphasis on concurrency here and because I want to extend the support to work with Futures shortly.

Here is the Concurrency aspect needed to support this basic tag (and yes, everything in this post is source compiled and working as-described with the latest AspectJ 5 development builds).

         
public aspect Concurrency { private ExecutorService executor = Executors.newCachedThreadPool(); public void shutdown() { executor.shutdown(); } pointcut voidParallelExecutionJoinPoint() : execution(@ParallelExecution void *.*(..)); pointcut futureParallelExecutionJoinPoint() : execution(@ParallelExecution Future *.*(..)); pointcut unsupportedParallelExecutionJoinPoint() : execution(@ParallelExecution * *.*(..)) && !voidParallelExecutionJoinPoint() && !futureParallelExecutionJoinPoint(); declare warning : unsupportedParallelExecutionJoinPoint() : "Methods scheduled for parallel execution must return either " + "void or a Future."; void around() : voidParallelExecutionJoinPoint() { executor.execute(new Runnable() { public void run() { proceed(); } }); return; } Future around() : futureParallelExecutionJoinPoint() { Future result = proceed(); if (result instanceof Runnable) { executor.execute((Runnable)result); } return result; } }

To get my application to terminate correctly, I added one more little aspect into the mix:

aspect ControlledShutdown { after() returning : execution(static void main(String[])) { Concurrency.aspectOf().shutdown(); } }

There are other more elegant techniques for this, but I'm only hacking to get a quick prototype up and running....

So that's the easy one dealt with, concurrent execution of a method with a void return type. Note that even in this case though, you can't just blindly use the annotation and hope things work out. If the method has side effects that need to complete before subsequent work happens in the thread you're going to need some redesign. I'm interested in making it easier to focus on the important issues in concurrency, and hiding some of the noise - not waving a magic wand.

Wouldn't it be great if you could do the same trick with the second loop, and just add the annotation to the goEvenFaster method too. If we try this:

@ParallelExecution private int goEvenFaster(int i) { try { Thread.sleep(500); } catch (InterruptedException intEx) {} System.out.print("."); return i; }

Then the AspectJ compiler will produce the following warning:

SpeedMeUp.java:59 [warning] Methods scheduled for parallel execution must return either void or a Future. @ParallelExecution private int goEvenFaster(int i) { ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ method-execution(int SpeedMeUp.goEvenFaster(int)) see also: Concurrency.aj:24::0 1 warning

Don't you just love design-level assertions :) (Take a look at the declare warning statement in the Concurrency aspect if you can't figure out why this message is coming out.

If a method returns a value, and we need it, we can't just kick of an asynchronous piece of work and never see it again, we need some way to get the return value. Java 5 supports this with the notion of a Future. I won't go into how they work here, suffice to say that a Future is like a promise that although you don't have the answer right now, you'll be able to get it when you need it.

What's the minimum we have to do to our program to get parallel execution of the second loop then? The warning message (and the source of the Concurrency aspect) should give you a clue - we need to change the goEvenFaster method to return a Future. The implications of this switch on the client are significant enough that I can't immediately see how a simple aspect could hide them, and it's not clear that it should even if it could.

Here's a second version of the goEvenFaster method that returns a future:

@ParallelExecution private Future<Integer> goEvenFaster2(final int i) { return new FutureTask<Integer>( new Callable<Integer>() { public Integer call() { return goEvenFaster(i); } } ); }

We need to update the loop method too to work with this. Here we see what I mean about not being able to hide this concurrency from the client. Compare the first and second versions of the doIntLoop method:

private void doIntLoop() { for(int i = 0; i < 100; i++) { myInts[i] = goEvenFaster(i); } System.out.println(); } private void doIntLoop2() { List<Future<Integer>> myFutures = new ArrayList<Future<Integer>>(); for(int i = 0; i < 100; i++) { myFutures.add(goEvenFaster2(i)); } for(int i = 0; i < 100; i++) { Future<Integer> future = myFutures.get(i); try { myInts[i] = future.get(); } catch (InterruptedException intEx) { System.err.println("It's rude to interrupt..."); } catch (ExecutionException exeEx) { System.err.println(exeEx.getCause()); } } }

The control structure has had to change significantly since we can't store the return values directly in the myInts array as we go along (this would entail waiting for each call to complete, and we don't want to do that. Instead there are two passes: the first kicks off all of the computations, and the second fills in all of the return values. (And yes, there are other designs you could use here, like passing the goEvenFaster2 method an index in the myInts array to fill-in once its computed the value - I'm just doing a bit of quick hacking here....).

Take a look at the Concurrency aspect to see how it schedules the Future-returning computations for execution using around advice. This version computes the sum in 530ms rather than 5000ms. I'm sure we could go further. Working with a Future inside the client is quite cumbersome for example. I'm pretty sure I'd be adding:

declare soft: InterruptedException : call(* Future.get()); declare soft: ExecutionException : call(* Future.get());

to my aspect, coupled with some afterThrowing advice to handle the exception cases after not too long. That would at least reduce some of the noise in doIntLoop2, getting it back to:

private void doIntLoop2() { List<Future<Integer>> myFutures = new ArrayList<Future<Integer>>(); for(int i = 0; i < 100; i++) { myFutures.add(goEvenFaster2(i)); } for(int i = 0; i < 100; i++) { Future<Integer> future = myFutures.get(i); myInts[i] = future.get(); } }

As you can see, I'm only just scratching the surface of the kinds of things that might be possible. A really good aspect library for concurrency would make a great project for someone to work on. We'll ship it with AspectJ as soon as its ready :)

Posted by adrian at January 23, 2005 10:21 PM [permalink]

Comments

If you haven't yet, you should take a look at a paper from OOPSLA 2004, Transparent proxies for Java futures. I'd love to see something similar implemented using aspects and annotations. I think explicitly managing futures is still too hard to make the technique widely applicable.

Posted by: Piotr Kaminski at January 23, 2005 11:22 PM

The @ParallelExecution idea is very similar to my Asynchronous Methods in Java with @sync attribute thingy... really good stuff :)

Posted by: Talip Ozturk at January 24, 2005 05:39 AM

Nice article, Adrian!

Just today, I was actually wondering, why AspectJ does not expose a pointcut matching synchronized blocks (not synchronized methods - this works). Is there a special reason for this or did you just not come across any use cases?

Cheers,
Eric

Posted by: Eric Bodden at January 26, 2005 07:55 PM

Hi there,

Thanks for the article.

Is there a PRINT this entry link ?

Thank you,

BR,
~A

Posted by: anjanbacchu [TypeKey Profile Page] at April 7, 2005 10:49 PM

Hi,

Thanks for the article. I am new to concurrent programming. Can you please explain me where you get the ExecutorService from. Also, it would be great if you could post a link to your source code.

Regards,
MB

Posted by: researcher [TypeKey Profile Page] at April 14, 2005 04:08 PM

Post a comment

Thanks for signing in, . Now you can comment. (sign out)

(If you haven't left a comment here before, you may need to be approved by the site owner before your comment will appear. Until then, it won't appear on the entry. Thanks for waiting.)


Remember me?