Blocking Operations Within Script
To avoid concurrency issues with mutable state, the scripting engine is designed as an Active Object with operations performing on a single thread. Therefore special care needs to be taken while performing blocking operations within the script, since blocking the executing thread will also prevent other tasks, such as event handlers, from being executed and could lead to unexpected deadlocks.
See the Sequencer Technical Documentation for more information on the Scripting Engine design and Active Objects.
All the CSW related DSL’s provided in the scripting environment do not block main script thread. Hence, they can be used directly without worrying about stalling the script.
There is one exception to this: callback based API’s, such as an onEvent
handler. Here, you would not want to perform CPU/IO intensive tasks without following one of the patterns mentioned below.
There are three types of blocking operations identified here, each with its own patterns and recommendations to be followed while performing it.
- CPU Bound
- IO Bound
- Sleep
To handle the first two types of blocking operations, the DSL provide the blockingCpu
and blockingIo
constructs. These constructs cause the script execution engine to use different thread pools instead of the main script thread to execute the blocking code.
You can read more about these patterns of blocking here
The techniques mentioned here for performing blocking tasks causes them to be executed on a different thread than the main sequencer script. Therefore, accessing/updating mutable state defined in the main script from these blocking functions is not thread safe.
We recommend creating separate kotlin (.kt) file/files and maintaining all the CPU/IO bound blocking tasks there, to reduce the risk of improperly accessing mutable state.
CPU Bound
For any CPU bound operations follow these steps:
- Create a new function and mark it with the
suspend
keyword. - Wrap the function body inside a
blockingCpu
utility function. - Call the new function. Wrap it in an
async
block to run it in the background.
The following example demonstrates writing a CPU bound operation. In this example, the method BigInteger.probablePrime(4096, Random())
is CPU bound and takes more than few seconds to finish. We wrap it in another method, findBigPrime
and mark it with the suspend
keyword.
- Kotlin
-
source
// Calculating probablePrime is cpu bound operation and should be wrapped inside blockingCpu utility function // Following function takes around 10 seconds to find a 4096 bit length prime number suspend fun findBigPrime(): BigInteger = blockingCpu { BigInteger.probablePrime(4096, Random()) }
The following shows the usage of the above compute heavy function in main sequencer script in two ways. First it is called in the main script flow causing the main script to pause until the method completes with a value. Note that background tasks continue to execute while the method is being executed. In the second way, an async
block is used, which causes the method to be executed in the background. The async
block returns a Deferred
type, and the Deferred.await()
method can be used to pause script execution until the async
block completes without blocking the other background processes.
- Kotlin
-
source
script { loopAsync(100.milliseconds) { // loop represents the computation running on the main script thread. } onSetup("prime number") { // by default calling findBigPrime cpu intensive task suspends and waits for result // but this runs on different thread than the main script thread // which allows other background tasks started previously to run concurrently val bigPrime1: BigInteger = findBigPrime() // if you want to run findBigPrime in the background, then wrap it within async val bigPrimeDeferred: Deferred<BigInteger> = async { findBigPrime() } // ... // wait for compute intensive operation to finish which was previously started val bigPrime2: BigInteger = bigPrimeDeferred.await() // script continues... } }
IO Bound
For any IO bound operations follow these steps:
- Create a new function and mark that with
suspend
keyword. - Wrap function body inside
blockingIo
utility function. - Call the new function. Wrap it in an
async
block to run it in the background.
The following example demonstrates writing IO bound operation. In this example the method BufferredReader.readLine()
is IO bound and takes more than few seconds to finish. We wrap it in another method, readMessage
and mark it with the suspend
keyword.
- Kotlin
-
source
// Reading a line from a file is blocking IO operation and should be wrapped inside blockingIo utility function suspend fun readMessage(bufferedReader: BufferedReader): CharSequence? = blockingIo { bufferedReader.readLine() }
The following shows the usage of the above IO heavy function in main sequencer script, again in two ways: in main script flow and also using an async
block to execute in the background.
- Kotlin
-
source
script { loopAsync(100.milliseconds) { // loop represents the computation running on the main script thread. } onSetup("read file") { val reader = File("someFile.txt").bufferedReader() // by default calling readMessage (blocking io) task suspends and waits for result // but this runs on different thread than the main script thread // which allows other background tasks started previously to run concurrently val message1 = readMessage(reader) // if you want to run readMessage in the background, then wrap it within async val message2Deferred = async { readMessage(reader) } // ... // wait for blocking operation to finish which was previously started val message2: CharSequence? = message2Deferred.await() // script continues... } }
Sleep
There may be cases where you simply need to pause execution of your script for a specific amount of time. Using a traditional sleep
or wait
command performs as expects, pausing the executing thread, but due to the nature of the Active Object, these block background handlers as well. Therefore, the Kotlin Coroutine package provides a method, delay
, that should be used to prevent this blocking. Like sleep
, delay
takes the number of milliseconds to pause as an argument.