Package com.singularsys.jep.misc.parallelparsing


package com.singularsys.jep.misc.parallelparsing

Classes for parallel parsing of equations. To allow parallel parsing you need a separate parser for each thread, and a thread-safe variable table.

ConcurrentVariableTable

The ConcurrentVariableTable is a VariableTable backed by a ConcurrentHashMap. The ConcurrentVariableTable.addVariable(String) are atomic operations ensuring that variables are created only once.

ParallelConfigurableParser

The ParallelConfigurableParser is a ConfigurableParser that is safe to use in multiple threads. The only parings method supported is ParallelConfigurableParser.parse(Reader) which creates new instances of the Tokenizer and GrammarParser for each invocation, so is safe to use in multiple threads.

The ParallelConfigurableParser.restart(java.io.Reader) and ParallelConfigurableParser.continueParse() methods are not implemented as they are not suitable for multiple threaded used. If you need to parse multiple expressions in parallel you can use the ParallelConfigurableParser.newMultipleEquationParser(java.io.Reader) method to create a MultipleEquationParser in each thread. The MultipleEquationParser.continueParsing() method returns the result of parsing the next expression in the input.

To provide compatibility with the Jep class when using the ParallelConfigurableParser, the MultipleEquationParsingJep class provides implementations of the Jep.initMultiParse(java.io.Reader) and Jep.continueParsing() methods that work in a single threaded environment by using a single MultipleEquationParser.

The Jep class can be created as follows:

 Jep jep = new Jep(
     new ParallelConfigurableParser(new StandardConfigurableParser()),
     new ConcurrentVariableTable());
 

A set of equations can be parsed in parallel as follows:

 List<String> equations = new ArrayList<>(
                List.of("x=1","y=2","z=x+y"));
 // Service for executing threads
 ExecutorService service = Executors.newFixedThreadPool(20);
 // Used to wait for all threads to complete
 CountDownLatch latch = new CountDownLatch(equations.size());
 // The nodes created by the threads
 ConcurrentLinkedQueue<Node> nodes = new ConcurrentLinkedQueue<>();
 // Used to record failures 
 AtomicInteger failures = new AtomicInteger();
 // loop over the equations, starting a new thread for each
 for (String eqn : equations) {
     service.execute(() -> {
         try {
               // Parse the equation
             var node = jep.parse(eqn);
               // Add the node to the list
             nodes.add(node);
         } catch (ParseException e) {
             failures.incrementAndGet();
         }
         latch.countDown();
     });
 }
 // Wait for all threads to complete
 latch.await();
 // Print the parsed nodes. Order is random 
 for (var node : nodes) {
     jep.println(node);
 }

Creating a new parser per thread

An alternative to using the ParallelConfigurableParser is to use ConfigurableParser#ConfigurableParser(Jep, ConfigurableParser) constructor, to build a new ConfigurableParser instance for each thread that reuses the TokenMatchers, TokenFilters, and GrammarMatchers from an existing ConfigurableParser instance.

 Jep jep = new Jep(
     new StandardConfigurableParser(),
     new ConcurrentVariableTable());

 List<String> equations = new ArrayList<>(List.of("x=1", "y=2", "z=x+y"));
 // Service for executing threads
 ExecutorService service = Executors.newFixedThreadPool(numberOfThreads);
 // Used to wait for all threads to complete
 CountDownLatch latch = new CountDownLatch(equations.size());
 // The nodes created by the threads
 ConcurrentLinkedQueue<Node> nodes = new ConcurrentLinkedQueue<>();
 // Used to record failures
 AtomicInteger failures = new AtomicInteger();
 // loop over the equations, starting a new thread for each
 for (String eqn : equations) {
    service.execute(() -> {
       try {
           var myCp = new ConfigurableParser(jep, (ConfigurableParser) jep.getParser());
           // Parse the equation
           var node = myCp.parse(new StringReader(eqn));
           // Add the node to the list
           nodes.add(node);
       } catch (ParseException e) {
           failures.incrementAndGet();
       }
       latch.countDown();
    });
 }
 // Wait for all threads to complete
 latch.await();
 // Print the parsed nodes. Order is random
 for (var node : nodes) {
     jep.println(node);
 }

This is marginally faster. Slower alternatives build a new StandardConfigurableParser or StandardParser instance in each thread. A detailed comparison of the different methods can be found at ParallelParsingTest.

See Also: