Threads

On a multi-processor machine you may wish to evaluate the same expression, or set of expressions, in multiple threads. There are two main ways of evaluation in multiple threads:

  1. Use the ThreadSafeEvaluator which can use the same expression in multiple threads.
  2. Use ImportationVisitor to give each thread its own copy of an expression.

In both cases each thread will need its own Jep instance. Each Jep instance will have its own Evaluator and VaraibleTable but may share other components. The LightweightComponentSet is an easy way to create a Jep instance with minimal memory footprint. The second technique is slightly faster.

Using ThreadSafeEvaluator

Normally variables are evaluated by using a direct reference from a Node to a Variable object. This would not be thread safe as one thread might change the value of a variable. With the ThreadSafeEvaluator each thread has its own VariableTable and when the evaluator encounters a variable node in an expression it looks up its name in the VariableTable, effectively performing a hashtable lookup and preserving thread independence.

The Jep instance needs to be set up with

// create a Jep instance with the ThreadSafeEvaluator
Jep baseJep = new Jep(new ThreadSafeEvaluator());
        
// use thread-safe versions of the assignment and element-of operators
baseJep.getOperatorTable().getAssign().setPFMC(new ThreadSafeAssign());
baseJep.getOperatorTable().getEle().setPFMC(new ThreadSafeEle());

// use thread optimized version of the rand function
baseJep.addFunction("rand", new ThreadSafeRandom());

Each child thread would be set up with

// create a child Jep instance
Jep childJep = new Jep(new LightWeightComponentSet(baseJep));

A full example is

import com.singularsys.jep.EvaluationException;
import com.singularsys.jep.Jep;
import com.singularsys.jep.JepException;
import com.singularsys.jep.Variable;
import com.singularsys.jep.misc.LightWeightComponentSet;
import com.singularsys.jep.misc.threadsafeeval.ThreadSafeAssign;
import com.singularsys.jep.misc.threadsafeeval.ThreadSafeEle;
import com.singularsys.jep.misc.threadsafeeval.ThreadSafeEvaluator;
import com.singularsys.jep.parser.Node;
        
public class ThreadRunner {

    // Setup and run multiple threads using the same expression
    public void go(String expression, int nThreads) throws JepException {
        // create a Jep instance with the ThreadSafeEvaluator
        Jep baseJep = new Jep(new ThreadSafeEvaluator());
        
        // use thread-safe versions of the assignment and element-of operators
        baseJep.getOperatorTable().getAssign().setPFMC(new ThreadSafeAssign());
        baseJep.getOperatorTable().getEle().setPFMC(new ThreadSafeEle());
        
        // Parse a node in the base Jep instance
        Node baseNode = baseJep.parse(expression);
             
        // create a number of threads each with a different value for x
        EvaluationThread threads[] = new EvaluationThread[nThreads];
        for(int i=0; i<nThreads; ++i) {
            threads[i] = new EvaluationThread(baseJep,baseNode,"x", Math.PI * i / nThreads );
        }
        
        // run the threads each with a different value for x
        for(int i=0; i<nThreads; ++i) {
            threads[i].start();
        }

        // wait for all threads to finish and print results
        for(int i=0; i<nThreads; ++i) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
            }
            System.out.println("Thread "+i+" value "+ threads[i].varValue+" result "+threads[i].result);
        }
    }
        
    // Class to evaluate an expression in a thread
    class EvaluationThread extends Thread {
        Jep childJep;
        Node childNode;
        Variable childVar;
        double varValue;
        double result;
        
        // set up the tread before running 
        EvaluationThread(Jep baseJep, Node baseNode, String varName, double value) throws JepException {
            // create a child Jep instance
            childJep = new Jep(new LightWeightComponentSet(baseJep));
            // just use the baseNode node
            childNode = baseNode;
            // child copy of variable
            childVar = childJep.addVariable(varName);
            varValue = value;
        }
        
        // Run the thread
        @Override
        public void run() {
            try {
                // set variable value
            	childVar.setValue(varValue);
                // Evaluate the expression
                Object res = childJep.evaluate(childNode);
                result = ((Double) res);
            } catch (JepException e) {
                System.out.println(e.getMessage());
            }
        }            threads[i].start();
        
   }
}           

Using ImportationVisitor

The ImportationVisitor can import an expression from one jep instance to another

  ImportationVisitor iv = new ImportationVisitor(Jep childJep);
  Node childNode = iv.deepCopy(baseNode); 

This makes a copy of the expression changing references from one VariableTable to another. The new expression in childNode can then be evaluated using any evaluator.

The code to use this is very similar to the above, apart from simpler Jep construction, and the line to import the node.

import com.singularsys.jep.EvaluationException;
import com.singularsys.jep.Jep;
import com.singularsys.jep.JepException;
import com.singularsys.jep.Variable;
import com.singularsys.jep.misc.LightWeightComponentSet;
import com.singularsys.jep.parser.Node;
import com.singularsys.jep.walkers.ImportationVisitor;
        
public class ThreadRunner {

    // Setup and run multiple threads using the same expression
    public void go(String expression, int nThreads) throws JepException {
        // create a standard Jep
        Jep baseJep = new Jep();

        // use thread optimized version of the rand function
        baseJep.addFunction("rand", new ThreadSafeRandom());
                
        // Parse a node in the base Jep instance
        Node baseNode = baseJep.parse(expression);
             
        // create a number of threads each with a different value for x
        EvaluationThread threads[] = new EvaluationThread[nThreads];
        for(int i=0; i<nThreads; ++i) {
            threads[i] = new EvaluationThread(baseJep,baseNode,"x", Math.PI * i / nThreads);
        }

        // run the threads each with a different value for x
        for(int i=0; i<nThreads; ++i) {
            threads[i].start();
        }
        
        // wait for all threads to finish and print results
        for(int i=0; i<nThreads; ++i) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
            }
            System.out.println("Thread "+i+" result "+threads[i].result);
        }
    }
        
    // Class to evaluate an expression in a thread
    class EvaluationThread extends Thread {
        Jep childJep;
        Node childNode;
        Variable childVar;
        double varValue;
        double result;
        
        // set up the tread before running 
        EvaluationThread(Jep baseJep, Node baseNode, String varName, double value) throws JepException {
            // create a child Jep instance
            childJep = new Jep(new LightWeightComponentSet(baseJep));
            // use a child copy of expression
            childNode = (new ImportationVisitor(childJep)).deepCopy(baseNode);
            // child copy of variable
            childVar = childJep.addVariable(varName);
            varValue = value;
        }
        
        // Run the thread
        @Override
        public void run() {
            try {
                // set the variable value
                childVar.setValue(varValue);
                // Evaluate the expression
                Object res = childJep.evaluate(childNode);
                result = ((Double) res);
            } catch (EvaluationException e) {
                System.out.println(e.getMessage());
            }
        }
   }
}           

This method does not require special versions of the assignment and element of operators. If the rand() function is used then performance is improved by using the ThreadSafeRandom.

A slight variation of the above technique is to use a SerializableExpression. This can handle much longer expressions than ImportationVisitor. To create a child copy of a node use

SerializableExpression se = new SerializableExpression(baseNode);
childNode = se.toNode(childJep);

See the Serialization help page for more details.

Light-weight Jep instances

Creation of new Jep instances can have a considerable memory footprint, a Jep instance with a StandardParser takes about 56kB bytes for a Jep instance with a configurable parser takes about 14kB bytes. Its possible to create a light-weight Jep instance which reuses components from an existing Jep instance, such instances only take 1kB.

Light-weight instances have no parsing facility or printing, and their own copies of the VariableTable and Evaluator so are safe to uses in multiple threads.

Jep j = new Jep();
ComponentSet cs = new LightWeightComponentSet(j);
Jep lwj = new Jep(cs); 

Note that the new instance will share the FunctionTable and its associated PostfixMathCommands. See the LightWeightComponentSet LightWeightComponentSet documentation for issues which mary arise and workarounds. The above code will create copies of all variables. The public LightWeightComponentSet(Jep jep,boolean copyConstants) constructor can be used to just copy constants or leave the table empty.

The JepComponent interface defines a getLightWeightInstance() method which is used to create a light weight version of the component suitable for multiple thread use. It is typical for JepComponents to either return this, null as appropriate. Some JepComponents have more complex implementation.

There are two special classes providing do-nothing implementations with minimal footprint. These are both accessed by singleton static fields: NullParser.NULL_PARSER and PrintVisitor.NULL_PRINT_VISITOR.

Example applications

Two diagnostic applications com.singularsys.jepexamples.diagnostics.ThreadSafeSpeedTest com.singularsys.jepexamples.diagnostics.ThreadSpeedTest are available for testing the two different approaches. The first uses the ThreadSafeEvaluator and the second uses the ImportationVisitor. Both evaluate the same expression with half a million different values and compare the results when the work is split over multiple threads. Results will depend on the number of processors available and other tasks running on the system.

The com.singularsys.jeptests.system.ThreadTest runs a number of JUnit tests on the system, including the examples in this page.