Skip to content

Commit 541b00b

Browse files
authored
Make lazy initialization of static Val.Obj thread-safe (#136)
Static Val.Obj instances are created by the optimizer and will end up in the parse cache. If the cache is shared by multiple threads, initialization must be sufficiently safe, i.e. computing a value multiple times in race conditions is allowed (and cheaper than ensuring that it doesn't happen), but object must never get into an invalid intermediate state.
1 parent 8193180 commit 541b00b

3 files changed

Lines changed: 66 additions & 6 deletions

File tree

bench/src/main/scala/sjsonnet/MainBenchmark.scala

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ object MainBenchmark {
3131
interp.interpret0(interp.resolver.read(path).get, path, renderer).getOrElse(???)
3232
(parseCache.keySet.toIndexedSeq, interp.evaluator)
3333
}
34+
35+
def createDummyOut = new PrintStream(new OutputStream {
36+
def write(b: Int): Unit = ()
37+
override def write(b: Array[Byte]): Unit = ()
38+
override def write(b: Array[Byte], off: Int, len: Int): Unit = ()
39+
})
3440
}
3541

3642
@BenchmarkMode(Array(Mode.AverageTime))
@@ -42,11 +48,7 @@ object MainBenchmark {
4248
@State(Scope.Benchmark)
4349
class MainBenchmark {
4450

45-
val dummyOut = new PrintStream(new OutputStream {
46-
def write(b: Int): Unit = ()
47-
override def write(b: Array[Byte]): Unit = ()
48-
override def write(b: Array[Byte], off: Int, len: Int): Unit = ()
49-
})
51+
val dummyOut = MainBenchmark.createDummyOut
5052

5153
@Benchmark
5254
def main(bh: Blackhole): Unit = {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package sjsonnet
2+
3+
import java.util.concurrent.{ExecutorService, Executors, TimeUnit}
4+
5+
import org.openjdk.jmh.annotations._
6+
import org.openjdk.jmh.infra._
7+
8+
import scala.collection.mutable
9+
10+
@BenchmarkMode(Array(Mode.AverageTime))
11+
@Fork(4)
12+
@Threads(1)
13+
@Warmup(iterations = 30)
14+
@Measurement(iterations = 40)
15+
@OutputTimeUnit(TimeUnit.MILLISECONDS)
16+
@State(Scope.Benchmark)
17+
class MultiThreadedBenchmark {
18+
19+
val threads = 8
20+
21+
@Benchmark
22+
def main(bh: Blackhole): Unit = {
23+
val cache: ParseCache = new ParseCache {
24+
val map = new mutable.HashMap[(Path, String), Either[Error, (Expr, FileScope)]]()
25+
override def getOrElseUpdate(key: (Path, String), defaultValue: => Either[Error, (Expr, FileScope)]): Either[Error, (Expr, FileScope)] = {
26+
var v = map.synchronized(map.getOrElse(key, null))
27+
if(v == null) {
28+
v = defaultValue
29+
map.synchronized(map.put(key, v))
30+
}
31+
v
32+
}
33+
}
34+
35+
val pool: ExecutorService = Executors.newFixedThreadPool(threads)
36+
val futs = (1 to threads).map { _ =>
37+
pool.submit { (() =>
38+
if(SjsonnetMain.main0(
39+
MainBenchmark.mainArgs,
40+
cache, // new DefaultParseCache
41+
System.in,
42+
MainBenchmark.createDummyOut,
43+
System.err,
44+
os.pwd,
45+
None
46+
) != 0) throw new Exception): Runnable
47+
}
48+
}
49+
var err: Throwable = null
50+
bh.consume(futs.map { f =>
51+
try f.get() catch { case e: Throwable => err = e }
52+
})
53+
pool.shutdown()
54+
if(err != null) throw err
55+
}
56+
}

sjsonnet/src/sjsonnet/Val.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,12 @@ object Val{
155155

156156
private[this] def getValue0: util.LinkedHashMap[String, Obj.Member] = {
157157
if(value0 == null) {
158-
value0 = new java.util.LinkedHashMap[String, Val.Obj.Member]
158+
val value0 = new java.util.LinkedHashMap[String, Val.Obj.Member]
159159
allKeys.forEach { (k, _) =>
160160
value0.put(k, new Val.Obj.ConstMember(false, Visibility.Normal, valueCache(k)))
161161
}
162+
// Only assign to field after initialization is complete to allow unsynchronized multi-threaded use:
163+
this.value0 = value0
162164
}
163165
value0
164166
}

0 commit comments

Comments
 (0)