Thursday, February 07, 2008

Pod Racing: How to synchronize threads across multiple JVMs

Sometimes, you want to start the job in multiple JVMs only when all the VM has started. This scenario often comes up with multiplayer games. Let's take a pod racing game for example. The players need to start at the same time and each game server handles 1 player (for simplicity's sake)

Starting from Java 5, there's a CyclicBarrier to synchronize threads and is a perfect tool for this. However, it only handles threads in 1 VM. D'oh! Here's where Terracotta comes in. If you make that CyclicBarrier a shared object, Terracotta will handle the clustering for you, transparently (no API). All of a sudden, your CyclicBarrier object will be seen across all the VMs participated in the game.

Here's an example of how to do it. First, let's take a look at the PodRacer class
package demo;

import java.util.concurrent.CyclicBarrier;

public class PodRacer {
public final static int COUNT = 2;
private final CyclicBarrier barrier = new CyclicBarrier(COUNT);
private String name;

public PodRacer(String name) {
this.name = name;
}

public void ready() {
System.out.println(name + ": ready");
}

public void set() throws Exception {
/* in Terracotta world, all threads in different VMs will block here */
barrier.await();
}

public void go() throws Exception {
System.out.println(name + ": go");
Thread.sleep((int) (Math.random() * 5000) + 10);
System.out.println(name + ": arrived at " + System.currentTimeMillis());
}

public static void main(String[] args) throws Exception {
PodRacer racer = new PodRacer(System.getProperty("racer.name", "unknown"));
racer.ready();
racer.set();
racer.go();
}
}


Notice I hardcoded number of racers here to 2 but it can be made dynamic. If you run this class just like a normal Java program, it will block at racer.set() call because there's only 1 thread arriving at the barrier whereas it requires 2 for the barrier to be lifted. And if you don't use Terracotta underneath, it doesn't matter how many VMs you start, it will just block there.

Next, I'll add Terracotta into the mix and share the barrier object. That can be achieved by marking it in Terracotta configuration file tc-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<con:tc-config xmlns:con="http://www.terracotta.org/config">
<servers>
<server host="%i" name="localhost">
<dso-port>9510</dso-port>
<jmx-port>9520</jmx-port>
<data>terracotta/server-data</data>
<logs>terracotta/server-logs</logs>
</server>
</servers>
<clients>
<logs>terracotta/client-logs</logs>
</clients>
<application>
<dso>
<instrumented-classes/>
<roots>
<root>
<field-name>demo.PodRacer.barrier</field-name>
</root>
</roots>
</dso>
</application>
</con:tc-config>


And that's it. When I start 2 JVMs running PodRacer class with Terracotta, I will efficiently have 2 threads calling racer.set() on the shared barrier

To make it even easier to try out with Terracotta, there's a Maven 2 plugin that will handle starting up your project with Terracotta enabled. Here's the pom.xml of my project:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>PodRacing</groupId>
<artifactId>PodRacing</artifactId>
<version>0.0.1</version>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
<plugin>
<groupId>org.terracotta.maven.plugins</groupId>
<artifactId>tc-maven-plugin</artifactId>
<version>1.0.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>bootjar</goal>
</goals>
</execution>
</executions>

<configuration>
<processes>
<process nodeName="racer1"
className="demo.PodRacer"
jvmargs="-Dracer.name=Anakin"/>
<process nodeName="racer2"
className="demo.PodRacer"
jvmargs="-Dracer.name=Sebulba"/>
</processes>

</configuration>
</plugin>
</plugins>
</build>
<pluginRepositories>
<pluginRepository>
<releases />
<snapshots />
<id>terracotta</id>
<url>http://download.terracotta.org/maven2</url>
</pluginRepository>
</pluginRepositories>
</project>


See how I defined 2 racers through the process element in the plugin configuration. That will start 2 distinct VMs with Terracotta enabled. The output looks something like this:

[INFO] Starting DSO nodes
[INFO] Starting node racer1: c:\jdk\jdk1.6.0_02\jre/bin/java.exe -Dcom.tc.l1.modules.repositories=file:/C:/Users/hhuynh/.m2/repository/ -Dtc.nodeName=racer1 -Dtc.numberOfNodes=2 -Dtc.config=d:\work\workspace\projects\PodRacing\tc-config.xml -Dtc.classpath=file:/c:/Users/hhuynh/AppData/Local/Temp/tc-classpath37736.tmp -Dtc.session.classpath=/C:/Users/hhuynh/.m2/repository/org/terracotta/tc-session/2.5.0/tc-session-2.5.0.jar -Dcom.tc.l1.modules.repositories=file:/C:/Users/hhuynh/.m2/repository/ -Xbootclasspath/p:d:\work\workspace\projects\PodRacing\target\dso-boot.jar -Dracer.name=Anakin -cp d:\work\workspace\projects\PodRacing\target\classes; demo.PodRacer
[INFO] Starting node racer2: c:\jdk\jdk1.6.0_02\jre/bin/java.exe -Dcom.tc.l1.modules.repositories=file:/C:/Users/hhuynh/.m2/repository/ -Dtc.nodeName=racer2 -Dtc.numberOfNodes=2 -Dtc.config=d:\work\workspace\projects\PodRacing\tc-config.xml -Dtc.classpath=file:/c:/Users/hhuynh/AppData/Local/Temp/tc-classpath37737.tmp -Dtc.session.classpath=/C:/Users/hhuynh/.m2/repository/org/terracotta/tc-session/2.5.0/tc-session-2.5.0.jar -Dcom.tc.l1.modules.repositories=file:/C:/Users/hhuynh/.m2/repository/ -Xbootclasspath/p:d:\work\workspace\projects\PodRacing\target\dso-boot.jar -Dracer.name=Sebulba -cp d:\work\workspace\projects\PodRacing\target\classes; demo.PodRacer
[INFO] ------------------------------------------------------------------------

[INFO] [racer2] Sebulba: ready
[INFO] [racer1] Anakin: ready
[INFO] [racer1] Anakin: go
[INFO] [racer2] Sebulba: go
[INFO] [racer2] Sebulba: arrived at 1202406914274
[INFO] Finished node racer2
[INFO] [racer1] Anakin: arrived at 1202406915125
[INFO] Finished node racer1


Ouch, Anakin sucked!

Download the Maven project and try it out. Have fun.

P.S. More about sharing memory between JVMs here http://unserializableone.blogspot.com/2006/10/share-precious-heap-memory-accross.html or visit Terracotta

3 comments:

ngonidan said...

Excellent!!!! You hammered right to the marrow!!!

Just a few additions, to save a few hairs on the heads of other programmers


***I could not insert a full version of my additions due to restrictions on this server, which disallowed any special characters****


To compile the above java file, just go to your working directory , the file is in a package so you have to compile and run carefully.

mkdir classes
javac -d classes PodRacer.java



To run as an ordinary java file, simply use the java command


java -cp classes demo.PodRacer

** the –cp switch means class search path of directories and zip/jar files*****

Output


unknown: ready

***Note it stops at ready, even if you open another command prompt to run another thread, still they all stop there ****


So how do we synchronize the threads, we use Terracotta.

2) The tc-config.xml file has a few errors due to those multiple back slashes,remove them or use a basic tc-config.xml file found at terracotta.com, it worked fine

And that's it. Now to run the file, but this time NOT as an ordinary java file, but using java environment in Terracotta, open 2 command prompts, and Run the application using the Terracotta dso-java script found again in the Terracotta bin folder:

Output:

unknown: ready
unknown: go
unknown: arrived at 1204458149687

***Note use the complete path to dso-java.bat given above, it’s required****
**Also take note of the temporary pause of the first thread waiting for the second one***


**any further questions, get me at ngonidan@yahoo.com *****

Unknown said...

Hi, Nice post. I am new to blogger, I was wondering how you have formatted your source code in blogger?

Unknown said...

I use this tool http://code.google.com/p/syntaxhighlighter